Jerry's Blog

Less is more, Live and learning

Hello,I'm an iOS developer in China.


iOS页面卡顿及性能优化

卡顿产生的原因

在屏幕成像的过程中,CPU和GPU的职责及

CPU:负责对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)。

GPU:负责变换、合成、纹理的渲染。

CPU 把计算好的数据给 GPU,GPU 来渲染,渲染后的数据放在帧缓存(缓冲区,有两块缓冲区,前帧缓存和后帧缓存,协调使用,效率高)中。然后,视频控制器从缓冲区获取渲染后的数据显示在屏幕上。

图像显示原理

引用YY大神对于图像显示原理的分析

img1

一帧(或一页)数据就是:一个垂直同步信号(VSync )和一个水平同步信号(HSync)的组合。先发送一个垂直同步信号(VSync ),代表即将显示一页,再发送一个水平同步信号(HSync)就显示一帧。如果当下一次VSync信号到来之前,CPU和GPU还没有计算完成,就会产生卡顿。

在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因

从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。

卡顿的优化

从上述结论中可以得出,造成卡顿的原因是由CPU和GPU造成的,所有优化的时候也要从这两个方面来着手。那么解决卡顿的主要思路就是尽可能减少CPU、GPU资源消耗,按照60FPS的刷新帧率,每隔16ms就会有一次VSync信号。

CPU优化

  • 对象创建:尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用 CALayer 取代 UIView 。尽量推迟对象创建时间,并把对象的创建分到多个任务中去。如果对象可以复用,可以将对象放入缓存池中复用,这样cpu消耗会很小。

  • 对象调整:不要频繁地调整 UIView 的布局相关属性, 比如 frameboundscentertransform等属性,尽量避免调整视图层次、添加和移除视图。

  • 布局计算:尽量在后台提前计算好布局、并且对布局进行缓存,会提高很多性能。因为对这些属性调整会非常消耗资源,需一次性调整好,不要多次、频繁的计算修改这些属性。

  • Autolayout:对于复杂视图Autolayout 会比直接设置 frame 消耗更多的 CPU 资源。设置frame可以用一些工具(比如常用的:left/right/top/bottom/width/height 快捷属性),或使用ComponentKit、AsyncDisplayKit 等框架。

  • 文本处理:文本的宽高计算会占用一部分资源,可以参数UILabel内部实现,用:[NSAttributedString boundingRectWithSize:options:context:]来计算文本宽高,用[NSAttributedString drawWithRect:options:context:] 来绘制文本。为了不占用主线程资源,尽量放到后台处理。

  • 图片处理:图片在创建为UIImage时不会立即解码,在设置到UIImageView中,CALayer被提交到GPU中时才会去解码并且会发生在主线程。为了绕开这个机制,会在后台先把图片绘制到CGBitmapContext 中,然后从 Bitmap 直接创建图片。目前常见的网络图片库都自带这个功能。图片的绘制过程也可以放到子线程进行,比如常见的 [UIView drawRect:] ,原理如下:

    - (void)display {
        dispatch_async(backgroundQueue, ^{
            CGContextRef ctx = CGBitmapContextCreate(...);
            // draw in context...
            CGImageRef img = CGBitmapContextCreateImage(ctx);
            CFRelease(ctx);
            dispatch_async(mainQueue, ^{
                layer.contents = img;
            });
        });
    }
    
  • 图片的 size 最好刚好跟 UIImageViewsize 保持一致。

  • 控制子线程的最大并发数量。

GPU优化

  • 纹理渲染:GPU 能处理的最大纹理尺寸是 4096x4096,一旦超过这个尺寸,就会占用 CPU 资源进行预处理,这对CPU和GPU都会带来额外的资源消耗。 所以纹理尽量不要超过这个尺寸。
  • 视图混合:GPU会将多个视图混合在一起,如果视图结构复杂会消耗过多GPU资源。所以,应该尽量减少视图数量和层次,尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示。并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。
  • 图形生成:设置CALayer 的border、圆角、阴影、遮罩(mask)通常都会触发离屏渲染,而离屏渲染通常是发生在GPU中。在开发中尽量减少这个属性的使用,可以尝试开启CALayer.shouldRasterize的属性,会把原本离屏渲染操作转嫁到CPU上。对应只需要圆角的某些场合,可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。也可以把需要显示的图形在后台线程绘制为图片。

卡顿检测

  1. 主线程卡顿监控。通过子线程监测主线程的 runLoop,判断两个状态区域之间的耗时是否达到一定阈值。具体原理和实现,这篇文章介绍得比较详细。
  2. FPS监控。要保持流畅的UI交互,App 刷新率应该当努力保持在 60fps。监控实现原理比较简单,通过记录两次刷新时间间隔,就可以计算出当前的 FPS。

做性能优化的时候,不要过早的优化。根据业务需求,对流畅度要求高的地方优先进行优化,走修改代码- > Profile(测量)-> 修改代码的流程来逐步优化。

参考资料

iOS 保持界面流畅的技巧

iOS性能优化_讲的比较全面

微信读书 iOS 性能优化总结

最近的文章

iOS代码混淆之方法名替换(一)

这是iOS代码混淆系列文章的第一篇,后两篇可以点击下方链接查看 iOS代码混淆之资源替换(二) iOS代码混淆之编译优化(三)前言在iOS开发中,某些项目为了保护代码安全,不被黑客攻破,通常需要在上架之前对源代码进行加密混淆,下面就介绍一种简单可行的Objective-C代码混淆方案。混淆思路由于现在的逆向技术越来越高超,各种破解方案层出不穷,其中就可以使用class-dump命令行工具轻易的dump出demo.app文件中的所有头文件,代码立马回暴露在黑客面前,对项目产生了很多不...…

iOS 代码混淆继续阅读
更早的文章

iOS数据埋点方案

代码埋点代码埋点是一种常规且直观的方案,需要开发人员在需要埋点的页面或点击事件的响应方法中注入埋点统计相关方法进行数据统计上报。也可以接入三方统计分析SDK来实现,比如友盟UMCAnalytics统计分析库。实现方式如下代码所示:// 通过在HomeViewController 中的“页面已显示”和“页面已消失”两个回调中注入统计代码实现埋点@implementation HomeViewController//...other methods- (void)viewDidAppear:(...…

iOS继续阅读