序言
开始之前, 简要介绍一下移动客户端的动态化排版方案.为满足UI布局的灵活和后端可控性, 移动端开发了基于Card的动态排版渲染引擎:前后端制定好协议, 客户端解析后端下发的描述信息,构建和拼接不同UI元素。 相较于Native客户端固化布局, 动态化方案由于事先不知道UI属性和确切尺寸,需要动态创建并计算UI元素显示区域。 这对代码性能优化提出了更高的要求. 本文就帧率测试方法和优化经验做下总结.
工具选择
检测帧率,使用CADisplayLink API
检测函数执行耗时情况,使用XCode自带的TimeProfiler工具
检测渲染问题,使用模拟器Debug菜单下自带的离屏及图层颜色混合检测工具
大家平时检测帧率可能常用TimeProfile。该工具虽然功能强大,但不够轻量、准确。应用复杂时,由于这个工具需要跟设备通讯,会频繁读取设备状态,收集代码堆栈信息,产生的数据量非常大.我们测试时几分钟有时产生上GB数据。如果只是获取帧率,建议大家使用轻量的CADisplayLink,只有当需要获取更详尽的信息时,才考虑使用TimeProfile。
如何优化
可以把这个阶段分成两阶段: 定位主线程耗时代码和针对渲染问题优化。前者可通过TimeProfile统计到每个函数的耗时情况。先解决可能阻塞主线程的代码,比如有无读写IO操作(将其放入非主线程执行),有无耗时较为明显的函数,最后再通过模拟器定位离屏渲图层混合的问题,寻找优化方案。
阶段一:下面介绍下我们优化过程中统计出来的一些开销较高的系统API (可能你也遇到过)
1.字符格式化操作
+(instancetype)stringWithFormat:(NSString *)format, …
当代码中调用该接口较少时,你可以略过这个问题。但当主线程中大量的使用该API时,这个函数的耗时会变得明显, 因为这个函数的执行效率并不高。
解决方案:使用C函数,比如asprintf,snprintf等创建char,然后用char构建NSString。
2.图片资源访问
+(UIImage *)imageNamed:(NSString *)name
调用该接口加载图片后,系统会缓存该图片,以加快下次访问。但在系统压力较大的低端机型上,反复调用该接口获取某张固定图片,时间还是会很长。我们用TimeProfile也抓到了该函数取占位图时耗时较长的情况。从原理上看,该接口要考虑不同扩展名、不同机型下最佳适配资源(2x,3x分辨率图片),根据传入的文件名做模糊匹配。所以其效率也不是很高。
解决方案:1.使用分辨率更小的图片,这有助于缩短第一次加载时间。2.如果该图片属于公共访问非常频繁的资源(比如占位图),通过该接口获取到图片内存地址后,用全局指针保存起来。再次访问时可以直接使用保存好的指针,完全不会占用主线程时间。
3.文本绘制区域计算
-(CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options context:(NSStringDrawingContext *)context;
- (CGSize)sizeThatFits:(CGSize)size;
当文本控件较多,滑动过程中频繁使用这类接口计算文本显示区域,会占用较多主线程时间。
解决方案:
从UI设计上给定一个固定显示区域,让系统对过长的文本自动截断,从而避免调用显示接口。
2.如果方案1行不通,可以在调用一次后把计算结果缓存起来。用户再次访问时,直接使用已计算好的数值。这个方案需要考虑影响计算结果的因素,比如字体、字号、限定的宽高、行数、行距、截断方式等。如果将这么多变动的因素组合起来查询,效率会比较低。 我们的方法是将这些数据打包成一个对象,相当于计算结果跟原始的属性绑定到同一个指针指向的空间里了,这样使用时不需查询,通过指针就可直接访问。
4.UIView层级调整有关的代码
-(void)insertSubview:(UIView*)view belowSubview:(UIView *)siblingSubview;
-(void)insertSubview:(UIView*)view aboveSubview:(UIView *)siblingSubview;
-(void)removeFromSuperview;
-(void)bringSubviewToFront:(UIView *)view;
-(void)sendSubviewToBack:(UIView *)view;
…
解决方案:如果你的程序运行过程中有较多调用这类动态插入或者调整View层级的代码,可以在创建view时将层级固定下来,并对临时用不到的view设置为隐藏,再在合适的时机显示出来。
5.NSScan的使用
数字跟字母混合情况下,有很多人会选用这个API做数值转换,用来分离出数值部分,但经测试,该API性能并不好。
解决方案:1.大多简单的数字跟字母混合字符串,直接转换即可。比如要取出字符16px里的16出来,使用intvalue就可以。2.也可使用strtoul(const char *nptr,char **endptr,int base ),比如一个色值数据#6C6C6C,使用该接口配合位运算,能很高效的分离出RGB3个10进制数值来。
阶段二:渲染优化
渲染层面,影响流畅性的因素主要有离屏渲染和图层混合。系统也提供了一些优化开关,默认的优化开关是关闭的,需要根据UI特点,验证后再决定是否开启。下面介绍这方面的知识。
1.关于离屏渲染Off-Screen Rendering vs On-Screen Rendering
Off-Screen Rendering(离屏渲染)需要先创建屏幕外缓冲区做渲染,然后将渲染结果写入存储像素信息的帧缓冲区中。这个过程除了要创建额外的缓存,还涉及两次较为耗时的上下文切换:从当前屏切到屏幕外缓冲区,再切换回当前屏缓冲区,所以如果有大量离屏渲染会影响帧率。
引起离屏渲染的常见原因有:
重写drawRect,并调用Core Graphics接口,会在CPU上执行离屏渲染
UI中有圆角且masksToBounds=YES时,阴影,组透明allowsGroupOpacity=true,光栅化shouldRasterize=true等情况时,会在GPU上进行离屏渲染
我们对常见的情况做了下总结:
a.圆角问题的处理,总结了五种方案
1.通过CALayer的masksToBounds = true组合cornerRadius来实现圆角效果。这种方案虽然会产生离屏渲染,但在圆角图层上覆盖新的图层不会出现圆角被新图层覆盖的问题,较为通用
2.只设置cornerRadius,可以避免离屏渲染,但可能被新加的图层覆盖,导致圆角出不来,应用场景有限
3.通过后台线程自绘,生成图片,方案较为通用,而且可以解决系统圆角的某些显示问题(下面会讲),但绘制函数较为耗时。
4.用图片遮罩来处理圆角:制作一张四周圆角外带颜色中间透明的图片,遮到需要圆角的VIEW上。渲染时只需要进行图层混合,相比离屏渲染性能好的多。但由于各处圆角尺寸不固定,而且要求透明色区域外的颜色跟圆角的superview背景色一致,很难做成通用方案。
- 后端提供带圆角的图片,这个方案性能最好,前端无工作量,但比较依赖后端服务能力。
当圆角是一个整圆,并且指定了宽线条外框时(比如头像处理成一个圆形的),系统绘制的圆圈(上文提到的方案1)周边可能会显示出不太明显的杂色点,用自绘(上文提到的方案3)就没有这个问题.我们基础库需要考虑通用性,所以组合了1、3两种方案,优先使用系统实现,有显示杂色情况时使用方案3.
b.阴影:阴影会降低流畅性。解决方案:1.跟UED要一张不带中间内容的阴影外框图贴到最底层。 2.如果layer尺寸是固定的,不需要频繁更改其尺寸,可以使用shadowpath代替shadowoffset。
c.组透明度allowsGroupOpacity:IOS7之后默认是开启的。开启后会使子Layer继承其父layer的透明度。如果不用处理透明,可以关闭它,以提高性能。
d.光栅化shouldRasterize:光栅化即将渲染过的layer临时缓存为位图,以供将来渲染使用。这个选项会增加内存的使用,导致渲染时间变长。但如果VIEW层级较多效果复杂,且内容不变,开启后有利于增强性能。
2.关于Blending图层像素混合
Blending概念:可以想象你手里拿着几张塑料卡片。当前面的塑料片不透明时,我们看到的只是离你最近那张的颜色(系统只绘制最顶层VIEW颜色);但当卡片是半透明时,我们看到的可能是多张卡片的混合色(系统对多个layer内的像素值做叠加合成处理)。所以设计时建议优先考虑用不透明的图层。
3.如果你重写了UIView的drawRect方法,考虑是否打开以下两个开关
a. clearsContextBeforeDrawing
这个值可以决定在drawRect调用时是否清理之前显示的内容。系统默认开启,以保证你在重绘时渲染区是“干净”的,即被刷新为(R:0,G:0,B:0,A:0)黑透明色。有时我们只需更新一小部分区域,此时这个清理步骤并不是必须的,我们可以设置属性=NO来提高绘制性能。
b. drawsAsynchronously异步绘制开关
开启后drawRect,drawInContext虽然仍在主线程调用,但这里的代码不会做任何事情,真正的绘制会异步化到后台线程。由于异步化系统需要做更多的处理,需要测试对比开启关闭的效果后再决定是否开启。
4.CGRect
这是个容易被忽略的优化点。由于我们card化大多UI元素frame是经计算得出的,很多CGRect存储的浮点型转化到屏幕像素点后也不是整数。这个问题可能导致图形边缘模糊,还会导致GPU做更多的抗锯齿运算。应尽量保证其映射为屏幕像素点后还为整数值。
5.检查后台返回的图片
显示区域和后台给的图片尺寸应基本一致。图片分辨率过高不仅解码慢,内存占用高(比如一张3x3图片解码成位图后将会是2x2分辨率图片的2.25倍),渲染时对图像放缩也会耗费性能。
6.其他
如果使用SDWebimage下载图片,并且下载完成后需要对图片重绘(比如圆角化,模糊化后再展示,可以在请求图片的时候,设置SDWebImageAvoidAutoSetImage,防止sd设置一张并不需要展示的图片。
对于重用的cell,设置数据前,判断使用的model与重用前的是否相同,再决定是否需要再执行UI重新布局。
检查下有无过于复杂的VIEW层级,尽量减少或合并一些VIEW层级;如果不需处理触摸事件,可以用layer代替UIView。
针对特定低端机型做优化, 比如降低动画效果,减少阴影, 关闭圆角。
检查有无线程锁操作,避免主线程对锁的访
总结
随着APP代码的复杂,流畅问题逐步演化为多因素叠加一起相互影响的问题.比如剩余内存量,APP线程数量,CPU频率,操作系统版本(即使不降频,这几年IOS每个版本新系统整体性能比旧版本要差)。业内也有不少探索,通过将UI相关的计算并行化,提供线程调度管理及预加载等机制来保证流畅性,欢迎就此多做交流。此文抛砖引玉,以供参考。