核心编程探索(8)-界面优化

一、视图加载

优质文章推荐

  • iOS 界面渲染流程分析
Advanced Graphics and Animations for iOS Apps.jpeg

我们可以看到,在Application这一层中主要是CPU在操作,而到了Render Server这一层,CoreAnimation会将具体操作转换成发送给GPU的draw calls(以前是call OpenGL ES,现在慢慢转到了Metal),显然CPU和GPU双方同处于一个流水线中,协作完成整个渲染工作。

整个渲染分为三个步骤

  • 1、CPU 阶段
  1. 布局(Frame)
  2. 显示(Core Graphics)
  3. 准备(QuartzCore/Core Animation)
  4. 通过 IPC 提交(打包好的图层树以及动画属性)
  • 2、OpenGL ES(Metal) 阶段
  1. 生成(Generate)
  2. 绑定(Bind)
  3. 缓存数据(Buffer Data)
  4. 启用(Enable)
  5. 设置指针(Set Pointers)
  6. 绘图(Draw)
  7. 清除(Delete)
  • 3、GPU 阶段
  1. 接收提交的纹理(Texture)和顶点描述(三角形)
  2. 应用变换(transform)
  3. 合并渲染(离屏渲染等)

二、 帧率与丢帧


三、 卡顿原理


四、 卡顿检测&优化

  • 质量监控-卡顿检测

1. 检测方法

  • 主线程相关
  1. fps :我们可以根据CADisplayLink刷新的时间间隔,以及刷新的频率来判断是否有卡顿
// fps 监控
- (void)startFpsMonitoring {
    CADisplayLink * fpsDisplay = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayFps:)];
    [fpsDisplay addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)displayFps: (CADisplayLink *)fpsDisplay {
    static NSInteger count; // 没刷新一次 计数加一
    static double    lastUpadateTime; // 最后的统计时间
    
    count++;
    CFAbsoluteTime threshold = CFAbsoluteTimeGetCurrent() - lastUpadateTime; // 一个周期用了多少时间
    
    if (threshold >= 1.0) { //
        NSLog(@"一个周期内刷新了%f下,每%f毫秒刷新一下",(count / threshold),fpsDisplay.duration); // 一个周期内 刷新了多少下
        lastUpadateTime = CFAbsoluteTimeGetCurrent();
        count = 0;
    }
}
2020-07-22 10:40:04.997406+0800 PHTemplate[18396:2917339] [Storyboard] Unknown class ViewController in Interface Builder file.
2020-07-22 10:40:05.034140+0800 PHTemplate[18396:2917339] 375.000000
2020-07-22 10:40:05.042258+0800 PHTemplate[18396:2917339] 一个周期内刷新了0.000000下,每0.016667毫秒刷新一下
2020-07-22 10:40:05.053648+0800 PHTemplate[18396:2917339] Unbalanced calls to begin/end appearance transitions for .
2020-07-22 10:40:06.053587+0800 PHTemplate[18396:2917339] 一个周期内刷新了60.328246下,每0.016667毫秒刷新一下
2020-07-22 10:40:07.054198+0800 PHTemplate[18396:2917339] 一个周期内刷新了59.977028下,每0.016667毫秒刷新一下
2020-07-22 10:40:08.070924+0800 PHTemplate[18396:2917339] 一个周期内刷新了60.011309下,每0.016667毫秒刷新一下
  1. ping : 是一种常用的网络测试工具,用来测试数据包是否能到达ip地址。在卡顿发生的时候,主线程会出现短时间内无响应这一表现,基于ping的思路从子线程尝试通信主线程来获取主线程的卡顿延时:
// 子线程与主线程通讯
- (void)ping {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (![NSThread currentThread].isCancelled) {
            dispatch_sync(dispatch_get_main_queue(), ^{
                static  CFAbsoluteTime pingTime;
                NSLog(@"%f",CFAbsoluteTimeGetCurrent()-pingTime);
                pingTime = CFAbsoluteTimeGetCurrent();
            });
        };
    });
}
  1. runloop
- (void)startRunLoopMonitoring {
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        if (CFAbsoluteTimeGetCurrent() - _lastActivityTime >= _threshold) {
            ......
            _lastActivityTime = CFAbsoluteTimeGetCurrent();
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
  • 非主线程
  1. stack backtrace

代码质量不够好的方法可能会在一段时间内持续占用CPU的资源,换句话说在一段时间内,调用栈总是停留在执行某个地址指令的状态。由于函数调用会发生入栈行为,如果比对两次调用栈的符号信息,前者是后者的符号子集时,可以认为出现了卡顿恶鬼:

@interface StackBacktrace : NSThread
......
@end

@implementation StackBacktrace

- (void)main {
    [self backtraceStack];
}

- (void)backtraceStack {
    while (!self.cancelled) {
        @autoreleasepool {
            NSSet *curSymbols = [NSSet setWithArray: [StackBacktrace backtraceMainThread]];
            if ([_saveSymbols isSubsetOfSet: curSymbols]) {
                ......
            }
            _saveSymbols = curSymbols;
            [NSThread sleepForTimeInterval: _interval];
        }
    }
}

@end
  1. msgSend observe

OC方法的调用最终转换成msgSend的调用执行,通过在函数前后插入自定义的函数调用,维护一个函数栈结构可以获取每一个OC方法的调用耗时,以此进行性能分析与优化:

#define save() \
__asm volatile ( \
    "stp x8, x9, [sp, #-16]!\n" \
    "stp x6, x7, [sp, #-16]!\n" \
    "stp x4, x5, [sp, #-16]!\n" \
    "stp x2, x3, [sp, #-16]!\n" \
    "stp x0, x1, [sp, #-16]!\n");

#define resume() \
__asm volatile ( \
    "ldp x0, x1, [sp], #16\n" \
    "ldp x2, x3, [sp], #16\n" \
    "ldp x4, x5, [sp], #16\n" \
    "ldp x6, x7, [sp], #16\n" \
    "ldp x8, x9, [sp], #16\n" );
    
#define call(b, value) \
    __asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
    __asm volatile ("mov x12, %0\n" :: "r"(value)); \
    __asm volatile ("ldp x8, x9, [sp], #16\n"); \
    __asm volatile (#b " x12\n");


__attribute__((__naked__)) static void hook_Objc_msgSend() {

    save()
    __asm volatile ("mov x2, lr\n");
    __asm volatile ("mov x3, x4\n");
    
    call(blr, &push_msgSend)
    resume()
    call(blr, orig_objc_msgSend)
    
    save()
    call(blr, &pop_msgSend)
    
    __asm volatile ("mov lr, x0\n");
    resume()
    __asm volatile ("ret\n");
}

2. 卡顿的解决方法

  • iOS开发-常用解决页面卡顿技巧

五、ASDK分析(AsyncDisplayKit):已经换库Texture

  • ASDK源码剖析

六、离屏渲染

离屏渲染的主要原因就是,多了一些渲染完成后又要修改的情况
关于iOS离屏渲染的深入研究

你可能感兴趣的:(核心编程探索(8)-界面优化)