您的位置:首页 > 其它

书架卡顿问题引发的显示类知识梳理&性能检测

2016-06-12 18:24 441 查看


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


开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。

WWDC心得与延伸:iOS图形性能


3.2.2 使用方法

滑动书架,查看卡顿时 GPU Driver 检测到的上述几个指标。发现Renderer Utilization和Tiler Utilization都达到了90-100之间。可以确认是GPU 处理顶点和渲染花费很大性能。


3.2.3 相关概念

1)光栅化(Rasterize)

将图转化为一个个栅格组成的图像,每个元素对应帧缓冲区中的一像素。
如何理解OpenGL中着色器,渲染管线,光栅化等概念?
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)为什么需要离屏渲染?

圆角、阴影、遮罩等效果需要预合成,在合成之前不能直接在当前屏幕绘制,因此需要离屏渲染完成之后,切换上下文到当前屏幕绘制。
离屏渲染学习笔记
iOS 离屏渲染的研究
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可光栅化。则会造成大量的离屏渲染,降低图形性能。
WWDC心得与延伸:iOS图形性能
极客学院:性能调优
推酷: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-好使了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: