1. 问题描述
百度阅读 iOS 版书架在宫格、列表模式下进行快速滑动均感觉到明显卡顿,当书架中全部为图书\小说时滑动较流畅,但生成文件夹后卡顿加强。
2. iOS 显示原理
- Vsync是什么?
- CPU\GPU协同方式?
- V-Sync 机制是什么?
- 双缓冲工作原理?
- 垂直同步工作原理?
- 掉帧卡顿是如何产生的?
避免复制粘贴,上面的这些问题可以从下面这篇文章找到答案:
ibireme:如何让iOS 保持界面流畅?这些技巧你知道吗
3. 检测手段
3.1 用YYFPSLabel进行简单的监测
@ibreme 的 YYKit中一个简易组件,可加入到要被检测的 VC 的- (void)viewDidLoad 中。
// 加测试 fps
_fpsLabel = [[YYFPSLabel alloc] initWithFrame:CGRectMake(0, 100, 200, 200)];
[_fpsLabel sizeToFit];
// 当前顶层窗口
UIWindow *window = [[UIApplication sharedApplication].windows lastObject];
// 添加到窗口
[window addSubview:_fpsLabel];
通过这个控件检测发现,书架卡顿时,CPU 消耗很少,最卡的时候帧数也在55-60之间。排除 CPU 的原因,要监测 GPU 我们开始使用更锋利的匕首——instruments。
3.2 用 instruments全面检测
- GPU Driver
- Core Animation
- OpenGL ES Driver
- 模拟器中的『Color debug options View debugging』
3.2.1 观测的指标
-
GPU Driver
相关的检查选项:设备利用率 Device Utilization %
:GPU在渲染上花费的所有时间。>95%意味着该应用程序是GPU绑定的。-
渲染利用率 Renderer Utilization %
:GPU在绘制像素上花费的时间。超过了~50%,就意味着你的动画可能对帧率有所限制,很可能因为离屏渲染或者是重绘导致的过度混合。 -
Tiler利用率 Tiler Utilization %
:GPU在处理顶点上花费的时间。超过了~50%,就意味着你的动画可能限制于几何结构方面,也就是在屏幕上有太多的图层占用了。 -
分割次数 Split count
:帧分割的数量,顶点信息无法使用分配的缓冲区。
-
Core Animation
相关的检查选项:Color Blended layers
勾选这个选项后,blended layer 就会被显示为红色,而不透明的layer则是绿色。我们希望越少红色区域越好。-
Color Hits Green and Misses Red
如果shouldRasterize被设置成YES,对应的渲染结果会被缓存,如果图层是绿色,就表示这些缓存被复用;如果是红色就表示缓存会被重复创建,这就表示该处存在性能问题了。如果光栅化的层变红得太频繁那么光栅化对优化可能没有多少用处。位图缓存从内存中删除又重新创建得太过频繁,红色表明缓存重建得太迟。可以针对性的选择某个较小而较深的层结构进行光栅化,来尝试减少渲染时间。 -
Color copied images
这个选项主要检查我们有无使用不正确图片格式,若是GPU不支持的色彩格式的图片则会标记为青色,则只能由CPU来进行处理。我们不希望在滚动视图的时候,CPU实时来进行处理,因为有可能会阻塞主线程。 -
Color misaligned images
这个选项检查了图片是否被放缩,像素是否对齐。被放缩的图片会被标记为黄色,像素不对齐则会标注为紫色。 -
Color Offscreen-Rendered Yellow
开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。
3.2.2 使用方法
滑动书架,查看卡顿时 GPU Driver 检测到的上述几个指标。发现Renderer Utilization和Tiler Utilization都达到了90-100之间。可以确认是GPU 处理顶点和渲染花费很大性能。
3.2.3 相关概念
1)光栅化(Rasterize)
将图转化为一个个栅格组成的图像,每个元素对应帧缓冲区中的一像素。
2)离屏渲染(Off-Screen Rendering)
(What)什么是离屏渲染?
GPU在当前屏幕缓冲区之外开辟一个新的缓冲区
进行渲染操作,并多次切换上下文。因此比当前屏幕渲染更耗费性能。
(When)什么时候会离屏渲染?
下面的这些属性会触发离屏绘制
:
-
shouldRasterize(光栅化)
shouldRasterize = YES在其他属性触发离屏渲染的同时,会将光栅化后的内容 缓存起来,如果对应的layer及其sublayers 没有发生改变 ,在下一帧的时候可以直接复用。
光栅化是把GPU的操作转到CPU上了,生成位图缓存,直接读取复用。
-
masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
- 复杂形状设置圆角等
- 渐变
还有一种特殊的离屏渲染——CPU渲染
:如果我们重写了drawRect
方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内 同步地
完成,渲染得到的bitmap最后再交由GPU用于显示。
(Why)为什么需要离屏渲染?
圆角、阴影、遮罩等效果需要预合成,在合成之前不能直接在当前屏幕绘制,因此需要离屏渲染完成之后,切换上下文到当前屏幕绘制。
3) 混合(Blending)
What
blending指混合像素颜色的计算。若两个图层重叠,第一个图层有透明度,则最终像素颜色计算需要考虑第二个图层。
When
alpha < 1
Why 导致性能耗损
如果是不透明的图层,则该像素颜色显示即为图层在该像素的颜色。若含透明度,则需要引入更多计算,计算下面的图层混合后在该像素显示的颜色。
4. 避免卡顿的解决办法
4.1 开发时的tips
1、每次都看一下有没有能重用的 cell,而不是永远重新新建(这个是 UITableView 的常识)
2、Cell 里尽量不要用 UIView 而是全部自己用 drawRect 画(之前因为 iOS 有 bug,这样做会有性能上质的飞越。也有很多大神写过很多文章解释原理,有兴趣的自己去看看吧我就不做复制粘贴了。后来 iOS 也改掉了这个问题,这么做的效果就没那么明显了。)
3、图片载入放到后台进程去进行,滚出可视范围的载入进程要 cancel 掉
4、圆角、阴影之类的全部 bitmap 化,或者放到后台 draw 好了再拿来用
5、Cell 里要用的数据提前缓存好,不要现用现去读文件
6、数据量太大来不及一次读完的做一个 load more cell 出来,尽量避免边滚边读数据,这样就算是双核的 CPU 也难保不会抽
4.2 避免离屏渲染
1)阴影绘制:使用ShadowPath来替代shadowOffset等属性的设置。
2)裁剪图片为圆。
3)blending
4)不要在滚动视图使用 cornerRadius\mask\ shouldRasterize;
如果一定要实现圆角
效果:
视图内容不变
的情况下、又图省事的话,可以:
// 开启光栅化,使视图渲染内容被缓存起来,下次绘制的时候可以直接显示缓存
self.layer.shouldRasterize = YES;
self.layer.rasterizationScale = [UIScreen mainScreen].scale;
视图内容变化
时:
后台预先生成圆角图片,并缓存起来,再在主线程显示,避免离屏渲染。视图背景为单色
时,可在图片上面覆盖一个镂空圆形图片。
滚动类视图(tableview\collection view)的重绘是很频繁的(因为Cell的复用),如果Cell的内容不断变化,则Cell需要不断重绘,如果此时设置了cell.layer可光栅化。则会造成大量的离屏渲染,降低图形性能。
推酷:Core Animation系列之CADisplayLink
4.3 分析界面性能问题的步骤
1)定位帧率
,为了给用户流畅的感受,我们需要保持帧率在60帧左右。当遇到问题后,我们首先检查一下帧率是否保持在60帧。
2)定位瓶颈
,究竟是CPU
还是GPU
。我们希望占用率越少越好,一是为了流畅性,二也节省了电力。
3)检查有没有做无必要的CPU渲染
,例如有些地方我们重写了drawRect
,而其实是我们不需要也不应该的。我们希望GPU负责更多的工作。
4)检查有没有过多的offscreen渲染
,这会耗费GPU的资源,像前面已经分析的到的。offscreen 渲染会导致GPU需要不断地onScreen和offscreen进行上下文切换。我们希望有更少的offscreen渲染。
5)检查我们有无过多的Blending
,GPU渲染一个不透明的图层更省资源。
6)检查图片的格式
是否为常用格式,大小是否正常。如果一个图片格式不被GPU所支持,则只能通过CPU来渲染。一般我们在iOS开发中都应该用PNG格式,之前阅读过的一些资料也有指出苹果特意为PNG格式做了渲染和压缩算法上的优化。
7)检查是否有耗费资源多的View或效果
。我们需要合理有节制的使用。例如,UIBlurEffect。
8)最后,我们需要检查在我们View层级
中是否有不正确的地方。例如有时我们不断的添加或移除View,有时就会在不经意间导致bug的发生。像我之前就遇到过不断添加View的一个低级错误。我们希望在View层级中只包含了我们想要的东西。
5. 书架卡顿问题定位
5.1 问题定位的思路
- 根据问题复现缩小范围
滑动到全部为图书时相对流畅
滑动到有文件夹时出现严重卡顿
宫格\列表都卡顿 - 由于这部分代码不是自己写的,不是很了解逻辑,所以先用简易控件 YYFPSLabel看一下 CPU 消耗,发现帧数在出现明显卡顿时仍保持在55-60之间,可排除 CPU。
- 用 Instruments 中的 GPU Driver 检测 GPU 的相关指标,判断可能与顶点和渲染有关。
- code review 文件夹功能的 view,发现书架中使用了大量 layer.cornerRadius。
- 注释掉初步判断可能为问题的代码,继续用 GPU Driver 检测,发现指标上升,滑动流畅。定位改善圆角可起到优化性能的作用。
5.2 解决方法:
1)到处是圆角。直接覆盖一张中间为圆形透明的图片,或与 PM 沟通,把不是必须为圆角的改为直角;
2) 书架 blended layer 太多,用Color Blended layers检测几乎满屏红色,可优化;
3)翻转到书城,再返回书架后,用Color off-screen rendering 满屏黄色,可优化;
当滚动类视图中需要使用大量圆角时,要注意卡顿。
6. 总结画圆角的几种方式:
1)layer.cornerRadius
aImageView.layer.cornerRadius = aImageView.frame.size.width/2.0;
aImageView.layer.masksToBounds = YES;
优点:使用简单
缺点:不适合大量圆角的场景。若有大量圆角,用此方式会造成离屏渲染增加 GPU 消耗。
测评:ios9.0之前UIImageView和UIButton都高亮为黄色。ios9.0之后只有UIButton高亮为黄色。
2) 在 drawRect:中绘制
- (void)drawRect:(CGRect)rect {
CGRect bounds = self.bounds;
[[UIColor whiteColor] set];
UIRectFill(bounds);
[[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8.0] addClip];
[self.image drawInRect:bounds];
}
优点:把 GPU的压力转给 CPU,适用于 CPU 压力不大的情况。可以写在SDWebImage的completed回调里,在主线程异步绘制。 也可以封装到UIImageView里,写了个DSRoundImageView。后台线程异步绘制,不会阻塞主线程。
缺点:要重写视图,有点麻烦,CPU\内存消耗增大。
测评:没离屏渲染(但是CPU消耗和内存占用会很大)
3)layer.mask
CAShapeLayer *layer = [CAShapeLayer layer];
UIBezierPath *aPath = [UIBezierPath bezierPathWithOvalInRect:aImageView.bounds];
layer.path = aPath.CGPath;
aImageView.layer.mask = layer;
优点:可不局限于圆角,由 mask 控制边角显示为什么样。
缺点:效率和 layer.cornerRadius 一样。
4)直接覆盖一张中间为圆形透明的图片
这种方法就是多加了一张透明的图片,GPU计算多层的混合渲染blending也是会消耗 一点性能的,但比第一种方法还是好上很多的。
优点:无离屏渲染
缺点:得求 UI 给个图,如果是非纯色背景,此方法不适合。
测评:无任何高亮,说明没离屏渲染。
如果要效率(例如要提高table view的滚动帧数),就多用方法二。要方便,自然是方法一。如果需要的特殊形状UIBezierPath对象无法构成,则考虑方法三。
5)SDWebImage处理图片时Core Graphics绘制圆角
//UIImage绘制为圆角
int w = imageSize.width;
int h = imageSize.height;
int radius = imageSize.width/2;
UIImage *img = image;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(NULL, w, h, 8, 4 * w, colorSpace, kCGImageAlphaPremultipliedFirst);
CGRect rect = CGRectMake(0, 0, w, h);
CGContextBeginPath(context);
addRoundedRectToPath(context, rect, radius, radius);
CGContextClosePath(context);
CGContextClip(context);
CGContextDrawImage(context, CGRectMake(0, 0, w, h), img.CGImage);
CGImageRef imageMasked = CGBitmapContextCreateImage(context);
img = [UIImage imageWithCGImage:imageMasked];
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
CGImageRelease(imageMasked);
以上代码可以写成UIImage的类别:UIImage+RoundImage.h 并在SDWebImage库里处理image的时候使用类别方法绘制圆角并缓存。
测评:无任何高亮,说明没离屏渲染,而且内存占用也不大。(看起来是最优解)
7. iOS 9的优化
iOS 9.0 之前,UIimageView、UIButton设置圆角都会触发离屏渲染
iOS 9.0 之后,UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。
8. 备注(instruments的 bug)
记录一个instruments 的问题,避免其他人踩坑。
apple 说自己的 xcode instruments有个 bug ,用 instruments 进行调试时在 xcode 7上经常出现无法选择测试设备的情况,让人头疼不已。 但苹果把它从 xcode 6.4开始就作为 known issue,现在都 xcode7.2了,不知何时才能修好。
看开发者网站上,有位童鞋的法子挺好,简述就是:
关手机-拔设备-关 xcode\instruments-开手机-插设备-clean-build-profile-好使了。