开发杂谈iOS开发之常用技术点

iOS 2D Graphic(2)—— Performance

2016-07-21  本文已影响372人  ac3

在本系列上一篇《iOS 2D Graphic(1)—— Concept 基本概念和原理》中,我们已经了解了关于iOS 图形图像的基本要素。在过去相当长的一段时间里,较之于Android,优秀顺畅的UI操作体验一直是iOS引以为豪的地方。这个不仅和iOS的UI操作线程设计机制有关,也与iOS图形图像上对性能这部分的深度优化有关。但是虽然Apple替我们做了很多优化的动作,在实际开发中,如果不注意和图形图像相关的性能损失点,仍然会造成App的性能问题。这篇将重点关注如何处理图形图像的性能问题。


CPU Bound vs. GPU Bound

首先,我们需要理解两个不同的性能影响因素:CPU约束型(CPU bound) 和 GPU约束型(GPU bound)。
我们回过头来看看上一篇中提到的一个完整的Core Animation图形操作,需要经过哪几步:

Core Animation Pipeline.png

搞清楚了这个步骤,那顾名思义CPU bound和GPU bound的概念,就意味着影响性能的操作可以分为两种:前者主要集中在CPU上,也就是说对应于上图中的1~2步;后者主要集中在GPU上,对应于上图的第3步骤。

这两种不同的场景,直接决定了使用不同的方式来性能,CPU bound的场合下,需要减轻CPU的压力,而把一部分工作转交给GPU更擅长的方式去处理;而在GPU bound的场合下,需要减轻GPU的压力,把一部分工作提前交给CPU去做预处理。当然了,你要是说那CPU和GPU都很忙怎么办?这种情况下,就可能需要你重新设计系统的架构,然后不断的调试,不断的验证了。

那么,怎么区分你的App在出现性能问题时,是CPU bound还是GPU bound呢?这个时候,你需要一些工具来帮助你分析问题的根源所在。最主要的工具就是Instrument。


Instrument

“工欲善其事,必先利其器”
-- 《论语》

Apple提供的Instrument是一个很强大的测试平台工具,打开Instrument,你可以看到很多小工具能针对性的提供不同的功能,这里主要强调和Graphic相关的两个:Core Animation instrument和OpenGL ES Driver instrument。

1. Core Animation Instrument
Core Animation Instrument.png

选择Core Animation Instrument后,在面板内你能看到系统提供的默认两个不同的Profiler,一个是Core Animation(图中1),一个是Time Profiler(图中2)。Core Animation Profiler中,你能看到最主要的一个信息就是Frame Rate。这个值是判定你的App有没有UI性能问题的主要标准,一切的一切优化目标都是要求Frame Rate达到60帧每秒(60 fps)!所以当你看到这个值明显低于55-60的时候,你就可以确定你的App需要优化。

而Time Profiler则直观的显示了你的CPU Utilization,在这里你能看到最耗CPU时间的操作和调用。也就是说,这里是分析CPU Bound问题的切入点,在这里找到最耗时间的操作,然后再去找到应对措施。

Profilers in Core Animation Instrument.png

Core Animation 一栏在右下方,还有一个非常有用的工具集合:Color Debug Options(图中3)。这里有一系列的debug选项,这是辅助你找到和GPU Bound操作相关的信息入口。比较常用的选项包括:

Color Blend Layers.png

这个选项的意思是,如果该区域有图层混合的操作,则标记成红色,混合的图层越多,颜色越深,否则为绿色。如果你看到你的界面有大量的深红色区域,则表示你当前的UI可能做颜色混合的操作会比较影响你的性能。图层混合主要是因为layer的透明度。

屏幕上每一个点都是一个像素,像素有R、G、B三种颜色构成,另外还有一个alpha值。如果某一块区域上覆盖了多个layer,最后的显示效果受到这些layer的共同混合(即Blending)的结果。举个例子,上层是蓝色(RGB=0,0,1),透明度A为50%,下层是红色(RGB=1,0,0), 不透明。那么最终的显示效果是紫色(RGB=0.5,0,0.5)。这种颜色的混合需要消耗一定的GPU资源,不透明图层越多,blending消耗越大。但是如果对于某一层layer的透明度设置为100%(不透明),则GPU会忽略下面所有的layer,从而节约了很多不必要的开销。

Color Hits Green and Misses Red.png

这个选项的意思是,Core Animation是否有使用缓存进行绘制。如果成功使用了缓存,则标记成绿色,如果当前区域有缓存,但是当前缓存失效了,则标记成红色。这个选项主要和光栅化(Rasterization)相关。光栅化是将一个layer以及它的sub layer预先完成混合和渲染,并生成一个静态的位图(bitmap),然后加入缓存中。后续如果这个渲染结果不再变化,则可以复用这个缓存的位图直接绘制在屏幕上。这对于屏幕上有大量静态内容时,是很好的优化。后文将详细介绍Rasterization的部分。

Color Copied Imanges.png

这个选项主要检查是否有使用不正确图片格式。iOS推荐的图片格式是不带Alpha通道的PNG和JPG格式。若是其它GPU不支持的色彩格式的图片则会标记为青色,此时只能先由CPU来进行转换处理,然后将处理完的图片交给GPU。青色是我们需要避免的,因为CPU实时进行处理图片可能会阻塞主线程。

Color Offscreen-Rendered Yellow.png

这个选项是用来检查在当前UI中哪些区域的绘制必须使用离屏渲染(Offscreen Rendered)。离屏渲染,顾名思义,是指当前屏幕的绘制操作并不是直接发生在当前的帧缓冲区内,而是会合并/渲染图层树的一部分到一个新的缓冲区,然后该缓冲区被渲染到屏幕上。产生离屏渲染的原因有很多,它可以被 Core Animation 自动触发,也可以被应用程序强制触发。

一般情况下,你需要避免离屏渲染,因为这是很大的消耗。直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。

关于离屏渲染,如果展开来说,又是一个相对而言比较复杂的话题,我将在下一篇文章《iOS 2D Graphic(3)—— Offscreen Rendering离屏渲染》中详细讨论和总结这部分的内容。

除了以上常用的选项外,还有一个你可能也会用的到的选项:

Color Misaligned Images.png

这个选项检查了图片是否被放缩,像素是否对齐。被放缩的图片会被标记为黄色,像素不对齐则会标注为紫色。
黄色的场景比较多见,比如你将一个200200的图片塞到了一个100100的UIImageView里,那么图片自然就会被缩放;紫色的Misaligned Image场景一般很难见到,主要表示要绘制的点无法直接映射到频幕上的像素点,此时系统需要对相邻的像素点做anti-aliasing反锯齿计算。通常这种问题出在对某些View的Frame重新计算和设置时产生的。

2. OpenGL ES Driver Instrument
OpenGL ES Driver.png

相对于前面介绍的Core Animation Profiler来说,OpenGL ES Driver的侧重点非常集中,它的目标很明确,就是针对GPU的使用进行检测。你可以在右下角的Panel里选择感兴趣的指标选项,然后在下方的列表里观察数值。一般而言,Device Utilization,Renderer Utilization和Core Animation Frame Per Second是你应该要关注的首选项,这些指标将直接反应当前GPU的使用率。如果你发现GPU的使用率明显偏高,那么很明显你面临了GPU Bound的情况。然后,你可以再用前文介绍的Core Animation Profiler,使用Color Debug去分析具体可能的原因。

OpenGL ES Driver 2.png

优化策略和方法

那么当我们已经知道了Graphic的性能大致上分为CPU Bound和GPU Bound两种类型,并且我们也知道可以使用Instrument来分析具体的源头所在,那么我们能采取哪些措施和策略来解决问题呢?

为了能够清晰的阐述这些方法,我们需要思考下iOS中Animation的实现步骤,因为即使是非Animation的静态页面,我们也可以看作是只有一帧画面的特殊Animation,所以分析优化策略,可以对照着Animation的核心步骤来进行。

我们来看上一篇中出现过的描述一个完整的Animation的步骤的图:

Core Animation Pipe Line.png

了解这上面这3大步骤,将整个Animation从准备到完成的过程分成了两个清晰的阶段来做性能评估:“动画的响应速度”(Responsiveness)和“动画的平滑性”(Smoothness),其中前者对应于1,2步,后者对应于第3步。于是,我们讨论优化策略都将在这两个大方向上做文章。


(1)响应速度优化

  1. 只重绘变化的部分
    • 第一黄金原则:“避免使用drawRect:”:
    • 尽可能使用CALayer的属性,而避免使用DrawRect重绘。经典案例就是设置背景颜色,使用backgroundColor而不是UIColor setFill:

    // bad
    -(void)drawRect:(CGRect)rect {
    [[UIColor redColor] setFill];
    UIRectFill([self bounds]);
    }
    // good
    [myView setBackgroundColor:[UIColor redColor]];
    • 如果你必须重写drawRect:,则尽可能调用setNeedsDisplayInRect:而不是 -setNeedsDisplay。这将让Core Graphic自动为你的drawRect:代码创建clipRect:而无需改动任何其它代码,然后在绘制时自动忽略clipRect之外的部分,它能够显著提高性能;
    • 如果你无法提前预知更新的视图范围,那么只在需要的时候调用-setNeedsDisplay

  2. **正确处理图片(Be smart with images) **
    • 使用UIImageView而不是直接绘制image。这是因为此时blending是发生在GPU中,而不是提前在CPU中混合,同时Core Animation能够高效的从UIImage(CGImage)中取出bitmap绘制到View中,然后自动的将bitmap进行缓存(Built-in bitmap caching)

    // bad

    • (void)drawRect:(CGRect)rect
      {
      [self.image drawInRect:[self bounds]
      blendMode:kCGBlendModeNormal alpha:1.0];
      }

    // good
    myView.layer.contents = (id)[self.image CGImage];
    • 尽可能使用没有Alpha通道的图片。这样能够最大限度的减少图层混合。
    • 使用正确的图片格式。Apple强调在iOS中,PNG和JPEG是Apple官方推荐的格式,Xcode能够针对这两种格式做额外的优化,不要使用其它格式的图片比如TIFF等;
    • 对UI上的缩略图使用单独的Image,而不是将原图缩放到小图里。这里对缩略图有一条规则“如果使用PNG能够在失真允许的情况下获得足够好的压缩效果,则使用PNG!”
    • 关于缓存:
    [UIImage imageNamed:]将自动缓存在可删除的内存里(purgeable memory ),同时保存索引在 image table中以便复用;而[UIImage imageWithContentsOfFile:] 不会!
    • 当你设置layer contents的时候,所有作为layer backstore的CGImages都会被缓存;
    • 如果你手动创建一个CGImage,则打开kCGImageSourceShouldCache显式地建立缓存;
    • 通常情况下,不要自己手动建立Image的缓存

关于图片的这部分,我们可以利用上文里提到的Instrument中的“Color Copied Image”debug选项,来检测是否有额外的图片操作是可以避免的。


(2)动画平滑性优化

Animation的Step3发生在Render Server和GPU上:Render Server是以per layer, per frame的方式工作在CPU上,而Rendering本身是发生在GPU上。所以这一步同样是CPU Bound和GPU Bound同时存在。而影响动画的平滑性,更多的是GPU的负担过重,使用OpenGL ES instrument,查看Device Utilization的使用率,能够很好的指导你是否发生了GPU bound issue。记住我们一切一切的目标,就是60 fps!


(3)并发

除了上面提到的具有针对性的2大部分内容,凡是提到“性能”两个字,有经验的人都会自然而然的想到另外两个字“并发”。并发实际上是充分利用了多核CPU的并行处理能力,让繁重的处理操作和界面响应操作能够分别在不同的线程里并行的完成,让界面的操作能够及时被响应而不被其它的耗时处理所堵塞。

根据长时高计算的操作的类别不同,可以将并发的方式分为“数据并发”和“绘图并发”:

Concurrent Drawing.png

在这种情况下,只需要注意以下两点:
•Drawing APIs在任何队列里调用都是安全的,不一定非得是主线程队列,但是必须确保begin和end在同一个上下文中(同一个operation中)
•最终必须在Main Queue中更新图片


2D Graphic Optimization Demo

在Apple的 **WWDC2012 Session 506 - "optimazing 2D graphics and animation performance" **上,有一个绘图Demo叫iPaint,很好的示例了多种不同的优化方法带来的效果。但是这个Demo并没有Code可以下载。于是我就模仿演讲者在Demo过程中提到的内容和屏幕上能够看到的一些片段的Code,自己重新写了一个Demo。你可以在Github上下载 iPaint Demo

iPaint_flatten_scale.gif

因为篇幅的原因,我们只来看这个Demo的一个具体示例,你可以看到不同的开关对最终绘图的性能效果影响。

首先,在打开“Calculate Dirty Rect”开关计算更新区域并使用- setNeedDisplayInRect之前,由于打开了Flatten Long Path选项,在绘画路径长到一定程度的时候,会将整个视图扁平化到一个单独的Image中,在第一个版本的实现上,我们可以看到效果非常差,FPS直接掉到10以下:

iPaint_flatten_scale_bad.gif

用Instrument查看OpenGL ES Driver,发现GPU的压力并不是很大,但是帧率很低:


Screen Shot 2016-06-28 at 5.03.56 PM.png

使用Time Profiler查看,发现CPU绝大部分的开销都在DrawImage上面:

Screen Shot 2016-06-28 at 4.51.05 PM.png

那么在不改变当前Drawing策略的前提下,只是更改一个地方,打开Calculate Dirty Rect开关,每次drawInRect只是重新绘制当前更新的一小部分区域(红框内的区域),可以看到性能明显提升:

iPaint_flatten_scale_bad_dirty.gif Screen Shot 2016-06-28 at 5.08.48 PM.png

但是,可以看到,帧率仍然不够理想,离目标60 fps还差得很远。观察CPU的使用,还是因为在Flatten Path的时候绘制图像的时间太长,可以在Time Profiler中,CGContextDrawImage仍然花了大部分时间。如果这个时候,关闭Flatten Path开关,帧率立即就上来了。

Screen Shot 2016-06-28 at 5.05.07 PM.png

可是Flatten Path是有用的,否则随着Path的不断增长,帧率还是会下降,所以关键问题是在Flatten的时候,如何降低绘图的开销。这个问题一直困扰了我很久,因为WWDC视频的代码并没有显示作者是如何做Flatten的。(这个地方是不能用rasterization的,因为整个Layer的图层在不断变化,使用光栅是没有用的,必须手动将图层上的物件绘制到一张图片上)

最后,我终于发现了问题所在:因为在UIKit和Core Graphic的坐标系问题,直接使用UIGraphicsGetImageFromCurrentImageContext()得到的图片是颠倒的,所以我原本使用的是一个Flip()函数,将CoreImage又重新绘制一次,这样颠倒2次就得到了正确的图片。可是这样会造成极大的性能损失,前面Time Profiler中检测到的CPU损耗原因就在此,于是我想到了额外的方法,是在drawRect中,将原先得到的颠倒的flatten image,使用Core Animation的转换矩阵CTM,直接将图片反转。只此改动,帧率立马到58左右!

Screen Shot 2016-06-28 at 4.51.39 PM.png

所以从这点上,我们也许可以在上文中的优化策略中再增加一条:


2016.8.12 补充优化内容:

关于CAShapeLayer和CALayer:
CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。你指定诸如颜色和线宽等属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就自动渲染出来了。当然,你也可以用Core Graphics直接向原始的CALyer的内容中绘制一个路径,相比直下,使用CAShapeLayer有以下一些优点:


总结

到此,此文中提到的各种优化场景和策略已经能够涵盖绝大多数情况下的问题,但是这些都是通用对策,并不是万能钥匙,只是给我们在遇到问题时提供对策的思路,至于真正的具体实施方案,对于每一个具体问题都是不同,我们需要不断的测试和调试来找到最佳的方案。

最后,用Apple WWDC上的两张图来简单总结一下遇到Graphic Performance问题的对策:

Paste_Image.png

希望此文能帮助到你!

部分参考:
WWDC2012 Session 211 - Building Concurrent User Interfaces on iOS
WWDC2012 Session 238 - iOS App Performance Graphics and Animation
WWDC2012 Session 506 - Optimizing 2D Graphics and Animation Performance
WWDC2014 Session 419 - Advanced Graphics and Animation Performance
Designing for iOS: Graphics and Performance
Mastering UIKit Performance
iOS 保持界面流畅的技巧
When should I set layer.shouldRasterize to YES
WWDC心得与延伸:iOS图形性能
UIKit性能调优实战讲解
Layer Trees vs. Flat Drawing – Graphics Performance Across iOS Device Generations

2016.7.20 完稿于南京

上一篇下一篇

猜你喜欢

热点阅读