iOS绘制性能调研

iOS,绘制

Posted by Karim on August 30, 2018

前言

在上篇iOS截图引起的思考留下了一些疑问,会在这篇全部解开。
在上一篇结尾的时候,我提出了:

即使是相同的path,drawRect也是有可能会比CAShapeLayer要快的。

正文

先来看两段贝塞尔画出来的路径:

  1. 这一段路径是在悦跑圈实际遇到的轨迹情况
    func setupAPath() -> UIBezierPath {
     let bezier = UIBezierPath()
     //https://github.com/Fidetro/drawPath/blob/master/DrawPath/test.plist
     let plistPath = Bundle.main.path(forResource: "test", ofType: "plist")!
     let points = (NSArray.init(contentsOfFile: plistPath) as! [String]).map{CGPointFromString($0)}
     for (index,point) in points.enumerated() {
         if index == 0 {
             bezier.move(to: point)
         }else{
             bezier.addLine(to: point)
         }
     }
     return bezier
    }
    
  2. 这一段路径是两点之间来回绘制5000次
    func setupBPath() -> UIBezierPath {
     let bezier = UIBezierPath()
     for index in 0..<10000 {
         if index == 0 {
             bezier.move(to: CGPoint.init(x: 0, y: 0))
         }else{
             let point = index%2==0 ? CGPoint.init(x: 100, y: 100) : CGPoint.init(x: 0, y: 0)
             bezier.addLine(to: point)
         }
     }
     return bezier
    }
    

这两段路径分别使用CAShapeLayerCGContext绘制,下面是粗略统计的耗时时间:

耗时 APath BPath
CAShapeLayer 3~4s 5~6min
drawRect 4~5min 5~6s

这里有一点要注意的是,CGContext会消耗CPU和内存,使用Xcode或者Instruments可以发现,在绘制APath的时候CPU使用率基本在90%以上,CAShapeLayer使用GPU绘制,但仍然会造成卡顿。(还没找到监测GPU使用的方法,如果有可以评论告诉我。

要了解造成这两者之间差异的原因,就得知道CAShapeLayerCGContext的由来。

在上一篇中已经简单介绍过,从官方文档中可以得知,CAShapeLayer是基于Core Animation上封装的类,用于绘制抗锯齿形状图层。而CGContext是使用Core Graphics。光知道他们调用是底层是什么,并不能够解决现在的问题,还需要更深入去了解Core AnimationCore Graphics

Core Animation

Core AnimationMetalCore Graphics更上层的框架。使用Core Animation绘制,只需要设置一些参数(例如起点和终点),然后Core Animation会将绘制工作交给GPU去渲染。

CAShapeLayer

同时关于CAShapeLayer,苹果更建议我们在一些简单的路径的时候去使用。
CAShapeLayer在合成的时候根据提供的路径去渲染位图,优势是尽可能的用更好的分辨率去绘制路径,但是这种优势是要以额外的渲染时间作为代价,如果提供的路径非常复杂,那么可能会导致栅格化的代价十分昂贵。同时如果图层大小不断变化,导致经常需要重新绘制,那么绘制花费的时间最终可能会变成性能瓶颈。

Core Graphics

Core Graphics也叫做Quartz 2D,是二维绘图引擎,Quartz 2D提供低级、轻量级2D渲染,很方便实现透明层,基于路径的绘图,离屏渲染,高级颜色管理,消除锯齿渲染以及PDF创建,显示和解析。

Core Graphics建议我们在遇到以下情况的时候使用CGLayer去绘制。

  • 重复使用高质量的离屏渲染。例如你在构建一个需要复用的场景时候,将背景绘制在图层上,然后在需要的时候绘制图层。
  • 重复绘制。如果你打算绘制一个由多个相同的项目组成的图层,将其绘制到CGLayer上,能得到性能提升。(参考下图)
  • 缓冲。虽然你可能会因此使用layer,但是Quartz Compositor并不需要缓冲,如果你必须绘制到缓冲区,请使用layer而不是context。

12-1

使用CGLayerRef能实现最佳性能,Quartz会根据关联的图形上下文来缓存CGLayer。例与视显卡相关联的图形上下文可以缓存显卡上的图层,这会让绘制图层中的内容比从位图上下文构造的图像渲染的更快。因为,与位图图形上下文相比,Layer通常是离屏渲染的更好选择。

CGLayerRef的使用可以参考下面这段代码,CGLayerRef在swift中对应的是CGLayer

        let context = UIGraphicsGetCurrentContext()!
        let layer = CGLayer.init(context, size: rect.size, auxiliaryInfo: nil)
        let layerCtx = layer!.context!
        layerCtx.beginPath()
        layerCtx.move(to: CGPoint.init(x: 0, y: 0))
        layerCtx.addLine(to: CGPoint.init(x: 100, y: 100))
        layerCtx.setLineWidth(2)
        layerCtx.setStrokeColor(UIColor.red.cgColor)
        layerCtx.strokePath()
        layerCtx.closePath()
        context.draw(layer!, in: self.frame)

总结

通过两者文档的对比,似乎CGLayerRefCAShapLayer要更好,但是实际上,在绘制复杂的路径的时候,个人还是更偏向使用CAShapeLayer,大部分情况下,CPU的性能比GPU的性能更加昂贵,在两者之间选择的时候,应该根据实际要求,如果需要更高质量的图形渲染,应该选择Core Graphics
另外在刚开始的时候提出CAShapeLayer绘制BPath路径的时候会更加卡顿的问题,尽管苹果推荐我们CAShapeLayer应该去绘制简单的路径,但是苹果也在文档中给出了解决的方法: 通过将复杂的形状分解成更简单的形状可以减少绘制花费的时间,将多个CAShapeLayer对象叠加在一起要比单独绘制一个CAShapeLayer对象快得多。因为绘制操作是在CPU上,而合成是在GPU上。但是这部分的性能优化,需要根据实际的情况去制定基线。

参考链接

Core Animation Programming Guide
Quartz 2D Programming Guide
示例Demo


请保持转载后文章内容的完整,以及文章出处。本人保留所有版权相关权利。

分享到: