iOS进阶-界面优化

2,181 阅读8分钟

本文讨论框架级的界面优化,其中不会过分讨论具体的实现小知识点或者其他,仅为界面优化提供方向与思路。在文章开始之前你需要具备对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

上面是layout操作的伪代码,简单说就是: 所有的图层layer会被收集到一个全局的LayerTree中,然后遍历调用layer本身的[layer layoutSublayers];方法

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操作的伪代码,简单说就是:

  • 可以看到 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预解码

在图片加载过程中网络传送过来的是二进制数据Data Buffer,需要decode(上文讲过,prepare:CPU解码操作)后,放到像素缓冲区(Image Buffer),之后才能被渲染。而其中decode这步可以放到异步子线程中解码,达到减少耗时减少卡顿的目的。像YYImage,SDWebImage这些都是通过异步子线程画出位图(bit map),然后切到主线程赋值。整个原理和预渲染差不多。

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进行直接显示。

写在最后

如果文章对您有帮助,烦请点个赞,小弟在此谢过了!

参考 :

www.jianshu.com/p/58e7571d7… www.jianshu.com/p/9aa6d23d0…