FPS

FPS 是测量用于保存、显示动态视频的信息数量,每秒钟帧数愈多,所显示的动作就会愈流畅,一般应用只要保持 FPS 在 50-60,应用就会给用户流畅的感觉,反之,用户则会感觉到卡顿

FPS(Frames Per Second)。
Frame rate (expressed in frames per second or FPS) is the frequency (rate) at which consecutive images called frames are displayed in an animated display. The term applies equally to film and video cameras, computer graphics, and motion capture systems. Frame rate may also be called the frame frequency, and be expressed in hertz.

网络上流传的最多的关于测量 FPS 的方法,GitHub 上有关计算 FPS 的仓库基本都是通过以下方式实现的:

@implementation YYFPSLabel {
    CADisplayLink *_link;
    NSUInteger _count;
    NSTimeInterval _lastTime;    
}

- (id)init {
    self = [super init];
    if( self ){        
    _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        
    }
    return self;
}

- (void)dealloc {
    [_link invalidate];
}

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

上面是 YYText 中 Demo 的 YYFPSLabel,主要是基于CADisplayLink以屏幕刷新频率同步绘图的特性,尝试根据这点去实现一个可以观察屏幕当前帧数的指示器。YYWeakProxy的使用是为了避免循环引用。

值得注意的是基于CADisplayLink实现的 FPS 在生产场景中只有指导意义,不能代表真实的 FPS,因为基于CADisplayLink实现的 FPS 无法完全检测出当前 Core Animation 的性能情况,它只能检测出当前 RunLoop 的帧率。

如何监控卡顿
两种方案
  • FPS 监控:这是最容易想到的一种方案,如果帧率越高意味着界面越流畅,上文也给出了计算 FPS 的实现方式,通过一段连续的 FPS 计算丢帧率来衡量当前页面绘制的质量。
  • 主线程卡顿监控:这是业内常用的一种检测卡顿的方法,通过开辟一个子线程来监控主线程的 RunLoop,当两个状态区域之间的耗时大于阈值时,就记为发生一次卡顿。美团的移动端性能监控方案 Hertz 采用的就是这种方式

FPS 的刷新频率非常快,并且容易发生抖动,因此直接通过比较 FPS 来侦测卡顿是比较困难的;此外,主线程卡顿监控也会发生抖动,所以微信读书团队给出一种综合方案,结合主线程监控、FPS 监控,以及 CPU 使用率等指标,作为判断卡顿的标准。Bugly 的卡顿检测也是基于这套标准。

当监控到应用出现卡顿,如何定位造成卡顿的原因呢?试想如果我们能够在发生卡顿的时候,保存应用的上下文,即卡顿发生时程序的堆栈调用和运行日志,那么就能凭借这些信息更加高效地定位到造成卡顿问题的来源。下图是 Hertz 监控卡顿的流程图
在这里插入图片描述
主线程卡顿监控的实现思路:开辟一个子线程,然后实时计算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值,来断定主线程的卡顿情况,可以将这个过程想象成操场上跑圈的运动员,我们会每隔一段时间间隔去判断是否跑了一圈,如果发现在指定时间间隔没有跑完一圈,则认为在消息处理的过程中耗时太多,视为主线程卡顿。

问题: 自己理解的难点是 RunLoop 监控卡顿为什么要用kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting状态判定

说下我对主线程卡顿的理解,就是主线程在Runloop的某个阶段进行长时间的耗时操作

Runloop 的顺序
	 1 进入runloop
	 2 通知Timer
	 3 通知Source
	 4 处理Source
	 5 如果有Source1 调转到 1
	 6 通知 BeforeWaiting
	 7 wait
	 8 通知afterWaiting
	 9 处理Timer
	 10 处理dispatch到main_queue的block
	 11 处理Source1
	 12 进入 2
	 13 退出
	 

理清楚Runloop的运行机制,就很容易明白处理事件主要有两个时间段 kCFRunLoopBeforeSources 发送之后和 kCFRunLoopAfterWaiting 发送之后

dispatch_semaphore_t 是一个信号量机制,信号量到达、或者 超时会继续向下进行,否则等待,如果超时则返回的结果必定不为0,信号量到达结果为0

利用这个特性我们判断卡顿出现的条件为 在信号量发送 kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting后进行了大量的操作,在一段时间内没有再发送信号量,导致超时。也就是说主线程通知状态长时间的停留在这两个状态上了。转换为代码就是判断有没有超时,超时了,判断当前停留的状态是不是这两个状态,如果是,就判定为卡顿。

这样就能解释通为什么要用这两个信号量判断卡顿。这么一个简单的问题,思路转不过来就绕进去了,现在回看感觉这个很简单,也是耗了一天时间。

//头文件
+ (instancetype)shareInstance;

- (void)beginMonitor;
- (void)stopMonitor;
// m文件变量定义
@interface YMSubThreadMonitor()
{
	int timeoutCount;
    CFRunLoopObserverRef ymObserver;
    @public     //@public 外部能访问到
    CFRunLoopActivity currentRunloopActivity;
    dispatch_semaphore_t semaphore;    
}

@end

@implementation YMSubThreadMonitor
+ (instancetype)shareInstance{
    static dispatch_once_t onceToken;
    static YMSubThreadMonitor *monitor;
    dispatch_once(&onceToken, ^{
        monitor = [YMSubThreadMonitor new];
    });
    return monitor;
}
// 主要代码
- (void)beginMonitor{
    
    if (ymObserver) {
        return;
    }
    
    //创建观察者
    /**
     typedef struct {
     CFIndex    version;
     void * info;
     const void *(*retain)(const void *info);
     void   (*release)(const void *info);
     CFStringRef    (*copyDescription)(const void *info);
     } CFRunLoopObserverContext;
     */
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL,NULL};
    
    /**
     CFRunLoopObserverRef CFRunLoopObserverCreate(
     CFAllocatorRef allocator,
     CFOptionFlags activities,
     Boolean repeats,
     CFIndex order,
     CFRunLoopObserverCallBack callout,
     CFRunLoopObserverContext *context
     );
     */
    ymObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &mObservercallBack, &context);
    
    //向主线程添加 观察者
    CFRunLoopRef mainLoop = CFRunLoopGetMain();
    CFRunLoopAddObserver(mainLoop, ymObserver, kCFRunLoopCommonModes);
    
    //创建子线程开始监控
    dispatch_queue_t monitorQueue = dispatch_queue_create("com.ym.monitorQueue", DISPATCH_QUEUE_CONCURRENT);
    
    //创建同步信号量
    semaphore = dispatch_semaphore_create(0);
    
    dispatch_async(monitorQueue, ^{
        
        //开一个持续的  loop
        while (YES) {
            //超时时间设置
            dispatch_time_t outTimer = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);
            
            //信号量到达、或者 超时会继续向下进行,否则等待、
            long result = dispatch_semaphore_wait(semaphore, outTimer);
            
            if (result != 0) {
                
                //超时,判断最后停留的信号量是哪一个,是否处理为卡顿现象。
                if (!ymObserver) {
                    NSLog(@"--NO ymObserver---");
                    semaphore = 0;
                    currentRunloopActivity = 0;
                    timeoutCount = 0;
                    return ;
                }
                
                //判断当前 监听到的 信号(也就是说上一个信号量超过2秒没有更新,故卡顿)
                /**
                 RunLoop 顺序
                 1、进入
                 
                 2、通知Timer
                 3、通知Source
                 4、处理Source
                 5、如果有 Source1 调转到 11
                 6、通知 BeforWaiting
                 7、wait
                 8、通知afterWaiting
                 9、处理timer
                 10、处理 dispatch 到 main_queue 的 block
                 11、处理 Source1、
                 12、进入 2
                 
                 13、退出
                 
                 由上可知,主要处理任务阶段为 AfterWaiting 之后、通知Source之后
                 如果 发送 AfterWaiting 后在限定时间内没有发送其他信号量,
                 可以认为这中间存在耗时操作,判定为卡顿。
                 同理 通知Source之后 超时也可以判定为卡顿。
                 
                 */
                if (currentRunloopActivity == kCFRunLoopBeforeSources || currentRunloopActivity == kCFRunLoopAfterWaiting) {
                    //出现卡顿、进一步处理
                    NSLog(@"--卡顿啦----From 卡顿监控线程");
                    // log current stack info
                    if (++timeoutCount < 5) {
                   		 continue;
                    }else {
                    	NSLog(@"卡顿了");
                    }
                }
                timeoutCount = 0;
            }
            
            NSLog(@"--系统运行良好--From 卡顿监控线程");
        }
    });
    
}

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

#pragma mark -Private Method

/**
 * 观察者回调函数
 */
static void  mObservercallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    //每一次监测到Runloop发送通知的时候,都会调用此函数
    //在此过程修改当前的 RunloopActivity 状态,发送同步信号。
    YMSubThreadMonitor *monitor = (__bridge YMSubThreadMonitor *)info;
    
    monitor->currentRunloopActivity = activity;
    dispatch_semaphore_t tempSemaphore = monitor->semaphore;
    dispatch_semaphore_signal(tempSemaphore);
}
RunLoop下的卡顿监控

界面出现卡顿,一般是下面几种原因:
主线程做大量计算
主线程大量的I/O操作
大量的UI绘制
主线程进行网络请求以及数据处理
离屏渲染
监控界面卡顿,主要是监控主线程做了哪些耗时的操作,之前的文章中已经分析过,iOS中线程的事件处理依靠的是RunLoop,正常FPS值为60,如果单次RunLoop运行循环的事件超过16ms,就会使得FPS值低于60,如果耗时更多,就会有明显的卡顿。
正常RunLoop运行循环一次的流程是这样的:

SetupThisRunLoopRunTimeOutTimer();
do {
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
  
        __CFRunLoopDoBlocks();
        __CFRunLoopDoSource0(); // 处理source0事件,UIEvent事件,比如触屏点击

        CheckIfExitMessagesInMainDispatchQueue(); // 检查是否有分配到主队列中的任务

        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        var wakeUpPort = SleepAndWaitForWakingUpPorts(); // 开始休眠,等待ma ch_msg事件
        
        // mach_msg_trap
        // ZZz.....   sleep
        // Received mach_msg,  wake up
        
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting); // 被事件唤醒
        // Handle msgs
        if (wakeUpPort == timePort) { // 被唤醒的事件是timer
              __CFRunLoopDoTimers(); 
        } else if (wakePort == mainDispatchQueuePort) { // 主队列有调度任务
              __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
        } else { // source1事件,UI刷新,动画显示
              __CFRunLoopDoSource1();
        }
        __CFRunLoopDoBlocks();
} while (!stop && !timeout)

从这个运行循环中可以看出,RunLoop休眠的事件是无法衡量的,处理事件的部分主要是在kCFRunLoopBeforeSources之后到kCFRunLoopBeforeWaiting之前和kCFRunLoopAfterWaiting 之后和运行循环结束之前这两个部分

监控这两个部分的耗时,使用CFRunLoopObserverRef来监控RunLoop的状态:

代码中的 NSEC_PER_SEC,代表的是触发卡顿的时间阈值,单位是秒。可以看到,我们把这个阈值设置成了 3 秒。那么,这个 3 秒的阈值是从何而来呢?这样设置合理吗?其实,触发卡顿的时间阈值,我们可以根据 WatchDog 机制来设置。WatchDog 在不同状态下设置的不同时间,如下所示:
启动(Launch):20s;
恢复(Resume):10s;
挂起(Suspend):10s;
退出(Quit):6s;
后台(Background):3min(在 iOS 7 之前,每次申请 10min; 之后改为每次申请 3min,可连续申请,最多申请到 10min)。
通过 WatchDog 设置的时间,我认为可以把启动的阈值设置为 10 秒,其他状态则都默认设置为 3 秒。总的原则就是,要小于 WatchDog 的限制时间。当然了,这个阈值也不用小得太多,原则就是要优先解决用户感知最明显的体验问题。

如何获取卡顿的方法堆栈信息?

直接调用系统函数方法的主要思路是:用 signal 进行错误信息的获取。具体代码如下:



static int s_fatal_signals[] = {
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGSEGV,
    SIGTRAP,
    SIGTERM,
    SIGKILL,
};

static int s_fatal_signal_num = sizeof(s_fatal_signals) / sizeof(s_fatal_signals[0]);

void UncaughtExceptionHandler(NSException *exception) {
    NSArray *exceptionArray = [exception callStackSymbols]; //得到当前调用栈信息
    NSString *exceptionReason = [exception reason];       //非常重要,就是崩溃的原因
    NSString *exceptionName = [exception name];           //异常类型
}

void SignalHandler(int code)
{
    NSLog(@"signal handler = %d",code);
}

void InitCrashReport()
{
    //系统错误信号捕获
    for (int i = 0; i < s_fatal_signal_num; ++i) {
        signal(s_fatal_signals[i], SignalHandler);
    }
    
    //oc未捕获异常的捕获
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}

int main(int argc, char * argv[]) {
    @autoreleasepool {
        InitCrashReport();
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值