iPhone 上面的应用一直都是以流畅的操作体验而著称,但是由于之前开发人员把注意力更多的放在开发功能上面,比较少去考虑性能的问题,可能这其中涉及到 objective-c,c++ 跟 lua,优化起来相对复杂一些,导致应用在比如 touch 等较低端的产品上,光从启动到进入页面就花了将近一分钟的时间,页面之间的切换没有那种很流畅的感觉,内存也居高不下,比较影响应用的用户体验,所以很有必要进行一些优化,下面记录一下我在优化的过程中的一些心得:
在 iOS 上进行性能分析的时候,首先考虑借助 instruments 这个利器分析出问题出在哪,不要凭空想象,不然你可能把精力花在了 1% 的问题上,最后发现其实啥都没优化,比如要查看程序哪些部分最耗时,可以使用 Time Profiler,要查看内存是否泄漏了,可以使用 Leaks 等。关于 instruments 网上有很多资料,作为一个合格 iOS 开发者,熟悉这个工具还是很有必要的。
在 iOS 里关于 UIKit 的操作都是放在主线程,因此如果主线程被阻塞住了,你的 UI 可能无法及时响应事件,给人一种卡顿的感觉。大多数阻塞主线程的情况是在主线程做 IO 操作,比如文件的读写,包含数据库、图片、json 文本或者 log 日志等,尽量将这些操作放放到子线程 (如果数据库有一次有较多的操作,记得采用事务来处理,性能相差还是挺大的),或者在后台建立对应的 dispatch queue 来做这些操作,比如一个低级别的 serial queue 来负责 log 文件的记录等等。程序中如果你的代码逻辑是按照同步的逻辑来写的,尽量修改逻辑代码吧。。。
一般为了提升用户体验,都会在应用中使用缓存,比如对于图片资源可以使用 SDwebImage 这个开源库,里面就实现了一个图片缓存的功能。参考 SDWebImage 的代码自己也可以实现缓存功能:
业务层根据资源的 url 向 resourcemanager 获取对应的资源,resourcemanager 首先会到 memorycache 这边去获取资源,memorycache 可以利用 NSCache 实现,因为 NSCache 首先是线程安全的,而且在收到内存警告的时候会自己释放对应的内存;如果 memorycache 没有对应的资源再去 disk 查找,disk 也没有的话再去 internet 获取,获取到的话会更新到 memorycache 和 disk 中,具体可以去参考一下 SDWebimage 的实现细节。
当用户点击 app 的图标之后,程序应该尽可能快的进入到主页面,尽可能减少用户的等待时间,比如我们的应用程序在启动的时候会去做 3d 模型的渲染操作,完成之后在进入首页面展示,但其实我们可以先进入到主页面,将渲染 3d 的任务放到子线程去完成,缩短用户需要等待的时间。
根据不同的业务场景来选择合适的数据结构,可能在数据量比较少的时候看不出什么区别,但是假如你存储的数据量比较大且数据结构比较复杂的话,这有可能会影响到你的程序性能。一般用的比较多的数据结构就是 array,但我们知道它的查找复杂度是 O(n),因此假如需要快速的查找某个元素,可以使用 map。可以参考下 。
一般开发都使用的 ARC,不太需要开发者去关注内存的创建和释放这块,但假如你使用的是 MRC,并且跟其它语言混杂在一起 (比如 c++ 和 lua) 等的时候,如何确保内存正确释放就是你需要考虑的问题了。有时候一些内存泄漏 instruments 可能无法准确的分析出来,那么就需要自己去排查了,可以使用方法来辅助我们排查内存泄漏的问题,确保程序的正确运行。
不要在 cell 里面嵌套太多的 view,这会很影响滑动的流畅感,而且更多的 view 也需要花费更多的 CPU 跟内存。假如由于 view 太多而导致了滑动不流畅,那就不要在一次就把所有的 view 都创建出来,把部分 view 放到需要显示 cell 的时候再去创建。
由于项目的业务是以及部分框架是用 lua 语言实现的,因此也顺便说一下 lua 这块遇到的问题。lua 号称是最快的脚本语言,一般性能上不会有什么问题,如果 lua 代码要优化的话,网上也有很多这块优化的,这次我主要说个可能影响性能的点 ---lua 的垃圾回收。垃圾回收是一个比较耗时的操作,假如垃圾回收的操作太过于频繁势必会影响到这个程序的运行,比如在 iPod 在利用 lua_cjson 解析一份 4.7M 的 json 文件是花了 3.43s 的时间,后来发现跟垃圾回收这块有关。一般内存的使用量适中的话,可以不用去理他,让 lua 的 incremental 模式自己去处理,正常情况这个会工作的比较好;假如想要自己去控制 gc 的运行,可以设置 gc 的参数,这些参数可能会跟项目有一定的关系,可以自己多试验取最优值。
- //gc 的参数设置,根据情况取最优值
- collectgarbage("setpause", 150)
- collectgarbage("setstepmul", 200)
以上是我在优化过程中的一些记录总结
关于 iOS 图形性能这块的优化
这次我们来说说 iOS app 中滑动的那些事。iOS 为了提高滑动的流畅感,特意在滑动的时候将 runloop 模式切换到 UITrackingRunLoopMode,在这个过程中专心做跟滑动相关的工作,这也就是在滑动过程中为什么 nstimer 无法工作的原因,因为两个没在同一 mode 下面。但我们可能经常会遇到滑动不怎么流畅的情况,比如在项目中碰到在滑动 tableview 的时候不怎么顺畅,感觉有点不爽,即便是在测试中表现最好的 5s(touch 之类的感受更直观)。
?? 那碰到这种情况该怎么处理,分析图像动画性能主要用的是 Core Animation 这个组件,先简单介绍一下里面一些经常用到的选项:
简单介绍完 Core Animation 的一些东西之后我们回过头来看看哪些问题会影响到图形的性能,下面这张图摘自 WWDC2014(,这上面的一些分享非常有技术性)
当你碰到性能问题的时候,你可以思考一下:
- 是否受到CPU或者GPU的限制?
- 是否有不必要的CPU渲染?
- 是否有太多的离屏渲染操作?
- 是否有太多的图层混合操作?
- 是否有奇怪的图片格式或者尺寸?
- 是否涉及到昂贵的view或者效果?
- view的层次结构是否合理?
那么哪些是你最该开始考虑的方向呢?通常发生图形性能问题的时候,比如列表滑动不顺畅、动画卡顿等,大部分都是由于 Offscreen Rendering(离屏渲染) 或者 blending 导致的,因为这在动画的每一帧都会涉及到。
什么是 offscreen-render?offscreen-render 涉及的内容比较多,有 offscreen-render 那就有 onscreen render,onscreen render 指的是 GPU 在当前用于显示的屏幕缓冲区进行渲染,相反 offscreen-render 就是不在当前的屏幕缓存区,而在另外的缓冲区进行渲染,offscreen-render 有两种形式:
使用 CPU 来完成渲染操纵,通常在你使用:
使用 GPU 在当前屏幕缓冲区以外开辟一个新的缓冲区进行绘制,通常发生的情况有:
offscreen-render 对性能到底有什么影响?通常大家说的离屏渲染指的是 GPU 这块 (当然 CPU 这块也会有影响,也需要消耗一定的资源),比如修改了 layer 的阴影或者圆角,GPU 需要做额外的渲染操作。通常 GPU 在做渲染的时候是很快的,但是涉及到 offscreen-render 的时候情况就可能有些不同,因为需要额外开辟一个新的缓冲区进行渲染,然后绘制到当前屏幕的过程需要做 onscreen 跟 offscreen 上下文之间的切换,这个过程的消耗会比较昂贵,涉及到 OpenGL 的 pipeline 跟 barrier,而且 offscreen-render 在每一帧都会涉及到,因此处理不当肯定会对性能产生一定的影响,所以可以的话尽量减少 offscreen-render 的图层,查看哪些图层需要离屏渲染可以用 Instruments 的 Core Animation 工具进行检测,Color Offscreen-Rendered Yellow 选项会将对应的图层标记为黄色。
假如最上层的 view 是不透明的,那直接使用这个 view 的对应颜色之就可以,但如果 view 是透明的,在计算像素的颜色值时就需要计算它下面图层,透明的视图越多,计算量就越大,因此也会对图形的性能产生一定的影响,所以可以的话也尽量减少透明图层的数目。
下面给出一个简单的优化过程,这个 demo 里面涉及到的问题是在实际项目中所碰到的,也就是最上面那张图里列表滑动不流畅情况 --- 由阴影以及圆角导致的 offscreen-render。
整个页面就是一个简单的 tableview,其中头像为圆角,一个 label 有阴影效果,滑动的时候在 iPod 上帧率只有可怜的 28FPS。
其中黄色的区域就是离屏渲染的地方,也就是含有圆角跟阴影的 layer。
设置 label 的阴影效果可以通过:
- cell.sign.layer.shadowOffset = CGSizeMake(0, 2);
- cell.sign.layer.shadowOpacity = 0.5;
- cell.sign.layer.shadowColor = [UIColor blackColor].CGColor;
但是你可以发现这会导致离屏渲染,一个简单的不需要离屏渲染的方法就是制定阴影的路径,也就是设置 layer 的 shadowPath 属性,通过 instruments 发现阴影的地方没有黄色了,帧率也提高到了 40FPS:
- cell.sign.layer.shadowPath = [UIBezierPath bezierPathWithRect:cell.sign.bounds].CGPath;
对于圆角这种类似导致的性能问题,最简单的就是在列表中不要使用圆角,假如要使用圆角的话,一种最快提升性能的方式就是设置 layer 的 shouldRasterize 为 YES:
- cell.layer.shouldRasterize = YES;
- cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
虽然被 Rasterize 的图层也会引起离屏渲染,如下图所示,整个 cell 都被标示为黄色:
layer 设置 shouldRasterize=YES 之后,会把被光栅化的图层保存成位图并缓存起来,其中圆角或者阴影之类的效果也是直接保存到位图当中,当需要渲染到屏幕上的时候只需要到缓存中去取对应的位图进行显示就行了,加快了整个渲染过程。可以通过勾选 instruments core animation 中的 Color Hits Green and Misses Red 选项来查看图层是否被缓存了,如果图层显示为绿色则表示已经被缓存起来了,也就是这个缓冲区的内容被复用了,不用在去重新创建缓冲区,反之则是用红色标示。如下图可以看到设置 shouldRasterize 之后,cell 都被标示为绿色了,如果滑动过程中发现都是红色的证明就有问题了:
再看看现在滑动的帧率:
可以发现现在滚动的性能大大提高了,光栅化对于那些有很多子 view 嵌套在一起、view 的层级复杂或者有很复杂特效效果的图层有很明显的提升,因为这些内容都被缓存到位图当中了。但是使用光栅化需要注意一些内容:
- UIImage *downloadedImage = ...;
- [self.avatarImageView performSelector:@selector(setImage:)
- withObject:downloadedImage
- afterDelay:0
- inModes:@[NSDefaultRunLoopMode]];
来源: http://www.bubuko.com/infodetail-1971945.html