卡顿发生的原因
首先我们需要明确一下刷帧率的概念,玩游戏的人对FPS这个词一般不会陌生,刷帧率代表设备在1s内刷新显示图像的帧数,iPhone推荐的刷帧率是60,即16.7ms内刷新一帧。
这里借用一下ibireme大神的插图来介绍一下刷新一帧图像系统都做了哪些事情
首先CPU会计算显示内容,包括布局的计算、图片解码、文本的预先绘制(绘制成GPU渲染需要的纹理资源),随后由GPU进行变换、合成等一些光影的计算,最后将渲染结果提交到renderBuffer,等待接收到上一帧图像显示完毕后发来的垂直同步信号后进行显示。如果接收到垂直同步信号后CPU或者GPU没有完成计算和内容的提交,那这一帧就会丢失,等待下一次机会再显示,映射到屏幕上,便会出现卡顿现象。
耗费CPU及GPU资源的具体操作可以查看大神的原文,(他是YYKit的作者,也是一个动漫爱好者,博客中的其他文章也相当不错,对iOS中一些技术的理解很有深度。)文章中还介绍了Facebook出品的优化界面流畅度的框架AsyncDisplayKit,YYKit中关于优化部分的灵感亦是来自于ASDK,
实际操作时可以使用YYKit针对正常的文字图片等视图展示进行优化,除此之外,还有一些其他不合理的操作需要避免。
透明色与图层混合
一块覆盖多层layer的区域当上层图层透明度不为1时,在显示时则综合多个图层的颜色混合进行显示,使用instrument调试时可以发现,这种Color Blended Layers是需要消耗一定GPU资源的,所以尽量将控件设置成不透明的。(这里说的既包含控件的背景色,也包含控件上显示的图片)
离屏渲染
上图是正常的渲染通道,通过OpenGL将需要展示的buffer从CPU内存复制到GPU,GPU经过渲染计算后将渲染结果放到renderBuffer中。再看一下离屏渲染
在前两个渲染通道中,GPU分别得到了渲染结果,但是并没有直接放入RenderBuffer中,而是等到第三个通道开始渲染时,将两者组合,放入renderBuffer中,这种临时性的保存无疑会占用更多的GPU资源。可能会触发离屏渲染的操作包括:
- 重写drawRect方法
- 使用View的layer.masksToBounds、layer.shadow属性时
- 高斯模糊效果
开启光栅化layer.shouldRasterize = true
所谓光栅化,是将一个layer预先渲染成bitmap位图,然后加入缓存中,对于比较消耗资源的静态内容进行缓存,可以得到小幅性能提升。这个预先缓存的有效期只有100ms,如果在100ms内没有被使用则会被清除。除了会触发离屏渲染,在实际使用时经常会出现未命中缓存的现象,且预先缓存有可能会消耗更多的时间,所以要慎用。
最彻底的避免办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。
图标资源
cell中美工出的图尽可能使用恰当的大小、格式,对于图片的缩放显示和不同格式间的图片编解码(iOS中切图一般用png格式)会耗费一些额外的性能,而且格式转换还可能会产生一些莫名其妙的问题(比如带有带有透明像素的图像从png转到jpg时透明像素会变成白色像素)。
圆角效果
当大量需要圆角和阴影效果时不能直接设置 layer.cornerRadius 和 layer.masksToBounds 属性,同样不可以直接重写drawRect方法绘制一个圆角layer然后使用maskLayer属性来裁剪控件,这和直接使用maskToBounds一样都会触发离屏渲染。 最简单的方法是让设计师直接给一个圆角的图片背景,也可以开启一个位图上下文动态的绘制一个圆角矩形然后导出UIImage设置为视图背景。
为UIImageView设置圆角时,则可以直接将需要显示的图片绘制成一个圆角图片然后再设置到ImageView中显示。
但是使用以上方法时记得设置控件的背景色为透明。
调试手段和方法
使用xcode内置的Instrument可以很方便的进行程序的性能测试,包括内存泄露、处理器使用情况等。测试界面刷帧性能时我们使用Core Animation。
上图中左边的frames per second是每秒钟显示图像帧数,即刷帧率,右下角勾选相应的选项可以将发生相应事件的部分在手机屏幕上以不同的颜色标注出来,比如我们勾选了Color Blend layer,手机屏幕上会将发生混合图层的区域以红色标记出来。下面两张图片可以看出,我拉出手机下方的半透明菜单栏后,处于半透明遮罩下的整个屏幕都发生了颜色混合。(黄色代表发生离屏渲染,图中的黄色区域是中文的原因,Stackoverflow上有人给出的解释是,如果文字是中文,label会有一个子layer,导致图层混合)
由此,我们可以定位一些影响性能的操作从而逐一解决。