界面的优化

屏幕显示图像的原理

请添加图片描述

  • CPU 计算好显示内容提交到 GPU,(计算视图的创建/视图布局)
  • GPU 渲染完成后将渲染结果放入帧缓冲区,
  • 随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,
  • 经过可能的数模转换传递给显示器显示。

帧缓冲区只有一个,这时帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,显示系统通常会引入两个缓冲区,即双缓冲机制

所以双缓存机制

GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。

双缓冲虽然能解决效率问题,但会引入一个新的问题。当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象
请添加图片描述

为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync)

垂直同步

当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。

卡顿原理

卡顿产生的原因:掉帧

卡顿检测

FPS (Frames Per Second) 是图像领域中的定义,表示每秒渲染帧数,通常用于衡量画面的流畅度,每秒帧数越多,则表示画面越流畅,60fps 最佳,一般我们的APP的FPS 只要保持在 50-60之间,用户体验都是比较流畅的。

监测FPS也有好几种,这里只说最常用的方案,我最早是在YYFPSLabel

YYKit 的YYFPSLabel检测

YYFPSLabel 的实现原理

原理是向主线程的RunLoop的添加一个commonModes的CADisplayLink,每次屏幕刷新的时候都要执行CADisplayLink的方法,所以可以统计1s内屏幕刷新的次数,也就是FPS

FPSLabel 实现思路:

  • CADisplayLink 默认每秒 60次;
  • 将 CADisplayLink add 到 mainRunLoop 中;
  • 使用 CADisplayLink 的 timestamp 属性,在 CADisplayLink 每次 tick 时,记录上一次的
    timestamp;
  • 用 _count 记录 CADisplayLink tick 的执行次数;
  • 计算此次 tick 时, CADisplayLink 的当前 timestamp 和 _lastTimeStamp 的差值;
  • 如果差值大于1,fps = _count / delta,计算得出 FPS 数;

RunLoop检测

其实FPS中CADisplayLink的使用也是基于RunLoop,都依赖main RunLoop。我们来看看

先来看看简版的RunLoop的代码

// 1.进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled)
 
// 2.RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 3.RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 4.RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle)
// 5.执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
 
// 6.RunLoop 的线程即将进入休眠(sleep)。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
 
// 7.调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort)
 
// 进入休眠
 
// 8.RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting
 
// 9.如果一个 Timer 到时间了,触发这个Timer的回调
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
 
// 10.如果有dispatch到main_queue的block,执行bloc
 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
 
 // 11.如果一个 Source1 (基于port) 发出事件了,处理这个事件
__CFRunLoopDoSource1(runloop, currentMode, source1, msg);
 
// 12.RunLoop 即将退出
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
 


我们可以看到RunLoop调用方法主要集中在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之间,有人可能会问kCFRunLoopAfterWaiting之后也有一些方法调用,为什么不监测呢,我的理解,大部分导致卡顿的的方法是在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之间,比如source0主要是处理App内部事件,App自己负责管理(出发),如UIEvent(Touch事件等,GS发起到RunLoop运行再到事件回调到UI)、CFSocketRef。开辟一个子线程,然后实时计算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值,来断定主线程的卡顿情况。

代码

// 卡顿检测类
#import "YZMonitorRunloop.h"
#import <execinfo.h>
/**
 原理:利用观察Runloop各种状态变化的持续时间来检测计算是否发生卡顿
 一次有效卡顿采用了“N次卡顿超过阈值T”的判定策略,即一个时间段内卡顿的次数累计大于N时才触发采集和上报:举例,卡顿阈值T=500ms、卡顿次数N=1,可以判定为单次耗时较长的一次有效卡顿;而卡顿阈值T=50ms、卡顿次数N=5,可以判定为频次较快的一次有效卡顿
 */

// minimum
static const NSInteger MXRMonitorRunloopMinOneStandstillMillisecond = 20;
static const NSInteger MXRMonitorRunloopMinStandstillCount = 1;

// default
// 超过多少毫秒为一次卡顿
static const NSInteger MXRMonitorRunloopOneStandstillMillisecond = 50;
// 多少次卡顿纪录为一次有效卡顿
static const NSInteger MXRMonitorRunloopStandstillCount = 1;

@interface YZMonitorRunloop(){
    CFRunLoopObserverRef _observer;  // 观察者
    dispatch_semaphore_t _semaphore; // 信号量
    CFRunLoopActivity _activity;     // 状态
}

@property (nonatomic, assign) BOOL isCancel; //f是否取消检测
@property (nonatomic, assign) NSInteger countTime; // 耗时次数
@property (nonatomic, strong) NSMutableArray *backtrace;

@end


@implementation YZMonitorRunloop
+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static YZMonitorRunloop *cls;
    dispatch_once(&onceToken, ^{
        cls = [[[self class] alloc] init];
        cls.limitMillisecond = MXRMonitorRunloopOneStandstillMillisecond;
        cls.standstillCount  = MXRMonitorRunloopStandstillCount;
    });
    return cls;
}

//重写set方法,用KVO监听
- (void)setLimitMillisecond:(int)limitMillisecond{
    [self willChangeValueForKey:@"limitMillisecond"];
    _limitMillisecond = limitMillisecond >= MXRMonitorRunloopMinOneStandstillMillisecond ? limitMillisecond : MXRMonitorRunloopMinOneStandstillMillisecond;
    [self didChangeValueForKey:@"limitMillisecond"];
}
- (void)setStandstillCount:(int)standstillCount
{
    [self willChangeValueForKey:@"standstillCount"];
    _standstillCount = standstillCount >= MXRMonitorRunloopMinStandstillCount ? standstillCount : MXRMonitorRunloopMinStandstillCount;
    [self didChangeValueForKey:@"standstillCount"];
}
//开始检测
- (void)startMonitor
{
    self.isCancel = NO;
    [self registerObserver];
}
//结束检测
- (void) endMonitor
{
    self.isCancel = YES;
    if(!_observer) return;
    //    将observer从当前thread的runloop中移除
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    //    释放 observer
    CFRelease(_observer);
    _observer = NULL;
}
//为了保证子线程的同步监测,刚开始创建一个信号量是0的dispatch_semaphore。当监测到主线程的RunLoop,触发回调
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    if (activity != kCFRunLoopBeforeWaiting) {
        //        NSLog(@"-%s-- activity == %lu",__func__,activity);
    }
    
    YZMonitorRunloop *instance = [YZMonitorRunloop sharedInstance];
    // 记录状态值
    instance->_activity = activity;
    // 发送信号
    dispatch_semaphore_t semaphore = instance->_semaphore;
    //发送信号,使信号量+1
    dispatch_semaphore_signal(semaphore);
}

-(void)registerObserver{
    //    1. 设置Runloop observer的运行环境
    CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
    // 2. 创建Runloop observer对象
    
    //    第一个参数:用于分配observer对象的内存
    //    第二个参数:用以设置observer所要关注的事件
    //    第三个参数:用于标识该observer是在第一次进入runloop时执行还是每次进入runloop处理时均执行
    //    第四个参数:用于设置该observer的优先级
    //    第五个参数:用于设置该observer的回调函数
    //    第六个参数:用于设置该observer的运行环境
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                        kCFRunLoopAllActivities,
                                        YES,
                                        0,
                                        &runLoopObserverCallBack,
                                        &context);
    // 3. 将新建的observer加入到当前thread的runloop
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    // 创建信号  dispatchSemaphore的知识参考:https://www.jianshu.com/p/24ffa819379c
    _semaphore = dispatch_semaphore_create(0); Dispatch Semaphore保证同步
    
    __weak __typeof(self) weakSelf = self;
    
    //    dispatch_queue_t queue = dispatch_queue_create("kadun", NULL);
    
    // 在子线程监控时长
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //      dispatch_async(queue, ^{
        __strong __typeof(weakSelf) strongSelf = weakSelf;
        if (!strongSelf) {
            return;
        }
        while (YES) {
            if (strongSelf.isCancel) {
                return;
            }
            // N次卡顿超过阈值T记录为一次卡顿
            // 等待信号量:如果信号量是0,则阻塞当前线程;如果信号量大于0,则此函数会把信号量-1,继续执行线程。此处超时时间设为limitMillisecond 毫秒。
            // 返回值:如果线程是唤醒的,则返回非0,否则返回0
            long semaphoreWait = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, strongSelf.limitMillisecond * NSEC_PER_MSEC));
            
            if (semaphoreWait != 0) {
                
                // 如果 RunLoop 的线程,进入睡眠前方法的执行时间过长而导致无法进入睡眠(kCFRunLoopBeforeSources),或者线程唤醒后接收消息时间过长(kCFRunLoopAfterWaiting)而无法进入下一步的话,就可以认为是线程受阻。
                //两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够监测到是否卡顿
                if (self->_activity == kCFRunLoopBeforeSources || self->_activity == kCFRunLoopAfterWaiting) {
                    
                    if (++strongSelf.countTime < strongSelf.standstillCount){
                        NSLog(@"%ld",strongSelf.countTime);
                        continue;
                    }
                    
                    NSLog(@"@“检测到两次卡顿”");
                   
                }
            }
            strongSelf.countTime = 0;
        }
    });
}



- (void)logStack
{
    NSLog(@"-%s--",__func__);
    void* callstack[128];
    int frames = backtrace(callstack, 128);
    char **strs = backtrace_symbols(callstack, frames);
    int i;
    _backtrace = [NSMutableArray arrayWithCapacity:frames];
    for ( i = 0 ; i < frames ; i++ ){
        [_backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);
}

- (void)printLogTrace{
    NSLog(@"==========检测到卡顿之后调用堆栈==========\n %@ \n",_backtrace);
}


@end

// 卡顿检测类
/**
 原理:利用观察Runloop各种状态变化的持续时间来检测计算是否发生卡顿
 一次有效卡顿采用了“N次卡顿超过阈值T”的判定策略,即一个时间段内卡顿的次数累计大于N时才触发采集和上报:举例,卡顿阈值T=500ms、卡顿次数N=1,可以判定为单次耗时较长的一次有效卡顿;而卡顿阈值T=50ms、卡顿次数N=5,可以判定为频次较快的一次有效卡顿
 */

#import <Foundation/Foundation.h>


@interface YZMonitorRunloop : NSObject
+ (instancetype)sharedInstance;
/// 超过多少毫秒为一次卡顿 400毫秒
@property (nonatomic, assign) int limitMillisecond;

/// 多少次卡顿纪录为一次有效,默认为5次
@property (nonatomic, assign) int standstillCount;

/// 发生一次有效的卡顿回调函数
@property (nonatomic, copy) void (^callbackWhenStandStill)(void);

/**
 开始监听卡顿
 */
- (void)startMonitor;

/**
 结束监听卡顿
 */
- (void)endMonitor;
@end

实战优化项目

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值