最近通过内部渠道拿到了一台还未正式发布的新一代iPhone旗舰原型机(具体型号暂时不能透露大家暂时叫它新一代iPhone Pro Max吧后面正式发布了大家就知道是哪款了)作为一个做iOS开发的技术人员拿到新机器第一件事,当然不是体验新功能而是跑性能测试看看新机器的性能表现怎么样。

结果一测发现我们自己的App在这台新机器上的响应时间,居然比老机器还慢这就尴尬了按理说新机器性能更强应该更快才对怎么会更慢呢,于是我就开始了一番性能调优的过程经过几天的排查和优化终于把App的响应时间降了80%效果非常明显今天想分享一下这次性能调优的过程和经验希望能帮到同样在做iOS性能优化的朋友。

(注:本文基于新一代iPhone旗舰原型机的早期系统版本进行调优具体型号和系统版本待正式发布后大家可以对应参考调优思路和方法对所有iOS设备都适用。)

一、问题发现:新机器反而更慢?

拿到新机器后我首先,做的就是把我们团队开发的App装上去跑了一遍性能测试我们的App是一个电商类应用有商品列表商品详情购物车下单等功能我主要测了几个核心页面的启动时间和页面切换的响应时间用的是Xcode自带的Instruments工具测的比较准确。

测完的结果让我很意外在新机器上我们App的首页启动时间平均是1.2秒商品详情页打开时间平均是0.8秒而在我自己的老机器(iPhone X系统版本和新机器的原型系统版本接近)上首页启动时间平均是0.9秒商品详情页打开时间平均是0.6秒新机器,居然比老机器慢了30%左右这就很奇怪了新机器的处理器性能明显更强内存也更大按理说应该更快才对怎么会更慢呢?

一开始我以为是原型机的系统版本是测试版有性能问题,或者bug导致整体变慢,但是我测了一下其他App比如系统自带的Safari照片相机等都很流畅很快比老机器还快说明不是系统整体的性能问题而是我们自己的App的问题在新机器上暴露了出来老机器上可能,因为性能瓶颈在别的地方这个问题没暴露,或者不明显新机器性能更强了反而把我们App自身的性能问题暴露得更明显了这就是典型的"性能提升暴露了应用自身的瓶颈"新硬件性能更强了应用自身的问题就成了新的瓶颈,所以反而显得更慢了。

发现这个问题后我就决定好好做一次性能调优把我们App的性能问题找出来优化掉,不然等新机器正式发布了用户拿到新机器装我们App发现比老机器还慢那就太尴尬了也会影响用户体验和口碑,于是我就开始了性能调优的过程。

二、排查过程:用Instruments找瓶颈

性能调优的第一步不是瞎猜哪里慢,然后瞎优化而是要用工具测量找到真正的瓶颈在哪里,然后针对性地优化,不然很容易做无用功甚至越优化越慢我用的主要工具是Xcode自带的Instruments里面有很多性能分析模板,比如Time Profiler(时间分析看CPU时间花在哪里)Core Animation(核心动画看渲染性能和帧率)Allocations(内存分配看内存使用情况)Leaks(内存泄漏检测内存泄漏)等等这些工具非常强大能帮我们精准定位性能瓶颈。

我首先,用Time Profiler跑了一遍首页启动的过程看CPU时间都花在哪里了结果一跑就发现了问题在首页启动的过程中有一个函数占用了将近40%的CPU时间这个函数是我们自己写的一个处理商品图片圆角和裁剪的函数叫roundedImageWithCornerRadius这个函数在首页启动的时候,被调用了很多次,因为首页是商品列表每个商品都有一张图片需要裁剪成圆角,所以这个函数被反复调用每次调用都很耗时导致首页启动很慢。

我点进去看这个函数的实现发现这个函数是用Core Graphics(CG)来处理图片圆角的每次调用都会创建一个新的图形上下文(CGContext)然后把图片绘制进去裁剪成圆角再生成新的图片返回这个过程非常耗时,因为涉及到图片的解码绘制编码都是CPU密集型操作,而且我们还没有做缓存每次显示商品图片都重新处理一遍,所以在商品列表这种需要显示很多图片的场景就会非常慢CPU占用很高页面自然就卡了。

而且我还发现这个函数,不仅在主线程调用,而且是在cell显示的时候,同步调用的也就是在cellForRowAtIndexPath或者cellForItemAtIndexPath里直接调用这个函数处理图片,然后赋值给imageView这就更糟糕了,因为cellForRowAtIndexPath是在主线程执行的,而且是在列表滚动的过程中频繁调用的,如果在这里做耗时的图片处理就会阻塞主线程导致列表滚动卡顿页面响应慢这就是我们首页启动慢滚动也卡的主要原因。

除了这个图片处理的问题我还用Core Animation工具看了一下渲染性能发现我们的商品cell里有很多视图都设置了cornerRadiusmasksToBounds = true还有很多视图设置了shadowColorshadowOffset等阴影属性这些都会导致离屏渲染(Offscreen Rendering)离屏渲染会让GPU做额外的渲染工作降低渲染性能导致帧率下降页面卡顿特别是在商品列表这种需要快速滚动的场景离屏渲染的影响更明显这也是我们页面响应慢的一个原因。

另外我还用Allocations工具看了一下内存使用情况发现我们的App在首页启动的过程中内存占用飙升得很厉害一下子就涨了一百多兆,然后又慢慢降下来这是,因为我们在首页启动的时候,一次性加载了很多商品数据和图片都放在内存里没有做分页加载和图片缓存管理导致内存占用过高,虽然新机器内存大不会,因为内存不够而崩溃,但是内存占用过高会导致系统频繁回收内存也会影响性能和响应速度这也是一个问题。

通过Instruments的分析我基本定位了几个主要的性能瓶颈:

  1. 图片圆角处理耗时:用Core Graphics同步处理图片圆角耗时且在主线程执行阻塞主线程。
  2. 离屏渲染过多:大量视图设置cornerRadius和masksToBounds以及,阴影导致离屏渲染降低渲染性能。
  3. 内存占用过高:首页一次性加载过多数据和,图片没有分页和缓存管理内存占用飙升。
  4. 主线程做了太多耗时操作:除了图片处理,还有一些数据解析格式化等操作也在主线程做阻塞主线程。

找到瓶颈后就可以针对性地优化了下面说说我具体是怎么优化的。

三、优化过程:针对性解决瓶颈

优化1:图片圆角处理优化用YYWebImage替代异步处理+缓存

第一个优化也是效果最明显的优化就是图片圆角处理的优化原来我们是自己写函数用Core Graphics同步处理图片圆角,而且没有缓存每次都重新处理非常耗时我把这个改成了用第三方图片加载库YYWebImage来处理YYWebImage是一个非常优秀的iOS图片加载库性能非常好支持异步下载图片异步处理图片(包括圆角裁剪缩放等)而且自带内存缓存和磁盘缓存能避免重复下载和处理图片性能非常好。

具体来说,我做了以下改动:

  • 把自己写的roundedImageWithCornerRadius函数去掉不再自己处理图片圆角。
  • 用YYWebImage的yy_setImageWithURL:placeholder:options:completion:方法来加载商品图片在options里设置YYWebImageOptionSetImageWithFadeAnimationYYWebImageOptionAvoidSetImage等选项。
  • 在completion回调里用YYWebImage的yy_imageByRoundCornerRadius:borderWidth:borderColor:方法异步处理图片圆角,然后赋值给imageView。
  • 这样图片的下载和圆角处理都在后台线程异步执行不会阻塞主线程,而且处理好的图片会被缓存下次显示同样的图片直接从缓存取不用重新处理性能大大提升。

这个优化做完后效果非常明显首页启动时间直接从1.2秒降到了0.4秒降了将近70%列表滚动也流畅了很多不再卡顿了,因为图片处理不再阻塞主线程了,而且有缓存不用重复处理这个优化是这次调优中效果最明显的一个优化也让我深刻认识到不要自己造轮子特别是图片加载和处理这种性能敏感的场景用成熟的第三方库比自己写性能好很多也省很多事。

优化2:减少离屏渲染用贝塞尔曲线画圆角避免masksToBounds

第二个优化是减少离屏渲染原来我们的很多视图都设置了cornerRadiusmasksToBounds = true来实现圆角这种方式会导致离屏渲染降低渲染性能特别是,在列表滚动的场景影响很明显我把这些都改成了用贝塞尔曲线(UIBezierPath)画圆角的方式来实现圆角避免离屏渲染。

具体来说,对于需要圆角的视图我用了一个分类(Category)来实现用贝塞尔曲线画圆角核心代码大概是这样的:

- (void)yy_setCornerRadius:(CGFloat)cornerRadius rectCorner:(UIRectCorner)rectCorner {
    UIRectCorner corner = rectCorner ?: UIRectCornerAllCorners;
    CGRect bounds = self.bounds;
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:bounds byRoundingCorners:corner cornerRadii:CGSizeMake(cornerRadius, cornerRadius)];
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.frame = bounds;
    maskLayer.path = maskPath.CGPath;
    self.layer.mask = maskLayer;
}

不过这种方式也有一个问题就是当视图的frame变化的时候,需要重新设置mask不然会变形,所以我在视图的layoutSubviews方法里重新调用了这个方法设置圆角保证frame变化后圆角还是正确的。

另外对于图片的圆角我已经在第一个优化里用YYWebImage异步处理了直接把图片处理成圆角的图片,然后显示这样也不用给imageView设置cornerRadius和masksToBounds了也避免了离屏渲染一举两得。

还有阴影的问题原来我们的一些视图设置了阴影属性(shadowColorshadowOffsetshadowOpacityshadowRadius)这也会导致离屏渲染,因为系统需要先渲染视图的内容再根据内容生成阴影再合成到屏幕上这个过程就是离屏渲染很耗性能我把这些阴影都优化了对于必须要阴影的视图我设置了shadowPath属性指定阴影的路径这样系统就不用根据视图内容动态计算阴影路径了直接用指定的路径生成阴影能大大提升渲染性能避免离屏渲染对于不必须要阴影的视图我直接去掉了阴影用边框,或者其他方式替代减少性能消耗。

这个优化做完后用Core Animation工具再测发现离屏渲染的次数大大减少了GPU的利用率也降了下来列表滚动的帧率从原来的平均45帧提升到了接近60帧(满帧)滚动非常流畅页面响应也更快了效果很明显。

优化3:内存优化分页加载图片缓存管理

第三个优化是内存优化原来我们的首页商品列表是一次性加载所有商品数据和图片都放在内存里没有做分页加载和图片缓存管理导致内存占用过高我把这个改成了分页加载的方式每次只加载一页(20条)商品数据用户滚动到列表底部的时候,再加载下一页这样内存里就不会一次性放太多数据和图片内存占用就降下来了。

另外图片缓存管理我也用YYWebImage自带的缓存管理功能YYWebImage自带内存缓存和磁盘缓存,而且会自动管理缓存大小内存缓存超过设定的上限会自动清理最近最少使用的图片不会让内存无限增长我把内存缓存的上限设置成了50MB磁盘缓存上限设置成了200MB这样既能保证图片加载速度(常用图片在内存缓存里直接取不用重新下载和解码)又不会让内存占用过高影响整体性能。

还有我还处理了一下图片的尺寸原来我们服务器返回的商品图片都是原图很大分辨率很高,但是在手机上显示的时候,其实不需要,那么大的分辨率我和后端同学沟通了一下让他们支持图片裁剪参数根据手机屏幕宽度返回合适尺寸的图片,比如iPhone的屏幕宽度是375pt乘以屏幕比例(@2x或@3x)实际需要的图片宽度大概是750px或1125px不用返回原图(可能2000px甚至更宽)这样图片的文件大小就小了很多下载速度更快解码也更快内存占用也更小性能自然就更好了这个优化,虽然需要后端配合,但是效果也很明显特别是对于网络不好的用户体验提升很大。

这个优化做完后用Allocations工具再测发现首页启动的内存占用从原来的飙升100多MB降到了只涨20多MB而且很快就稳定下来了不会再无限增长系统也不用频繁回收内存了App的整体性能和稳定性都提升了很多也避免了,因为内存过高导致的系统killApp的情况。

优化4:主线程优化把耗时操作放到后台线程

第四个优化是主线程优化除了前面说的图片处理原来我们,还有一些其他的耗时操作也在主线程做,比如JSON数据解析日期格式化价格计算富文本排版等等这些操作,虽然单次耗时不像图片处理,那么长,但是加起来也不少,而且都在主线程执行会累积起来阻塞主线程影响页面响应速度我把这些操作都尽量放到后台线程去做做完再回到主线程更新UI这样主线程就只做UI更新的工作不会被耗时操作阻塞响应速度自然就快了。

具体来说,我做了以下改动:

  • JSON数据解析放到后台线程做用NSJSONSerialization或者MJExtension等库在,后台线程解析JSON成模型对象解析完再回到主线程刷新UI。
  • 日期格式化和价格计算也放到后台线程做,或者提前计算好缓存起来不要在cellForRowAtIndexPath里每次都重新计算。
  • 富文本排版也放到后台线程做提前把需要显示的富文本排版好缓存起来在cellForRowAtIndexPath里直接取缓存的富文本赋值不用每次都重新排版。
  • 一些其他的耗时操作,比如文件读写数据库查询等也都放到后台线程做不要阻塞主线程。

另外我还优化了一下cellForRowAtIndexPath的执行效率原来我们的cellForRowAtIndexPath里做了很多事情除了前面说的图片处理数据计算,还有一些视图创建和布局的操作我把能提前做的都提前做了,比如cell的创建和初始化用registerClass:forCellReuseIdentifier:提前注册cell类不要在cellForRowAtIndexPath里每次都判断cell是否存在再创建,还有cell的布局尽量用Auto Layout提前设置好约束不要在cellForRowAtIndexPath里每次都重新设置frame和约束这样cellForRowAtIndexPath的执行时间就大大缩短了列表滚动更流畅页面响应也更快。

这个优化做完后用Time Profiler再测发现主线程的耗时操作大大减少了主线程大部分时间都在做UI更新和事件响应不再被耗时操作阻塞页面的响应速度又提升了一截用户点击按钮,或者切换页面的时候,几乎是即时响应没有延迟感了体验非常好。

四、优化效果:响应时间降了80%

经过以上几个方面的优化我再用Instruments测了一遍性能结果非常让人满意首页启动时间从原来的平均1.2秒降到了平均0.24秒降了80%商品详情页打开时间从原来的平均0.8秒降到了平均0.15秒降了80%多列表滚动帧率从原来的平均45帧提升到了接近60帧(满帧)滚动非常流畅内存占用也从原来的飙升100多MB降到了稳定在30MB左右整体性能提升非常明显用户体验大大改善。

而且,不仅在新机器上性能提升明显在老机器上性能也提升了很多,因为这些优化都是通用的性能优化对所有iOS设备都适用老机器上首页启动时间也从原来的0.9秒降到了0.3秒左右列表滚动也更流畅了内存占用也降了下来避免了老机器上,因为内存不够导致的卡顿和崩溃可以说,这次性能调优,不仅解决了新机器上更慢的尴尬问题还整体提升了App在所有设备上的性能和用户体验非常值得。

通过这次性能调优我也总结了一些iOS性能优化的经验和心得分享给大家:

  1. 性能优化测量先行:不要瞎猜哪里慢,然后瞎优化一定要用Instruments等工具测量找到真正的瓶颈再针对性优化,不然很容易做无用功甚至越优化越慢。
  2. 不要阻塞主线程:主线程只做UI更新的工作所有耗时操作(图片处理数据解析文件读写数据库查询等)都要放到后台线程做做完再回到主线程更新UI这是iOS性能优化的核心原则。
  3. 避免离屏渲染:尽量减少cornerRadius + masksToBounds和阴影等会导致离屏渲染的操作用贝塞尔曲线画圆角设置shadowPath等方式替代提升渲染性能保证列表滚动流畅。
  4. 图片加载用成熟的第三方库:不要自己写图片加载和处理的代码特别是性能敏感的场景用YYWebImageSDWebImage等成熟的第三方库它们做了很多性能优化(异步加载处理缓存等)比自己写性能好很多也省很多事。
  5. 注意内存管理:不要一次性加载过多数据和图片做分页加载和缓存管理控制内存占用避免内存过高导致的性能问题和系统killApp。
  6. 优化服务器返回的图片尺寸:和后端配合根据手机屏幕尺寸返回合适大小的图片不要返回原图减少下载时间解码时间和内存占用提升图片加载性能和用户体验。
  7. 性能优化要持续做:性能优化不是一次性的工作要持续做每次加新功能都要注意性能用Instruments测一测发现问题及时优化不要等问题积累多了再集中优化那样成本更高也更难优化。

五、写在最后

以上就是我这次在新一代iPhone Pro Max原型机上做性能调优的过程和经验从发现新机器反而更慢的尴尬问题到用Instruments定位瓶颈再到针对性优化最后把响应时间降了80%整个过程,虽然花了几天时间,但是收获很大,不仅解决了问题提升了App性能和用户体验也积累了iOS性能优化的经验对iOS系统和性能优化有了更深的理解。

其实性能优化是iOS开发中非常重要的一部分直接影响用户体验和产品口碑很多开发者可能觉得功能做出来就行性能差一点没关系,但是实际上,用户对性能的感知是非常明显的一个App启动慢滚动卡响应延迟用户很快就会感觉到,然后就会觉得这个App很烂不好用甚至卸载,所以性能优化绝对不是可有可无的事情而是必须重视的事情特别是现在新设备性能越来越强用户对性能的期望也越来越高,如果你的App在新设备上都跑不流畅用户肯定会不满意,所以一定要重视性能优化持续提升App性能和用户体验。

另外这次经历也让我深刻体会到新硬件性能提升了反而会暴露应用自身的性能问题,因为老硬件上性能瓶颈可能在硬件本身应用自身的问题被掩盖了新硬件性能更强了应用自身的问题就成了新的瓶颈暴露出来,所以不要以为新硬件性能强了应用就自然会快就不用做性能优化了,恰恰,相反新硬件性能越强越能暴露应用自身的性能问题越需要我们做好性能优化让应用在新硬件上发挥出应有的性能给用户更好的体验。

最后用一句话结束这篇文章也是我这次性能调优最大的心得"性能优化没有终点,只有更好测量先行针对性优化持续改进才能让你的App越来越流畅用户体验越来越好。"

愿大家都能写出高性能的iOSApp给用户流畅好用的体验也愿新一代iPhone正式发布后大家的App都能在,上面跑得飞快不会出现新机器反而更慢的尴尬加油!