iOS UI帧率优化经验

序言

开始之前, 简要介绍一下移动客户端的动态化排版方案.为满足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. 后端提供带圆角的图片,这个方案性能最好,前端无工作量,但比较依赖后端服务能力。

当圆角是一个整圆,并且指定了宽线条外框时(比如头像处理成一个圆形的),系统绘制的圆圈(上文提到的方案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相关的计算并行化,提供线程调度管理及预加载等机制来保证流畅性,欢迎就此多做交流。此文抛砖引玉,以供参考。

更多文章

CocoaPods开源库的搭建
CocoaPods搭建私有库
CocoaPods搭建私有库遇到问题
CocoaPods私有库的升级维护
SKStoreReviewController之程序内评价
App应用程序图标的动态更换
开源框架 MGJRouter_Swift
iOS的MVP设计模式
iOS插件化
iOS FMDB的使用
Swift之ReactiveSwift
OC之ReactiveCocoa
OC之ReactiveCocoa进阶
iOS 性能考虑

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容

  • 1 CALayer IOS SDK详解之CALayer(一) http://doc.okbase.net/Hell...
    Kevin_Junbaozi阅读 5,135评论 3 23
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • 想和你说 却不知如何说起 无人可倾诉 我像是陷在沼泽里 一口气都不得呼吸 想你拉我一把 给我一份勇气
    瑜样阅读 114评论 0 0
  • 此前有过基础,所以很多常识性的东西对我来说只是回顾一下。大多数语言都有类型与运算、流程控制、数组等等,可能具体语法...
    root_zhb阅读 188评论 0 0
  • 我是一个有价值的人 我是一个有能力能把工作做仔细的人 我是一个喜欢专研的人 我是一个能力配得上我骄傲的人 我是一个...
    唐汤圆阅读 247评论 0 0