iOS保持界面流畅的技巧(性能优化)

2018/8/25 posted in  iOS性能优化

参考iOS 性能优化总结
参考微信读书 iOS 性能优化总结
参考iOS实时卡顿监控
参考移动端IM实践:iOS版微信界面卡顿监测方案
参考iOS保持界面流畅的技巧

本文是对上面几篇文章的总结。并与我自己的知识体系相融合,以达到一个自相融洽的整体。

卡顿原理

VSync信号到来时,系统图形服务会通过CADisplayLink等机制通知App,APP主线程开始在CPU中计算显示内容,比如视图的创建,布局计算,图片解码,文本绘制等。随后CPU会将计算好的内容提交到GPU,由GPU进行变换,合成,渲染。随后GPU会把渲染结果提交到帧缓冲区区,等待下一次VSync信号到来时显示到屏幕上。
由于垂直同步机制,若果在一个VSync时间内,CPU或者GPU没有完成内容的提交,则这个没有完成的提交不会显示到屏幕,在其提交后会到缓冲区,等待下一次机会在显示。那么这个下一次有可能是正常时间提交修改,则会将缓存区的内容覆盖。导致被覆盖的永远没机会显示了。就是掉帧了。
那么与runloop有什么关系呢?
Runloop的时间概念比1/60更小,也就是Runloop处理事物的时间远远比1/60要小。绝大部分时间是睡眠的。所以它们两个本身是没有关系的。绝大部分时候是主线程早就处理完毕所有显示数据,并提交到了渲染系统,渲染系统也完成了合成,也就是提交到了缓存区。只需要等待时钟的到来,将这个缓冲区的内容显示到屏幕上。

FPS的真正含义是,1s的时间真正有多少帧显示到了屏幕上。

卡顿监控思路

  1. 主线程卡顿监控
    通过子线程监测主线程的runloop,判断两个状态区域之间的耗时是否达到一定阈值。这两个状态就是kCFRunLoopBeforeSources与kCFRunLoopAfterWaiting

  2. 借助FPS监控
    CADisplayLink 可以将其看做一个定时器。定时器都与机器的硬件有关系。而这个定时器是有屏幕“垂直时钟”驱动。也就是与垂直时钟同步,1/60时间跳动一次。因为CADisplayLink的回调也要在线程里执行,将其加入到主线程的Runloop里。runloop的TimerSource就会触发runloop的“叫醒”,执行该执行的内容。若果不将CADisplayLink加入,当然就不会定固定时间“叫醒”。

CADislayLink的timestampe的两次差为1s之内的tick次数,即使FPS。

线程卡顿监控方案一的实现

iOS实时卡顿检测
这一方案的思路,就是从引起卡顿的本质来优化。引起卡顿,在kCFRunLoopBeforeSources通知后执行主队列里的代码,执行block的代码等。或则在kCFRunLoopAfterWaiting被唤起后,也会执行队列里的代码,block代码。

#import <CrashReporter/CrashReporter.h>

@interface PerformanceMonitor ()
{
    int timeoutCount;
    CFRunLoopObserverRef observer;
    
    @public
    dispatch_semaphore_t semaphore;
    CFRunLoopActivity activity;
}
@end

@implementation PerformanceMonitor

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

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    PerformanceMonitor *moniotr = (__bridge PerformanceMonitor*)info;
    
    moniotr->activity = activity;
    
    dispatch_semaphore_t semaphore = moniotr->semaphore;
    dispatch_semaphore_signal(semaphore);//没当状态发生变化的时候+1;这是在主线程执行
}

- (void)stop
{
    if (!observer)
        return;
    
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);
    observer = NULL;
}

- (void)start
{
    if (observer)
        return;
    
    // 信号
    semaphore = dispatch_semaphore_create(0);
    
    // 注册RunLoop状态观察
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                       kCFRunLoopAllActivities,
                                       YES,
                                       0,
                                       &runLoopObserverCallBack,
                                       &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    
    //在子线程监控时长
    //在子线程不断的监控信号量,如果在50 ms里还没有超时,也就是在50ms状态没有发生该边的话。就说明在两个状态的时间间隔里有执行时长超过50ms的
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (st != 0)
            {
                if (!observer)
                {
                    timeoutCount = 0;
                    semaphore = 0;
                    activity = 0;
                    return;
                }
                
                //若果超时的是kCFRunLoopBeforeSources 或者kCFRunLoopAfterWaiting 就更能说明问题
                //如果5此都是这样,说明有问题。几下主线程的调用堆栈。
                if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
                {
                    if (++timeoutCount < 5)
                        continue;
                    
                    PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
                                                                                       symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
                    PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
                    
                    NSData *data = [crashReporter generateLiveReport];
                    PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
                    NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
                                                                              withTextFormat:PLCrashReportTextFormatiOS];
                    
                    NSLog(@"------------\n%@\n------------", report);
                }
            }
            timeoutCount = 0;
        }
    });
}

@end