OC底层原理探索之界面优化

1,266

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

什么是卡顿

卡顿也就是所谓的掉帧、花屏。cpu对渲染的东西计算,GPU负责渲染,渲染的东西到了帧缓存区(framebuffer),然后通过VideoController交给Monitor来显示。 image.png

当CPU计算卡住的时候,VideoControlle就一直空等,会造成浪费的情况。此时系统推出了前帧和后帧也就是双缓存机制。但是此时又会发生这么一种情况,在换帧的时间间隔里,如果有一个帧没有被渲染出来,就会丢弃,也就是掉帧。这个来回在双缓存之间切换的时间也就是垂直同步信号VSync,在这个时间间隔之内能够计算完就能够正常显示,计算不完就丢掉。 image.png

**CADisplayLink**卡顿检测

YYKitYYFPSLable利用CADisplayLink来检测,因为这个是绑定在Vsync上的计时器。一般都是60

_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
- (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;
    // ...
}

runloop卡顿检测

  1. 首先在主线程的runloop上加上一个Observer和对应的callBack回调,该callBack回调**发送信号**dispatch_semaphore_signal+1
- (void)registerObserver{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    //NSIntegerMax : 优先级最小
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            NSIntegerMax,
                                                            &CallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;
    monitor->activity = activity;
    // 发送信号
    dispatch_semaphore_t semaphore = monitor->_semaphore;
    dispatch_semaphore_signal(semaphore);
}
  1. 在子线程开启一个do-while常驻线程, 在该子线程中创建一个信号量,设置dispatch_semaphore_wait的超时时间是1s。如果主线程不卡顿的话,子线程wait之后主线程马上signal,此时返回值应该是0,如果不为0的话此时就表明有卡顿存在。
- (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(@"检测到超过两次连续卡顿");
                }
            }
            self->_timeoutCount = 0;
        }
    });
}

微信卡顿检测工具

Matrix里面的方法addMonitorThread卡顿检测跟上面的runloop大致相同 ​

怎么解决卡顿

  1. 预排版:在网络数据请求回来之后,根据数据提前计算排版
  2. 预解码 & 预渲染(加载图片的时候)

UIImageView的结构组成它其实是一个模型, 图片的加载流程 image.png

image.png

我们可以参考下SDWebImage这个第三方在task成功之后做了什么操作,找到这个结果回调的函数

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error

SDWebImage在获取到结果之后开启了一个子线程异步解码编码队列中的图像SDImageLoaderDecodeImageData,那么为什么这么做呢。当我们调用这个[[UIImage] imageWithData:]方法时,苹果默认是在主线程来解码这些数据。而在实际应用中大部分卡顿的时间都在加载图片上,所以SDWebImage在子线程重写了解码的方法。

image.png 图形编解码插件音视频FFmpeg

  1. 按需加载:如果Cell不断的滚动此时可以只处理可视范围内的数据
  2. 异步渲染:将要绘制的操作放到异步绘制,主线程只负责刷新展示

UIView & UILayer

Commint Transaction做了什么

  1. layout: 构建视图frame , 遍历操作[UIViewlayerSubview][CALayer layoutSubLayers]
  2. display: 绘制 display--drawRect(),displayLyaer:(位图的绘制)
  3. Prepare: 额外的CoreAnimation工作,比如解码
  4. Commi: 打包图层并将它们发送到RenderServer