谈
本文讨论框架级的界面优化,其中不会过分讨论具体的实现小知识点或者其他,仅为界面优化提供方向与思路。在文章开始之前你需要具备对Runloop非常熟悉的能力
1、界面渲染流程
1.1、框架关系

- UIKit:负责将绘画事件传给下层
- CoreAnimation:核心动画层,负责将图层对象进行动画操作,其与UIKit紧密关联
- openGL ES/Metal:使用GPU做图层渲染计算
- Core Graphics:使用CPU做图层渲染计算
- Graphics Hardware:底层硬件做具体渲染操作
1.2、渲染流程
下面是一张图形生成过程图,来自WWDC2014。

-
Application 中布局 UIKit 视图控件间接的关联 Core Animation 图层
-
Core Animation 图层相关的数据提交到 iOS Render Server,即 OpenGL ES & Core Graphics
-
Render Server 将与 GPU 通信把数据经过处理之后传递给 GPU
-
GPU 调⽤ iOS 当前设备渲染相关的图形设备 Display
1.3 commit Transaction 内部做了什么
这里重点讲下commit Transaction 因为后面优化方面就是针对这些部分去做优化的
- layout 构建视图:CPU负责计算视图frame、约束,遍历的操作[UIView layerSubview],[CALayer layoutSubLayers]
- Display 绘制视图:调用drawRect()进行绘制
- Prepare 额外的Core Animation 工作,比如解码
- Commit 打包图层并将它们发送到 Render Server(openGL ES/Metal),这个一个递归操作,看图层的数量
1.3.1 layout

1.3.2 display 绘制流程
绘制流程分两种:[CALayer display]绘制和后台存储区(backingStore)绘制
[CALayer display]绘制
- 由[CALayer display] 发起绘制流程
- [CALayer drawIncontext:]启动绘制
- [UIView drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx]由代理UIview开始真正的绘制操作
-
- (void)displayLayer:(CALayer *)layer:UIviwe显示位图
-
- (void)closeContext:关闭上下文
后台存储区(backingStore)绘制
[CALayer display]绘制 这一套是由系统发起的,如果我们重写了[UIView drawRect:(CGRect)rect],那么就会进入到后台存储区去做绘制,其内部绘制好后,直接显示当前view中

- 可以看到 display这个方法时由runloop调用的
- 先进行layout后display
- UIview是 CALayer的代理
1.4 隐式动画&显示动画
每个图层都是可以做动画的,每个图层在将要显示的时候都会调用这个方法+ (nullable id<CAAction>)defaultActionForKey:(NSString *)event;根据返回的CAAction对象类型做不同处理
-
空对象:UIView在响应代理时默认会返回⼀个NSNull对象,表示属性修改后,不实现任何的动作,根据修改后的属性值直接更新视图。
-
nil:⼿动创建并添加到视图上的CALayer或其⼦类在属性修改时,没有获取到具体的修改⾏为。此时被修改的属性会被CATransaction记录,最终在下⼀个runloop的回调中⽣成动画来响应本次属性修改。由于这个过程⾮开发者主动完成的,因此这种动画被称作隐式动画,例如
self.View.backgroundColor = [UIColor redColor] -
CAAction的⼦类:如果返回的是CAAction对象,会直接开始动画来响应图层属性的修改。⼀般返回的对象多为CABasicAnimation类型,对象中包装了动画时⻓、动画初始/结束状态、动画时间曲线等关键信息。当CAAction对象被返回时,会⽴刻执⾏动作来响应本次属性修改;例如:
[UIView animateWithDuration:(NSTimeInterval) animations:<#^(void)animations#>]
1.5 UI操作本质
- 程序一启动Runloop会注册一个观察者observer监听
beforewating 和 exit两个阶段 - 当我们在操作UI的时候,被操作的UI/CALayer会被存入一个全局的待处理的容器当中。
- 而当observer收到监听的消息(回调Block)的时候,会去全局容器中遍历代理处的操作对象(UI/CALayer)

2、UIView&CALayer
2.1 继承关系
- UIView : UIResponder:UIView 负责响应事件
- CALayer : NSObject:CALayer 负责绘制 UI
2.2 UIView 是对 CALayer 封装属性
UIView 中持有一个 layer 对象,同时这个 layer 对象 delegate,UIView 和 CALayer 协同工作。
平时我们对 UIView 设置 frame、center、bounds 等位置信息,其实都是 UIView 对 CALayer 进一层封装,使得我们可以很方便地设置控件的位置;例如圆角、阴影等属性, UIView 就没有进一步封装,所以我们还是需要去设置 Layer 的属性来实现功能
2.3 UIView 是 CALayer 的代理
UIView 持有一个 CALayer 的属性,并且是该属性的代理,用来提供一些 CALayer 行的数据,例如动画和绘制。
//绘制相关
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
//动画相关
- (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event;
3、卡顿检测
3.1 卡顿产生原因
- 图像是由电子枪一行一行快速扫描(打)出来的
- 电子枪打之前会发出一个水平同步信号(HSync),其频率就是显示器的刷新频率
- CPU计算好frame等属性后,将计算的好内容交给GPU去做渲染
- 渲染好的“图片”会放在帧缓冲区
- 最后显示器会根据水平同步信号(HSync)逐行读取缓冲区的的数据,然后通过电子枪打出图像
- 由于垂直同步的机制,如果在一个 HSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
3.2 卡顿检测方案
3.2.1 基于FPS - YYFPSLabel
这个是YYKit中提供帧率显示控件,内部使用CADisplayLink(类似于一个timer)。其原理是CADisplayLink的频率是和当前界面刷新保持一致的,而CADisplayLink会被加入到当前Runloop中的由runloop调用,所以我们可以计算一段时间内调用ADisplayLink次数/间隔时间来计算屏幕FPS帧率
PS:YYFPSLabel内部使用NSProxy去解决循环引用问题
3.2.2 基于Runloop - Matrix微信自研卡顿分析工具
在上面的内容中我们说了UI操作的具体实现,其核心就是由Runloop去处理UI显示,那么我们可以这么认为从Runloop任务开始(beforeSource)--任务结束(beforewating)这段时间就是任务处理的时间t,如果这个t过长我们就可以认为产生了卡顿。Matrix就是利用了这个原理;
4、卡顿解决方案
4.1预排版
在异步子线程中提前计算好布局后在,在切回主线程更新布局;下面的例子中,cell被提前计算了高度
- (void)loadData{
//外面的异步线程:网络请求的线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//加载`JSON 文件`
NSString *path = [[NSBundle mainBundle] pathForResource:@"timeLine" ofType:@"json"];
NSData *data = [[NSData alloc] initWithContentsOfFile:path];
NSDictionary *dicJson=[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
for (id json in dicJson[@"data"]) {
TimeLineModel *timeLineModel = [TimeLineModel yy_modelWithJSON:json];
[self.timeLineModels addObject:timeLineModel];
}
for (TimeLineModel *timeLineModel in self.timeLineModels) {
TimeLineCellLayout *cellLayout = [[TimeLineCellLayout alloc] initWithModel:timeLineModel];
[self.layouts addObject:cellLayout];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.timeLineTableView reloadData];
});
});
}
4.2预渲染&预解码
4.2.1预渲染
在异步子线程中提前将圆角渲染好,下面是一个cell上圆角实现例子
- (void)configureLayout:(TimeLineCellLayout *)layout{
_layout = layout;
// 圆角的处理:主线程当中,这样会造成卡顿
// [self.iconButton sd_setImageWithURL:[NSURL URLWithString:layout.timeLineModel.iconUrl] forState:UIControlStateNormal];
// self.iconButton.layer.cornerRadius = 5;
//这里SDwebImage并不会直接将图片渲染到button上,而是在异步子线程中提前将圆角画好
[self.iconButton sd_setImageWithURL:[NSURL URLWithString:layout.timeLineModel.iconUrl] forState:UIControlStateNormal placeholderImage:nil options:SDWebImageAvoidAutoSetImage completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
//在回调中提前将圆角画好
[self.iconButton setImage:[image cornerRadius:self.iconButton.bounds.size.width/ 2.0 size:self.iconButton.bounds.size] forState:UIControlStateNormal];
}];
}
当然最好的方式还是直接找UI妹子画带有圆角的图,直接呈现。
4.2.2预解码

4.3按需加载
4.3.1 VVeboTableViewDemo
这个是微博加载首页是用到的解决方案:
- 只加载当前可视范围的cell+ 前3行后3行,减少cell计算处理数量。但是这个有个缺点就是在快速滑动的时候,会有空白也显示
4.3.2 Runloop分发
举个例子,当一个加载大量图片cell的tableView滑动时,由于图片的解码占用了大量的计算能力,CPU还没有处理完解码,此时滑动操作又来的就会造成卡顿。即滑动操作和图片加载操作重合了。所以我们可以使用runloop监听其将要休眠(kCFRunloopBeforeWaitng)的时候,我们再去做图片加载操作;

4.5 减少图层的层级
如果图层非常多那么在上文提到的layout阶段和commit阶段就要进行多次递归操作;所以减少图层的层级,就是减小CPU计算压力;这是种思路,可以知道我们去构建视图的结构。
4.6 异步渲染
当然如果上面这些都不能满足对性能的追求,那么可以考虑通过Core Graphics 合成一张位图bitMap
4.6.1 Graver
这个美团开源的一个框架,它通过异步绘制可以将多个图层合层一张位图

4.6.2 YYAsyncLayer
这又是YYKit中一个开源项目的中的组件;YYAsyncLayer为了异步绘制而继承CALayer的子类。通过使用CoreGraphic相关方法,在子线程中绘制内容Context,绘制完成后,回到主线程对layer.contents进行直接显示。
写在最后
如果文章对您有帮助,烦请点个赞,小弟在此谢过了!
参考 :