iOS 卡顿优化

卡顿原因分析:

1.屏幕显示图像的原理:

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

  • GPU: 负责纹理的渲染(将数据渲染到屏幕))

  • CPU 和 GPU 的协作:


    由上图可知,要在屏幕上显示视图,需要CPU和GPU一起协作,CPU计算好显示的内容提交到GPU,GPU渲染完成后将结果放到帧缓存区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

  • 垂直同步技术: 让CPU和GPU在收到vSync信号后再开始准备数据,防止撕裂感和跳帧,通俗来讲就是保证每秒输出的帧数不高于屏幕显示的帧数。

  • 双缓冲技术: iOS是双缓冲机制,前帧缓存和后帧缓存,CPU计算完GPU渲染后放入缓冲区中,当GPU下一帧已经渲染完放入缓冲区,且视频控制器已经读完前帧,GPU会等待vSync(垂直同步信号)信号发出后,瞬间切换前后帧缓存,并让CPU开始准备下一帧数据
    安卓4.0后采用三重缓冲,多了一个后帧缓冲,可降低连续丢帧的可能性,但会占用更多的CPU和GPU

  • 屏幕显示图像的原理: 图像的显示可以简单理解成先经过CPU的计算/排版/编解码等操作,然后交由GPU去完成渲染放入缓冲中,当视频控制器接受到vSync时会从缓冲中读取已经渲染完成的帧并显示到屏幕上。

iOS手机默认刷新率是60hz,所以GPU渲染只要达到60fps就不会产生卡顿。
以60fps为例,vSync会每16.67ms发出,如在16.67ms内没有准备好下一帧数据就会使画面停留在上一帧,产生卡顿,例如图中第3帧渲染完成之前一直显示的是第2帧的内容。

2.图片加载流程

  • 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
  • 然后将生成的 UIImage 赋值给 UIImageView
  • 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化
  • 在主线程的下一个 runloop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
    (1).分配内存缓冲区用于管理文件 IO 和解压缩操作
    (2). 将文件数据从磁盘读到内存中;
    (3).将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
    (4). 最后 Core Animation 中CALayer使用未压缩的位图数据渲染 UIImageView 的图层。
    (5). CPU计算好图片的Frame,对图片解压之后.就会交给GPU来做图片渲染
  • 渲染流程:
    (1).GPU获取获取图片的坐标
    (2).将坐标交给顶点着色器(顶点计算)
    (3).将图片光栅化(获取图片对应屏幕上的像素点)
    (4). 片元着色器计算(计算每个像素点的最终显示的颜色值)
    (5).从帧缓存区中渲染到屏幕上
为什么要解压缩图片:

既然图片的解压缩需要消耗大量的 CPU 时间,那么我们为什么还要对图片进行解压缩呢?是否可以不经过解压缩,而直接将图片显示到屏幕上呢?答案是否定的。要想弄明白这个问题,我们首先需要知道什么是位图
其实,位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点。我们在应用中经常用到的 JPEG 和 PNG 图片就是位图。
不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比。
所以在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,这就是为什么需要对图片解压缩的原因。

3. 卡顿原因:

上面讲解了图片显示的原理和屏幕渲染的原理,造成卡顿的原因有很多,最主要的原因是因为发生了掉帧。
由上面屏幕显示的原理,采用了垂直同步机制的手机设备。在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
在开发中,CPU和GPU中任何一个压力过大,都会导致掉帧现象,所以在开发时,也需要分别对CPU和GPU压力进行评估和优化。


卡顿监控:

  • 主线程卡顿监控。通过子线程监测主线程的runLoop,判断两个状态区域之间的耗时是否达到一定阈值。
  • FPS监控。要保持流畅的UI交互,App 刷新率应该当努力保持在 60fps。FPS的监控实现原理,上面已经探讨过这里略过。

1.RunLoop监测卡顿

loop的状态

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

    kCFRunLoopEntry , // 进入 loop

    kCFRunLoopBeforeTimers , // 触发 Timer 回调

    kCFRunLoopBeforeSources , // 触发 Source0 回调

    kCFRunLoopBeforeWaiting , // 等待 mach_port 消息

    kCFRunLoopAfterWaiting ), // 接收 mach_port 消息

    kCFRunLoopExit , // 退出 loop

    kCFRunLoopAllActivities  // loop 所有状态改变

}

NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿。

实现思路:只需要另外再开启一个线程,实时计算这两个状态区域之间的耗时是否到达某个阀值,便能揪出这些性能杀手。

阀值设定:假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)

2.FPS && CADisplayLink

CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它总是在屏幕完成一次更新之前启动,它的接口设计的和NSTimer很类似,所以它实际上就是一个内置实现的替代,但是和timeInterval以秒为单位不同,CADisplayLink有一个整型的frameInterval属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval为2,就是说动画每隔一帧执行一次(一秒钟30帧)。使用CADisplayLink监控界面的FPS值.

卡顿优化:

1.CPU优化:

  • 尽量用轻量级的对象,比如用不到事件处理的地方使用CALayer取代UIView

  • 尽量提前计算好布局(例如cell行高)

  • 不要频繁地调用和调整UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的调用和修改(UIView的显示属性实际都是CALayer的映射,而CALayer本身是没有这些属性的,都是初次调用属性时通过resolveInstanceMethod添加并创建Dictionary保存的,耗费资源)

  • Autolayout会比直接设置frame消耗更多的CPU资源,当视图数量增长时会呈指数级增长.

  • 图片的size最好刚好跟UIImageView的size保持一致,减少图片显示时的处理计算

  • 控制一下线程的最大并发数量

  • 尽量把耗时的操作放到子线程

  • 文本处理(尺寸计算、绘制、CoreText和YYText):
    (1). 计算文本宽高boundingRectWithSize:options:context: 和文本绘制drawWithRect:options:context:放在子线程操作
    (2). 使用CoreText自定义文本空间,在对象创建过程中可以缓存宽高等信息,避免像UILabel/UITextView需要多次计算(调整和绘制都要计算一次),且CoreText直接使用了CoreGraphics占用内存小,效率高。(YYText)

  • 图片处理(解码、绘制)
    图片都需要先解码成bitmap才能渲染到UI上,iOS创建UIImage,不会立刻进行解码,只有等到显示前才会在主线程进行解码,固可以使用Core Graphics中的CGBitmapContextCreate相关操作提前在子线程中进行强制解压缩获得位图.

  • TableViewCell 复用: 在cellForRowAtIndexPath:回调的时候只创建实例,快速返回cell,不绑定数据。在willDisplayCell: forRowAtIndexPath:的时候绑定数据(赋值)

  • 高度缓存: 在tableView滑动时,会不断调用heightForRowAtIndexPath:,当 cell 高度需要自适应时,每次回调都要计算高度,会导致 UI 卡顿。为了避免重复无意义的计算,需要缓存高度。

  • 视图层级优化: 不要动态创建视图,在内存可控的前提下,缓存subview。善用hidden。

  • 减少视图层级: 减少subviews个数,用layer绘制元素. 少用 clearColor,maskToBounds,阴影效果等。

  • 减少多余的绘制操作.

  • 图片优化:
    (1)不要用JPEG的图片,应当使用PNG图片。
    (2)子线程预解码(Decode),主线程直接渲染。因为当image没有Decode,直接赋值给imageView会进行一个Decode操作。
    (3)优化图片大小,尽量不要动态缩放(contentMode)。
    (4)尽可能将多张图片合成为一张进行显示。

  • 减少透明 view: 使用透明view会引起blending,在iOS的图形处理中,blending主要指的是混合像素颜色的计算。最直观的例子就是,我们把两个图层叠加在一起,如果第一个图层的透明的,则最终像素的颜色计算需要将第二个图层也考虑进来。这一过程即为Blending。

  • 理性使用-drawRect:
    当你使用UIImageView在加载一个视图的时候,这个视图虽然依然有CALayer,但是却没有申请到一个后备的存储,取而代之的是使用一个使用屏幕外渲染,将CGImageRef作为内容,并用渲染服务将图片数据绘制到帧的缓冲区,就是显示到屏幕上,当我们滚动视图的时候,这个视图将会重新加载,浪费性能。所以对于使用-drawRect:方法,更倾向于使用CALayer来绘制图层。因为使用CALayer的-drawInContext:,Core Animation将会为这个图层申请一个后备存储,用来保存那些方法绘制进来的位图。那些方法内的代码将会运行在 CPU上,结果将会被上传到GPU。这样做的性能更为好些。
    静态界面建议使用-drawRect:的方式,动态页面不建议。

  • 按需加载:
    局部刷新,刷新一个cell就能解决的,坚决不刷新整个 section 或者整个tableView,刷新最小单元元素。
    利用runloop提高滑动流畅性,在滑动停止的时候再加载内容,像那种一闪而过的(快速滑动),就没有必要加载,可以使用默认的占位符填充内容。

  • RunLoop优化tableview加载多图:如果要从网络加载高清大图到UITableViewCell上,而且每个Cell上面加载多张图片,当cell数量过多的时候,我们需要保持流畅度和加载速度。

1,因为这里用到了Runloop循环,那么我们可以监听到runloop的每次循环,
在每一次循环当中我们考虑去进行一次图片下载和布局。
2,既然要在每次循环执行一次任务,
我们可以先把所有图片加载的任务代码块添加到一个数组当中,
每次循环取出第一个任务进行执行。*
3,因为runloop在闲置的时候会自动休眠,
所以我们要想办法让runloop始终处于循环中的状态。

2.GPU 优化

  • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示

  • GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸

  • GPU会将多个视图混合在一起再去显示,混合的过程会消耗CPU资源,尽量减少视图数量和层次

  • 减少透明的视图(alpha<1),不透明的就设置opaqueYESGPU就不会去进行alpha的通道合成

  • 尽量避免出现离屏渲染.

  • 合理使用光栅化 shouldRasterize: 光栅化是把GPU的操作转到CPU上,生成位图缓存,直接读取复用。 CALayer会被光栅化为bitmapshadowscornerRadius等效果会被缓存。 更新已经光栅化的layer,会造成离屏渲染。 bitmap超过100ms没有使用就会移除。 受系统限制,缓存的大小为 2.5X Screen Size。 shouldRasterize 适合静态页面显示,动态页面会增加开销。如果设置了shouldRasterize为 YES,那也要记住设置rasterizationScalecontentsScale

  • 异步渲染.在子线程绘制,主线程渲染。例如 VVeboTableViewDemo

  • 什么是离屏渲染?
    在OpenGL中,GPU有2种渲染方式
    On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作
    Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
    离屏渲染消耗性能的原因
    需要创建新的缓冲区
    离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕

  • 什么操作会导致离屏渲染?
    1.光栅化,layer.shouldRasterize = YES
    2.遮罩,layer.mask
    3.圆角,同时设置layer.masksToBounds = YES、layer.cornerRadius大于0. 考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片
    4.阴影,layer.shadowXXX,如果设置了layer.shadowPath就不会产生离屏渲染.
    5.layer.allowsGroupOpacity为YES,layer.opacity的值小于1.0

参考:IOS面试考察(九):性能优化相关问题

你可能感兴趣的:(iOS 卡顿优化)