31、iOS底层分析 - 界面优化

大纲:

 1、界面渲染流程
 2、UIView & CALayer
 3、卡顿检测
 4、案例实战
 5、异步渲染


 1、界面渲染流程

 UIKit

日常交互组件基本上都是来自于 UIKit,我们可以通过设置 UIKit Layout ,来完成日程界面的绘画工作。但是并不具备在屏幕上成像的能力,在这个过程中,将对用户的操作事件进行响应,从而将当前的事件通过图层的图层树进行传递。


 Core Animation 

 Core Animation 是核心动画

OpenGL ES/Metal 
 这个绘制过程依赖于 OpenGL ES/Metal  GPU 渲染

Core Graphics 
 和 Core Graphics  CPU渲染

Graphics Hardware
 最底层得是通过 Graphics Hardware 这样的硬件进行图形的渲染
 
 

2、UIView & CALayer


 1、Application(UIKit)与 Core Animation进行关联。主要做的事情就是 commit Transaction 用来做事务的提交。
 2、Core Animation 把事务提交给了 Render Server(OpenGL ES 或者是 Core Graphics)在这个过程当中将我们处理过后的数据与GPU通讯,也就是将我们处理过后的数据传递给 GPUGPU 再调用设备相关的图形进行 Display 渲染操作流程。

Core Animation:注册观察者
操作UI,UIview/CALyaer待处理,全局的容器当中,RunLoop

Core Animation
 隐式动画 :设置属性,动画
 显示动画:CABasicAnimation 时长 关键动画


 Commit Transaction 做了什么
 1、Layout,构建视图
 2、Dispatch,绘制视图
 3、Prepare,额外的 Core Animation 工作,比如图片加载解码。
 4、Commit。打包图层并将它们发送到 Render Server
 接下来执行递归,图层树。如果图层树比较复杂那么Commit提交的开销操作时非常大的。
 
1、Layout 构建视图
 例如:
 这句代码到底是如何被系统知道,并绘制的?这里有个隐式动画的提交(CATransaction)
 view.backgroundColor = [UIColor whiteColor];

通过Instruments 查看一下渲染流程
 
 
 通过 [CATransaction commit] 隐式的将图层改变(相当于标记了图层的改变)提交到一个全局的容器当中(这个容器中保存的是修改了属性的图层,并不是遍历修改全部,而是增量修改)
 先执行 Layout 构建视图(例如:frame、bounds等等)

//伪代码。通过断点调试得来的流程
CA::Transaction::commit(){
        CA::Context::commit_transaction(CA::Transaction*)(){
            CA::Layer::layout_if_needed(CA::Transaction*)(){
                CollectLayersData *layerTree;
                CA::Layer::collect_layers_(CA::Layer::CollectLayersData*);
                for in layerTree{
                    [layer layoutSublayers];
                }
            }
        }
    }

View的三个方法区别
 setNeedsLayout:  标记,标记当前这个图层是需要改变的。会被提交到全局的容器当中,方便 CATransaction (事务) 来处理这个改变
 layoutIfNeeded:相关触发事件时,layoutSubviews方法会被调用,但这种调用机制是延迟的,而layoutIfNeeded可以保证立即调用
 layoutSubviews:当添加到view上或者是子控件发生变化、旋转、滚动等的时候会被调用


 2、Display 绘制视图
 函数之前 Display
 系统layer的绘制 分为两种情况
 1、backingStore:drawReact: 有后台存储区的参与绘制的
 2、直接通过 displayLayer:layer:contents = image(当前的)
 

 
当前绘制提供一个上下文的操作,也就是说我们当前绘制的这些像素点都放到 backingStore 里面
在这个过程中会调用 -[CALayer drawInContext]; 这个函数。但是这个函数并不会直接绘制,而是询问了一个由UIview实现的代理方法-[UIView(CALayerDelegate) drawLayer:inContext:]。同时,这个代理又会调用-[UIView drawRect:]。这里开启整个图层的绘制,绘制的内容放到 backingStore 当中。backingStore 是用来上传GPU去使用的。提交事务,也就是提交绘制到全局。

  1. -[CALayer drawInContext];
  2. -[UIView(CALayerDelegate) drawLayer:inContext:]
  3. -[UIView drawRect:]
  4. 绘制好的内容放到backingStore,提交给GPU处理

下面是伪代码。


 CA::Layer::layout_and_display_if_needed();
 代码分支,有两种情况


 1、是否需要布局的操作
 需要布局的操作
 调用 layoutSublayers
 再调用 代理方法
 是用来进行布局的计算,
 当前布局完成了之后,调用下面的 Display(显示)
 
 2、不需要布局
 [CALayer disPlay]
 先调用  [CALayer drawInContext];
 然后会询问代理方法有没有实现
 [UIView drawLayer:inContext]
 代理方法调用了
 [UIView drawRect]  来进行整个图层的绘制
 现在模拟一下这几个函数的执行流程。
 先看一下 CALayer,里面有这么几个代理方法,就是用来与UIView进行交互的

    @protocol CALayerDelegate <NSObject>
    @optional
     //如果定义,则由-display的默认实现调用方法,在这种情况下,它应该实现整个显示(通常通过设置' contents'属性)。* /
    - (void)displayLayer:(CALayer *)layer;
     //如果定义了,则由-drawInContext的默认实现调用:
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
     //如果定义,则由-display方法的默认实现调用。
    - (void)layerWillDraw:(CALayer *)layer
    API_AVAILABLE(macos(10.12), ios(10.0), watchos(3.0), tvos(10.0));
     //在布局之前,通过默认的- layoutsubblayers实现调用
    - (void)layoutSublayersOfLayer:(CALayer *)layer;
     //默认实现调用
    - (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event;
    
    @end

    //重新加载该层的内容。调用-drawInContext:方法
    - (void)display;
 

具体的代码

demo 路径是 002-界面优化 -> 1-界面渲染流程解析
 小结:
 layer 绘制流程
 [CALayer display]       调用栈的发起函数
 CALyer drawInContext    发起函数调用
 [UIView drawLayer:(CALayer*)layer inContext:(CGContexRef)ctx],  会询问代理方法,进行绘制的操作
 [UIView displayLayer:], layer.contens(位图)    更新layer的图层,最终绘制得到的是一张位图。也就是 layer.contens(其实一张位图)
 关闭上下文   整个流程完毕之后,关闭上下文,结束绘制
 


 - (void)drawRect:(CGRect)rect
 是在后台创建了一个存储区,提供了一个 context (上下文),供我们去绘制。
 如果去实现了 - (void)displayLayer:(CALayer *)layer (一般情况下不由我们自己去做)那么 drawRect: 将不再执行。
 
 setNeedsLayout: 标记,标记当前这个图层是需要改变的。会被提交到全局的容器当中,方便 CATransaction (事务) 来处理这个改变
 
 苹果的设计系统中每个图层都是可以做动画的。
 例如:
_view.backgroundColor = [UIColor redColor];
 就会调用到
 [CALayer actionForKey:]
 这个方法有几个返回值:
 1、空对象
 UIView在响应代理时默认会返回一个NSNull对象,表示属性修改后,不实现任何的动作,根据修改后的属性值直接更新视图。
 2、nil:
手动创建并添加到视图上的 CALayer 或其子类在属性修改时,没有获取到具体的修改行为。此时被修改的属性会被CATransaction(事务)记录,最终在下一个 runloop 的回调中生成动画来响应本次属性修改。由于这个过程非开发者主动完成的,因此这种动画被称为隐式动画。
 例如:上面修改view的背景颜色
 
 3、CAAction 的子类:
如果返回的是CAAction (活动)对象,会直接开始动画来响应图层属性的修改。一般返回的对象多为CABasicAnimation(动画)类型,对象中包装了动画时长、动画初始/结束状态、动画时间曲线等关键信息。当CAAction 对象被返回时,会立刻执行动作来响应本次属性修改。
例如:

    [UIView animateWithDuration:1.0 animations:^{
        self.ljlView.frame = CGRectMake(20, 200, 200, 200);
    }];

Core Animation
 [CALayer actionForKey:] 返回值不同。
 隐式动画:设置属性,非开发者主动完成动画。
 显式动画:CABasicAnimation 存储了时长、关键动画
 

实际验证一下。
 在 viewDidLoad 添加观察者,目的打印当前主线程的Runloop
 //before waiting 等待
 在进入休眠之前会去处理下面的变更背景色
 _ljlView.backgroundColor = [UIColor redColor];

[self registeObserver];

#pragma mark -- Observer
static void __runloop_callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    NSString *str;
    switch (activity) {
        case kCFRunLoopEntry:
            str = @"Min Entry";
            break;
        case kCFRunLoopBeforeTimers:
            str = @"Min Before Timersr";
            break;
        case kCFRunLoopBeforeSources:
            str = @"Min Before Sources";
            break;
        case kCFRunLoopBeforeWaiting:
            str = @"Min Before Waiting";
            break;
        case kCFRunLoopAfterWaiting:
            str = @"Min After Waiting";
            break;
        case kCFRunLoopExit:
            str = @"Min Exit";
            break;
        case kCFRunLoopAllActivities:
            str = @"Min AllActivities";
            break;
        default:
            break;
    }
    NSLog(@"current activity:%@",str);
}

static void __runloop_before_waiting_callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    NSString *str;
    switch (activity) {
        case kCFRunLoopEntry:
            str = @"Max Entry";
            break;
        case kCFRunLoopBeforeTimers:
            str = @"Max Before Timersr";
            break;
        case kCFRunLoopBeforeSources:
            str = @"Max Before Sources";
            break;
        case kCFRunLoopBeforeWaiting:
            str = @"Max Before Waiting";
            break;
        case kCFRunLoopAfterWaiting:
            str = @"Max After Waiting";
            break;
        case kCFRunLoopExit:
            str = @"Max Exit";
            break;
        case kCFRunLoopAllActivities:
            str = @"Max AllActivities";
            break;
        default:
            break;
    }
    NSLog(@"current activity:%@",str);
}

//Observer 观察者
-(void)registeObserver
{
    CFRunLoopObserverContext ctx = { 0, (__bridge void *)self, NULL, NULL };
    CFRunLoopObserverRef allActivitiesObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, YES, NSIntegerMin, &__runloop_callback, &ctx);
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), allActivitiesObserver, kCFRunLoopCommonModes);
    
    CFRunLoopObserverRef beforeWaitingObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, YES, NSIntegerMax, &__runloop_before_waiting_callback, &ctx);
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), beforeWaitingObserver, kCFRunLoopCommonModes);
}

通过修改view的frame等属性可以发现会有不同的打印。验证

Core Animation:注册观察者
 在RunLoop即将休眠的时候进行回调,看一下是否有 frame 或属性等的改变。
 如果没有只是回调不做当前事务的提交。如果有改变,会继续走 [CATransaction commit]; 进行提交,进行轮回的操作。


小结:
操作 UI 的时候,UIView/CALayer 是被标记为待处理的,会被提交到全局容器当中。Core Animation 在 RunLoop 当中注册了 Observer(观察者),去监听 beforeWating(休眠), exit(退出)这两个阶段,在这个过程中会进行回调。目的是遍历待处理的图层树,来进行界面更新。
 
 小结:
 Application 中布局 UIKit 视图控件间接的关联 Core Animation 图层
 Core Animation 图层相关的数据提交到 iOS Render Server,即 OpenGL ES & Core Graphics Render Server 将与 GPU 通信把数据经过处理理之后传递给 GPU
 GPU 调⽤用 iOS 当前设备渲染相关的图形设备 Display(显示)
 
 Layout :构建视图。其实就是frame 等操作,是一个遍历操作。会调用[UIView layerSubview],[CALayer layoutSubLayers];去构建视图
 Display:绘制视图。display 一、如果是 -drawReact()(画图)会创建一下上下文进行绘制。 二、displayLayer:(位图的绘制)
 prepare: 额外的 Core Animation 工作,比如解码。
 commit: 打包图层并将它们发送到 Render Server(也就是我们的GPU)
 这个过程当中会循环调用 CA::Layer::commit_if_needed: 去循环提交
 
 所以如果图层过于复杂,在图层树构建的时候是比较耗时的,在commit打包提交的时候也是比较耗时的。


 3、卡顿检测

 卡顿检测原理
 
 CPU 计算好显示的内容(视图的创建、文本的绘制、图片的解码等),计算好之后交给
 GPU 完成渲染之后存放到Frame Buffer 帧缓冲区中。
 Video Controller 视频控制器,根据我们的垂直信号,经过数模转换之后再交给显示器
 Monitor 显示器
 
 最初的帧缓冲区只有一个,需要每次渲染完才能读,每次都要等所以效率非常低。
 iPhone 使用双缓冲机制(前帧、后帧),用垂直信号VSync 同步缓冲操作。

 卡顿的原理
 当垂直信号VSync 到的时候才会进行新的一帧的渲染 和 缓冲区的更新操作。
 如果垂直信号到来的时候GPU还没有或无法完成新一帧的渲染,那么看到的就还是上一帧的画面,这个时候就是掉帧、卡顿了。
 掉帧分为两种情况,CPU任务繁重或者是GPU的任务繁重,在这16.67ms(两个垂直信号VSync 间隔)之中无法完成整个新一帧的缓存,就会出现掉帧。
 掉帧掉的是第二帧的内容
 
 卡顿的检测
 所有任务都是以RunLoop为单位,从任务的开始(beforeSource)- 任务的结束(berforWating)。时长过长,认为可能卡顿了。

FPS

扩展:
FPS :

Frames Per Second 的简称缩写,意思是每秒传输帧数,可以理解为我们常说的“刷新率”(单位为Hz);FPS是测量用于保存、显示动态视频的信息数量。每秒钟帧数愈多,所显示的画面就会愈流畅,fps值越低就越卡顿,所以这个值在一定程度上可以衡量应用在图像绘制渲染处理时的性能。


 CADisplayLink
 是一个用于显示的定时器, 它可以让用户程序的显示与屏幕的硬件刷新保持同步,iOS系统中正常的屏幕刷新率为60Hz(60次每秒)。
 CADisplayLink可以以屏幕刷新的频率调用指定selector,也就是说每次屏幕刷新的时候就调用selector,那么只要在selector方法里面统计每秒这个方法执行的次数,通过次数/时间就可以得出当前屏幕的刷新率了。


 FPS(只是一个反馈的指标),基于CADisplayLink 屏幕刷新频率保持一致。反馈的刷新率大致可以知道是否卡顿
 2、以 YYKit 为例 demo
 总刷新次数/间隔时间 = 每秒刷新的次数。满帧是60FPS,正常50-60我们肉眼是分辨不出来的。
 如何定位卡顿的地方,获取堆栈快照。这里不做分析
 

2、用信号量监听检测是否卡顿

//LJLBlockMonitor.h
#import <Foundation/Foundation.h>
@interface LJLBlockMonitor : NSObject

+(instancetype)sharedInstance;
-(void)start;

@end


//  LJLBlockMonitor.m
#import "LJLBlockMonitor.h"
@interface LJLBlockMonitor (){
    CFRunLoopActivity activity;
}
@property(nonatomic, strong) dispatch_semaphore_t semaphore;//信号量
@property(nonatomic, assign) NSUInteger timeoutCount;//超时统计
@end

@implementation LJLBlockMonitor
+ (instancetype)sharedInstance
{
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (instance == nil) {
            instance = [[self alloc] init];
        }
    });
    return instance;
}

- (void)start{
    [self registerObserver];
    [self startMonitor];
}

//监听的所有任务执行完成 发送信号
static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    LJLBlockMonitor *monitor = (__bridge LJLBlockMonitor *)info;
    monitor->activity = activity;
    // 发送信号
    dispatch_semaphore_t semaphore = monitor->_semaphore;
    dispatch_semaphore_signal(semaphore);
}

//创建一个有RunLoop的观察者,观察所有操作,优先级最小。也就是说在其他所有任务都处理完才会去执行CallBack
- (void)registerObserver{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    //NSIntegerMax : 优先级最小
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            NSIntegerMax,
                                                            &CallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

//在任务执行开始的时候创建信号,在子线程监听时长。设置超时wait 为1秒。如果1秒内没有收到信号(也就是1秒内任务没有执行完)可能有卡顿,当连续两次出现超时的时候,打印卡顿。
- (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;//第一次卡顿直接跳出while循环,连续2次卡顿的话不进入if 认为卡顿
                    }
                    NSLog(@"检测到卡顿");
                }
            }
            self->_timeoutCount = 0;
        }
    });
}
@end
//  ViewController.m
- (void)viewDidLoad {
    [[LJLBlockMonitor sharedInstance] start];
}
//点击屏幕的时候,执行循环。时间超过1秒未执行完就会超时,连续两次超时的话就会打印卡顿
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //    [NSThread sleepForTimeInterval:3.0];
    for(int i = 0 ; i < 100000; i++){
        UIView *v = [[UIView alloc] init];
        v.frame = CGRectMake(0, 100, 100, 100);
        v.backgroundColor = [UIColor redColor];
        [self.view addSubview:v];
    }
}


 3、微信的解决方案
 微信开源的一个卡顿检测的一个库(demos 的 002-界面优化 -> 2-卡顿检查),有卡顿检测、CPU检测等
 1、startBlockMonitor
 通过RunLoop添加一个观察者,来检查当前任务是否超过卡顿的阈值
 2、start
 前面是初始化了一系列变量
 添加RunLoop观察者
 [self addRunLoopObserver];
 创建子线程来监测
 [self addMonitorThread];
 
 添加了两个观察者
 1、beginObserver 任务开启的观察者。 优先级最大,保证在所有任务回调之前进行执行
 2、endObserver 任务结束的观察。 优先级最小,在所有任务完之后在进行回调
 
 1、g_bRun = YES 标识当前RunLoop正在运行
 g_tvRun C语言的时间戳 记录时间
 g_bRun==NO 的话说明当前RunLoop 进入休眠或退出
 当任务开始的时候,也就是RunLoop被唤醒(通过Timer、sources、AfterWating都是开始任务的状态标志,这个时候判断一下如果g_bRun==NO 说明刚被唤醒也就是刚要开始执行任务,这个时候记录一下当期那时间戳,也就是任务开始的时间戳),这个时候去记录当前的时间戳 放到 g_tvRun 结构体中
 因为我们不知道是从什么进入开始的,所以几种唤醒RunLoop的形式都做一下判断存储。
 
 创建子线程来监测
 子线程1秒检查一次。过任务执行开始的时间戳,与当前时间戳进行比对,如果差值大于2秒阈值就认为卡顿。然后记录。
 
  微信开源的卡顿检测主要代码如下:

- (void)startBlockMonitor
    {
        assert([NSThread mainThread]);
        if (_blockMonitor) {
            [_blockMonitor start];
        }
    }
    - (void)start
    {
       ......
//        初始化一些变量
        
        [self addRunLoopObserver];
        [self addMonitorThread];
    }
    
    - (void)addRunLoopObserver
    {
        NSRunLoop *curRunLoop = [NSRunLoop currentRunLoop];
        
        // the first observer
        CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};
        CFRunLoopObserverRef beginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &myRunLoopBeginCallback, &context);
        CFRetain(beginObserver);
        m_runLoopBeginObserver = beginObserver;
        
        // the last observer
        CFRunLoopObserverRef endObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &myRunLoopEndCallback, &context);
        CFRetain(endObserver);
        m_runLoopEndObserver = endObserver;
        
        CFRunLoopRef runloop = [curRunLoop getCFRunLoop];
        CFRunLoopAddObserver(runloop, beginObserver, kCFRunLoopCommonModes);
        CFRunLoopAddObserver(runloop, endObserver, kCFRunLoopCommonModes);
        
       ......
    }
    
    void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
    {
        g_runLoopActivity = activity;
        g_runLoopMode = eRunloopDefaultMode;
        switch (activity) {
            case kCFRunLoopEntry:
                g_bRun = YES;
                break;
                
            case kCFRunLoopBeforeTimers:
                if (g_bRun == NO) {
                    gettimeofday(&g_tvRun, NULL);
                }
                g_bRun = YES;
                break;
                
            case kCFRunLoopBeforeSources:
                if (g_bRun == NO) {
                    gettimeofday(&g_tvRun, NULL);
                }
                g_bRun = YES;
                break;
                
            case kCFRunLoopAfterWaiting:
                if (g_bRun == NO) {
                    gettimeofday(&g_tvRun, NULL);
                }
                g_bRun = YES;
                break;
                
            case kCFRunLoopAllActivities:
                break;
                
            default:
                break;
        }
    }
    
    void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
    {
        g_runLoopActivity = activity;
        g_runLoopMode = eRunloopDefaultMode;
        switch (activity) {
            //执行后休眠,换起RunLoop 记录时间戳
            case kCFRunLoopBeforeWaiting:
                gettimeofday(&g_tvRun, NULL);
                g_bRun = NO;
                break;
 
            //任务结束
            case kCFRunLoopExit:
                g_bRun = NO;
                break;
                
            case kCFRunLoopAllActivities:
                break;
                
            default:
                break;
        }
    }

    - (void)addMonitorThread
    {
        m_bStop = NO;
        m_monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadProc) object:nil];
        [m_monitorThread start];
    }
    
    - (void)threadProc
    {
       ......
        
        while (YES) {
            @autoreleasepool {
                if (g_bMonitor) {
                    //检测是否卡顿
                    EDumpType dumpType = [self check];
                    if (m_bStop) {
                        break;
                    }
                }
            }
        }
    }
    
    - (EDumpType)check
    {
        // 1. runloop time out
        BOOL tmp_g_bRun = g_bRun;
        struct timeval tmp_g_tvRun = g_tvRun; //我们启动的时间戳
        
        struct timeval tvCur;//当前时间戳
        gettimeofday(&tvCur, NULL);
        //计算启动到当前的时间差值
        unsigned long long diff = [WCBlockMonitorMgr diffTime:&tmp_g_tvRun endTime:&tvCur];
        
        ......
        m_blockDiffTime = 0;
        //diff > g_RunLoopTimeOut(2秒) 大于阈值
        //子线程的检查周期是1秒,1秒检查一次,如果说主线程的执行周期大于2秒就认为出现了卡顿
        //标识处于记录RunLoop状态    &tvCur RunLoop启动时间点小于当前时间点   diff > g_RunLoopTimeOut 启动时间戳与当前时间戳差值大于2秒阈值
        if (tmp_g_bRun && tmp_g_tvRun.tv_sec && tmp_g_tvRun.tv_usec && __timercmp(&tmp_g_tvRun, &tvCur, <) && diff > g_RunLoopTimeOut) {
            m_blockDiffTime = diff;
#if TARGET_OS_OSX
            MatrixInfo(@"check run loop time out %u bRun %d runloopActivity %lu block diff time %llu",
                       g_RunLoopTimeOut, g_bRun, g_runLoopActivity, diff);
#endif
            ......
        }
    }


 4、案例实战
 例如:将网络请求放到子线程、用子线程进行计算布局,然后到主线程刷新显示
 标记控件 Debug -> Colo Off-screen Rendered

不做过多分析,详见 demo 目录 002-界面优化 -> 3-案例优化

 

Graver 渲染流程

图片加载流程
 从本地拿到图片数据的二进制 Data Buffer -> decode(解码)预解码在子线程解码 ->Image Buffer (像素缓存区) -> frame Buffer
 SDWebImage、YYImage。通过异步子线程,画出来(跟预渲染差不多,画成bitMap)然后去更新layer
 


 按需加载
 通过 RunLoop
 思路:在快速滑动过程中加载可视范围内的前三行和后三行。
 缺点是滑动有空白,不建议使用所以也就不过多说了,比较麻烦只是一种以前的解决方案。需要自行百度吧。
 

5、异步绘制

 异步加载
目的是减少图层,为了减轻CPU压力(图层多的话Layout、commit循环递归非常耗时)。通过core Graphics 将图层合成一张位图(但是如果在其他方法都用过之后没有更好的优化方案的时候,使用,因为将图层画下来也是比较耗时的操作)。
 异步渲染可以使用几种
 Texture 太重了,坑太多了,脱离了UIKit,自己做的绘制操作
 Core Graphics  是将图层合成一张位图来实现的
 
 以 Graver (Core Graphics)为例
 demo 002-界面优化 -> 4-异步渲染
 
 GcoreText & CoreGraphics
 异步绘制的入口

-(void)displayLayer:(CALayer *)layer

异步绘制的关键,创建位图并复制 layer.contents = bitmapImage, bitmapImage(通过Core Grphicsy放在异步线程中,用来创建位图)
 需要后台绘制,就创建一个异步队列,再异步执行异步队列绘制任务。
 如果不是可以放到主队列。
 做的话需要计算文本坐标,frome计算等,比较繁琐。
 用位图的形式,减少图层的层级。
 
 

总结
  

1、界面渲染流程

  1.  UIKit       界面绘制,不具备显示在屏幕上的能力。通过图层树传递与Core Animation 关联
  2.  Core Animation        核心动画,把事务提交给GPU、CPU处理
  3.  OpenGL ES/Metal     GPU渲染 通过display                   Core Graphics  CPU渲染
  4.  Graphics Hardware   这样的硬件实现图形渲染

 
 2、Commit Transaction(提交事务 kit 和 Core Animation 关联)

  1.   Layout :构建视图。其实就是frame 等操作,是一个遍历操作。会调用[UIView layerSubview],[CALayer layoutSubLayers];去构建视图
  2.   Display:绘制视图。display 一、如果是 -drawReact()(画图)会创建一下上下文进行绘制。 二、displayLayer:(位图的绘制)
  3.   prepare: 额外的 Core Animation 工作,比如解码。
  4.   commit: 打包图层并将它们发送到 Render Server(也就是我们的GPU)

这个过程当中会循环调用 CA::Layer::commit_if_needed: 去循环提交
 1、4.如果图层复杂会多次递归,非常耗时
 Layout的过程 收集图层,然后根据图层树去递归执行 [layer layoutSublayers];
 
3、 Layout 构建视图
 [CATransaction commit] 隐式的将图层改变提交给全局容器
 去遍历执行[layer layoutsublayers];修改
 
 4、Display 绘制视图

  1.  -[CALayer drawInContext];
  2.   -[UIView(CALayerDelegate) drawLayer:inContext:] 代理方法
  3. -[UIView drawRect:] 代理调用这个来实现绘制
  4.  绘制好的内容放到backingStore,提交给GPU处理

Display系统layer的绘制 分为两种情况
  1、backingStore:drawReact: 有后台存储区的参与绘制的
  2、直接通过 displayLayer:layer:contents = image(当前的)
 
 CA::Layer::layout_and_display_if_needed();
 分两种情况
 一种需要布局
 一种不需要布局
 

5、动画都会调用
  [CALayer actionForKey:]
  这个方法有几个返回值:
  1、空对象:
 直接更新视图。
  2、nil:
 非开发者主动完成,修改属性等隐式动画。
  例如:上面修改view的背景颜色
  3、CAAction 的子类:
 动画
 
 6、UIView 与 CALayer 的关系
 1、通过 drawRect 方法绘制操作,是有额外的存储区参与,将绘制的内容提交给GPU 进行处理
 2、通过displayLayer 相当于layer.contents 本质上更新的是一个位图
 
 7、卡顿检测
 iPhone使用双缓冲机制空间换取时间。分为前帧、后帧。同步问题用VSync 垂直信号解决
 CPU + GPU 总时长 超过了VSync 的间隔就是掉帧(从任务的开始(beforeSource) - 任务的结束(berforewating))
 总刷新次数 / 间隔时间 = 每秒刷新的次数。
 
 微信的实现方案,
 1、创建两个RunLoop观察者,一个启动的优先级最大,一个结束的优先级最小。记录启动RunLoop的时间戳。
 2、创建一个异步子线程,每秒去检查一次,启动RunLoop时的时间戳与当前时间戳的比价,如果大于2秒就认为卡顿
 
 8、优化:
 预排版:

  •  将网络请求和排版放在子线程异步队列中,通过子线程异步排版好数据在去主线程刷新加载

 预解码:

  •  将图片的解码放到异步子线程去操作

 
 9、异步绘制
 通过Core Graphics 将图层合成一张位图来实现。
 可以用Graver实现
 入口是display,绘制的关键是 displayLayer().但是文本、frame等都需要计算,图层较多的话较为繁琐。如果图层较少的话不适合使用,因为将图层合成一张位图也是需要时间消耗的,所以图层较少不建议使用

发布了104 篇原创文章 · 获赞 13 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/shengdaVolleyball/article/details/105148247
今日推荐