iOS - 界面优化

项目开发过程中,随着页面复杂度及代码书写问题,某些界面可能会出现卡顿现在,这时我们就需要对界面进行优化.

界面展示原理

通常来说,计算机中的显示过程是通过CPU、GPU协调工作来将界面显示到屏幕上的

  1. CPU计算好显示的内容,提交到GPU
  2. GPU经过变换、合成、渲染完成后将渲染的结果放入帧缓存区(frameBuffer)
  3. 随后视频控制器(videoController)会按照垂直同步信号(VSync)逐行读取frameBuffer中的数据
  4. 经过可能的数模转换传递给显示器进行显示

11111.png 最开始时,frameBuffer只有一个,这种情况下frameBuffer的读取和刷新有很大的效率问题,为了解决这个问题,引入了双缓存区,也就是双缓冲机制.在这种情况下,GPU会预先渲染好一帧放入frameBuffer,让videoController读取,当下一帧渲染好后,GPU会直接将videoController的指针指向第二个frameBuffer.

双缓存区虽然解决了效率问题,可是会出现新的问题:当videoController还未读取完成时,例如内容只显示了一半,GPU将新的一帧内容提交到frameBuffer,并将videoController的指针指向新的frameBuffer,videoController就会将新的一帧数据的下半段显示到屏幕上,造成屏幕撕裂

为了解决这个问题,采用了VSync,当开启VSync后,GPU会等待显示器的VSync信号发出后,才进行新一帧的渲染和frameBuffer的更新.

目前iOS设备中采用的就是双缓存区 + VSync

界面卡顿的原因

我们知道了界面如果展示的,下面我们来看看界面卡顿的原因

VSync信号到来后,系统图形服务会通过CADDisplayLink等机制通知App进行 CPU -> GPU -> 渲染到frameBuffer -> 等待下一次VSync信号到来时显示到屏幕上.由于垂直同步到机制,如果在一个VSync时间内,CPU/GPU没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,显示屏会保留之前到内容不变,也就是俗称的掉帧

11112.png 从图可以看出,当CPU/GPU阻塞了显示流程时,就会造成掉帧现象,在开发中,我们需要进行卡顿检测来判断是否发生掉帧现象,已经发生掉帧现象的相应优化

卡顿监测

卡顿监测的方法一般有两种

  • FPS监测:为了保持流畅的UI交互,APP的刷新频率应保持在60fps左右,其原因是因为iOS设备默认的刷新频率≈60次/s,而一次刷新(VSync信号发出)的间隔是 1000/60 ≈ 16.67ms,所以在信号发出的间隔内没准备好下一帧,就会产生卡顿
  • runloop监测: 通过子线程监测主线程的Runloop,判断两个状态KCFRunloopBeforeSourcesKCFRunloopAfterWaiting之间的耗时是否到达设定的阀值
FPS监测

FPS的监控,参照YYKit中的YYFPSLabel,主要是通过CADisplayLink实现.借助link的时间差,来计算一次刷新刷新所需的时间,然后通过 刷新次数 / 时间差 得到刷新频次,并判断是否其范围,通过显示不同的文字颜色来表示卡顿严重程度.主要代码如下:

@implementation YYFPSLabel {
    CADisplayLink *_link;
    NSUInteger _count;
    NSTimeInterval _lastTime;
}
- (instancetype)initWithFrame:(CGRect)frame {
    _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    return self;
}
- (void)dealloc {
    [_link invalidate];
}
- (void)tick:(CADisplayLink *)link {
    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }
    _count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
    if (delta < 1) return;
    _lastTime = link.timestamp;
    float fps = _count / delta;
    _count = 0;
    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
}
复制代码

如果只是简单的监测,使用FPS足够了,其原理仍然是监测主线程的卡顿导致runloop调用tick的次数减少,来判断是否掉帧。

runloop监测

监控主线程RunLoop原理,因为卡顿的是事务,而事务是交由主线程RunLoop处理的.

实现思路:检测主线程每次执行消息循环的时间,当这个时间大于规定的阈值时,就记为发生了一次卡顿。这个也是微信卡顿三方matrix的原理

可以直接写一个进行RunLoop监控

@interface VTBlockMonitor (){
    CFRunLoopActivity activity;
}
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;
@end

@implementation VTBlockMonitor
+ (instancetype)sharedInstance {
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}
- (void)start{
    [self registerObserver];
    [self startMonitor];
}

static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    VTBlockMonitor *monitor = (__bridge VTBlockMonitor *)info;
    monitor->activity = activity;
    // 发送信号
    dispatch_semaphore_t semaphore = monitor->_semaphore;
    dispatch_semaphore_signal(semaphore);
}

// 注册监听事物
- (void)registerObserver{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    //NSIntegerMax : 优先级最小
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, NSIntegerMax, &CallBack, &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

- (void)startMonitor{
    // 创建信号
    _semaphore = dispatch_semaphore_create(0);
    // 在子线程监控时长
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES) {
            // 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
            long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
            if (st != 0)  {
                if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting) {
                    if (++self->_timeoutCount < 2){
                        NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
                        continue;
                    }
                    // 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
                    NSLog(@"检测到超过两次连续卡顿 - %lu",(unsigned long)self->_timeoutCount);
                }
            }
            self->_timeoutCount = 0;
        }
    });
}
@end
复制代码

也可以直接使用三方库

  •   Swift的卡顿检测第三方ANREye,其主要思路是:创建子线程进行循环监测,每次检测时设置标记置为true,然后派发任务到主线程,标记置为false,接着子线程睡眠超过阈值时,判断标记是否为false,如果没有,说明主线程发生了卡顿

  •   OC可以使用 微信matrix滴滴DoraemonKit

界面优化

CPU层面的优化
  1. 尽量用轻量级的对象代替重量级的对象,可以对性能有所优化,例如 不需要相应触摸事件的控件,用CALayer代替UIView

  2. 尽量减少对UIViewCALayer的属性修改

    •   CALayer内部并没有属性,当调用属性方法时,其内部是通过运行时resolveInstanceMethod为对象临时添加一个方法,并将对应属性值保存在内部的一个Dictionary中,同时还会通知delegate、创建动画等,非常耗时

    •   UIView相关的显示属性,例如frame、bounds、transform等,实际上都是从CALayer映射来的,对其进行调整时,消耗的资源比一般属性要大

  3. 当有大量对象释放时,也是非常耗时的,尽量挪到后台线程去释放

  4. 尽量提前计算视图布局,即预排版,例如cell的行高

  5. Autolayout在简单页面情况下们可以很好的提升开发效率,但是对于复杂视图而言,会产生严重的性能问题,随着视图数量的增长,Autolayout带来的CPU消耗是呈指数上升的,所以尽量使用代码布局.

  6. 文本处理的优化:当一个界面有大量文本时,其行高的计算、绘制也是非常耗时的

    • 如果对文本没有特殊要求,可以使用UILabel内部的实现方式,且需要放到子线程中进行,避免阻塞主线程
      •   计算文本宽高:[NSAttributedString boundingRectWithSize:options:context:]
      •   文本绘制:[NSAttributedString drawWithRect:options:context:]
    • 自定义文本控件,利用TextKit 或最底层的 CoreText 对文本异步绘制.并且CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整和绘制都需要计算一次),CoreText直接使用了CoreGraphics占用内存小,效率高
  7. 图片处理(解码 + 绘制)

    • 当使用UIImageCGImageSource 的方法创建图片时,图片的数据不会立即解码,而是在设置时解码(即图片设置到UIImageView/CALayer.contents中,然后在CALayer提交至GPU渲染前,CGImage中的数据才进行解码)。这一步是无可避免的,且是发生在主线程中的。想要绕开这个机制,常见的做法是在子线程中先将图片绘制到CGBitmapContext,然后从Bitmap 直接创建图片,例如SDWebImage三方框架中对图片编解码的处理。这就是Image的预解码
    • 当使用CG开头的方法绘制图像到画布中,然后从画布中创建图片时,可以将图像的绘制子线程中进行
  8. 图片优化

    • 尽量使用PNG图片,不使用JPGE图片
    • 通过子线程预解码,主线程渲染,即通过Bitmap创建图片,在子线程赋值image
    • 优化图片大小,尽量避免动态缩放
    • 尽量将多张图合为一张进行显示
  9. 尽量避免使用透明view,因为使用透明view,会导致在GPU中计算像素时,会将透明view下层图层的像素也计算进来,即颜色混合处理,可能触发离屏渲染,默认的圆角,阴影,裁切等操作也会触发离屏渲染

  10. 按需加载,例如在TableView中滑动时不加载图片,使用默认占位图,而是在滑动停止时加载,[self performSelector: withObject: afterDelay: inModes:NSDefaultRunLoopMode];

  11. 少使用addView 给cell动态添加view

GPU层面优化

相对于CPU而言,GPU主要是接收CPU提交的纹理+顶点,经过一系列transform,最终混合并渲染,输出到屏幕上.

  1. 尽量减少在短时间内大量图片的显示,尽可能将多张图片合为一张显示,主要是因为当有大量图片进行显示时,无论是CPU的计算还是GPU的渲染,都是非常耗时的,很可能出现掉帧的情况
  2. 尽量避免图片的尺寸超过4096×4096,因为当图片超过这个尺寸时,会先由CPU进行预处理,然后再提交给GPU处理,导致额外CPU资源消耗
  3. 尽量减少视图数量和层次,主要是因为视图过多且重叠时,GPU会将其混合,混合的过程也是非常耗时的
  4. 尽量避免离屏渲染,layer.background,圆角,阴影,裁切等
  5. 异步渲染,例如可以将cell中的所有控件. 视图合成一张图片进行显示,重写drawLayer: inContext:和displayLayer:进行异步绘制.可以参考Graver三方框架

猜你喜欢

转载自juejin.im/post/7111567690489135140