iOS底层原理(37) - 界面优化

885 阅读4分钟

卡顿的原理

图形处理的框架结构

image.png

图形显示的原理

image.png

image.png

image.png

image.png

卡顿的检测

屏幕刷新率

FPS是图像领域中的定义,是指画面每秒传输帧数,通俗来讲就是指动画或视频的画面数。

1、CADisplayLink监测

利用CADisplayLink监测垂直同步信号 image.png


//定义 CADisplayLink
_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    
    
//vsync信号事件回调
- (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;
}

2、Runloop监测

原理如下:

给主线程的 runloop 添加 observer 事件,监听 runloop 的所有活动,利用信号量的机制,根据限定时间内,runloop的活动状态是否发生改变,来判断是否发生了卡顿。

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};



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);
}

- (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(@"检测到超过两次连续卡顿");
                }
            }
            self->_timeoutCount = 0;
        }
    });
}

3、微信卡顿监测工具 Matrix

github上的地址:github.com/Tencent/mat…

该工具的原理也是利用对 runloop 的监听。但它不仅可以监测到卡顿,而且可以定位到卡顿的原因,报告堆栈信息等。

4、滴滴卡顿监测工具 DoraemonKit

github上的地址:github.com/didi/Doraem…

原理:开启一个子线程不断地Ping主线程,通过信号量控制来Ping主线程,判断主线程是否卡顿

滑动优化方案

CPU:

把以下操作放到子线程中:
1、对象创建、调整、销毁
2、预排版(布局计算、文本计算、缓存高度等)
3、预渲染(文本等异步绘制,图片解码等)

GPU:
纹理渲染,视图混合

预排版

布局计算、文本计算

以 TableView 为例,每次滑动屏幕的时候,cell内部控件Frame的计算,以及cell高度的计算等非常地频繁,如果布局非常复杂的话,那么大量的计算以及渲染会造成界面的卡顿。

缓存高度

将cell的内部控件的 Frame、Cell的高度提前计算出来,保存到 Model 中,这样就减少了非常多的计算量,卡顿的现象就会得到缓解。这个提前计算布局的方式就叫做预排版。

按需加载

监听tableview的滚动过程,当停止滚动的时候,开始加载,并且只加载可视范围内的cell。

预渲染

预渲染并不是GPU渲染,而是将渲染的部分前期准备工作交给CPU进行处理。

异步绘制

1、[self.layer.delegate displayLayer:]

2、代理负责生成对应的 bitmp

3、设置该 bitmap 作为该 layer.contents 属性的值。

预解码

图片加载流程 image.png

由于decode是在主线程进行的,而decode的过程是非常耗时的,因此预先进行decode可以加快图片的加载过程。

异步渲染

UIView为CALayer提供内容,以及负责处理触摸等事件,参与响应链;

CALayer负责显示内容contents,图形渲染。

异步渲染就是将渲染过程放到子线程当中去做。

drawrect 调用堆栈

image.png

位图的生成

1、layout 构建视图

2、displayer 绘制

3、prepare (core animation4、commit to render server

异步渲染框架 Graver

Graver 是一款高效的 UI 渲染框架,它以更低的资源消耗来构建十分流畅的 UI 界面。Graver 独创性的采用了基于绘制的视觉元素分解方式来构建界面,得益于此,该框架能让 UI 渲染过程变得更加简单、灵活。

避免离屏渲染

在OpenGL中,GPU有2种渲染方式

  • On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作;
  • Off-Screen Rendering: 离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作

离屏渲染消耗性能的原因

  • 需要创建新的缓冲区
  • 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕切换到离屏,等离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕。

GPU离屏渲染何时会触发?

  • 1、圆角, 同时设置 layer.masksToBounds = YES、layer.cornerRadius > 0
  • 2、遮罩 ,layer.mask
  • 3、阴影, layer.shadowXXX,如果设置了 layer.shadowPath 就不会产生离屏渲染
  • 4、光栅化 ,layer.shouldRasterize = YES
检测离屏渲染和图层混合

打开模拟器,菜单选中debug,显示如下:

image.png

  • 这里面的 Color Off-screen Rendered 选中,那么产生离屏渲染的图层会高亮变成黄色。

  • 这里的 Color Blended Layers 选中,那么红色区域表示图层发生了混合

光栅化

光栅化是将几何数据经过一系列变换后最终转换为像素,从而呈现在显示设备上的过程,光栅化的本质是坐标变换、几何离散化。

我们使用UITableview和UICollectionView时,经常会遇到各个 Cell 的样式是一样的,这时候我们可以使用这个属性提高性能:

cell.layer.shouldRaasterize=YES;
cell.layer.rasterizationScale=[[UIScreen mainScreen] scale];