本文首发于我的个人博客
前言
关于性能优化,我之前写过iOS性能优化,经过优化之后,我们的APP,冷启动,从2.7秒优化到了0.6秒。
关RunLoop,写过RunLoop详解之源码分析,以及详解RunLoop与多线程
,那么使用RunLoop如何来监控性能卡顿呢。
通过iOS性能优化 我们知道,简单来说App卡顿,就是FPS达不到60帧率,丢帧现象,就会卡顿。但是很多时候,我们只知道丢帧了。具体为什么丢帧,却不是很清楚,那么我们要怎么监控呢,首先我们要明白,要找出卡顿,就是要找出主线程做了什么,而线程消息,是依赖RunLoop的,所以我们可以使用RunLoop来监控。
RunLoop是用来监听输入源,进行调度处理的。如果RunLoop的线程进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者线程唤醒后接收消息时间过长而无法进入下一步,就可以认为是线程受阻了。如果这个线程是主线程的话,表现出来的就是出现了卡顿。
RunLoop和信号量
我们可以使用CFRunLoopObserverRef来监控NSRunLoop的状态,通过它可以实时获得这些状态值的变化。
runloop
关于runloop,可以参照 RunLoop详解之源码分析 这篇文章详细了解。这里简单总结一下:
- runloop的状态
1 | /* Run Loop Observer Activities */ |
CFRunLoopObserverRef 的使用流程
设置Runloop observer的运行环境
1
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
2. 创建Runloop observer对象
1
2
3
4
5
6
7
8
9
10
11
12
13
第一个参数:用于分配observer对象的内存
第二个参数:用以设置observer所要关注的事件
第三个参数:用于标识该observer是在第一次进入runloop时执行还是每次进入runloop处理时均执行
第四个参数:用于设置该observer的优先级
第五个参数:用于设置该observer的回调函数
第六个参数:用于设置该observer的运行环境
// 创建Runloop observer对象
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
3. 将新建的observer加入到当前thread的runloop
1
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
4. 将observer从当前thread的runloop中移除
1
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
5. 释放 observer
1
CFRelease(_observer); _observer = NULL;
信号量
关于信号量,可以详细参考 GCD信号量-dispatch_semaphore_t
简单来说,主要有三个函数
1 |
|
dispatch_semaphore_create(long value);和GCD的group等用法一致,这个函数是创建一个dispatch_semaphore_类型的信号量,并且创建的时候需要指定信号量的大小。
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); 等待信号量。如果信号量值为0,那么该函数就会一直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。
dispatch_semaphore_signal(dispatch_semaphore_t deem); 发送信号量。该函数会对信号量的值进行加1操作。
通常等待信号量和发送信号量的函数是成对出现的。并发执行任务时候,在当前任务执行之前,用dispatch_semaphore_wait函数进行等待(阻塞),直到上一个任务执行完毕后且通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),dispatch_semaphore_wait函数收到信号量之后判断信号量的值大于等于1,会再对信号量的值减1,然后当前任务可以执行,执行完毕当前任务后,再通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),通知执行下一个任务……如此一来,通过信号量,就达到了并发队列中的任务同步执行的要求。
监控卡顿
原理: 利用观察Runloop各种状态变化的持续时间来检测计算是否发生卡顿
一次有效卡顿采用了“N次卡顿超过阈值T”的判定策略,即一个时间段内卡顿的次数累计大于N时才触发采集和上报:举例,卡顿阈值T=500ms、卡顿次数N=1,可以判定为单次耗时较长的一次有效卡顿;而卡顿阈值T=50ms、卡顿次数N=5,可以判定为频次较快的一次有效卡顿
主要代码
1 | // minimum |
1 | -(void)registerObserver{ |
demo测试
我把demo放在了github demo地址
使用时候,只需要
1 |
|
控制器中,每次点击屏幕,休眠1秒钟,如下
1 |
|
点击屏幕之后,打印如下
1 | YZMonitorRunLoopDemo[10288:1915706] ==========检测到卡顿之后调用堆栈========== |
即可定位到卡顿位置
-[ViewController touchesBegan:withEvent:]
卡顿日志写入本地
上面已经监控到了卡顿,和调用堆栈。如果是debug模式下,可以直接看日志,如果想在线上查看的话,可以写入本地,然后上传到服务器
写入本地数据库
- 创建本地路径
1 | -(NSString *)getLogPath{ |
- 如果是第一次写入,带上设备信息,手机型号等信息
1 | NSString *filePath = [self getLogPath]; |
- 如果本地文件已经存在,就先判断大小是否过大,决定是否直接写入,还是先上传到服务器
1 | float filesize = -1.0; |
压缩日志,上传服务器
因为都是文本数据,所以我们可以压缩之后,打打降低占用空间,然后进行上传,上传成功之后,删除本地,然后继续写入,等待下次写日志
压缩工具
使用 SSZipArchive具体使用起来也很简单,
1 | // Unzipping |
代码中
1 | NSString *zipPath = [self getLogZipPath]; |
具体如果上传到服务器,使用者可以用AFN等将本地的 zip文件上传到文件服务器即可,就不赘述了。
至此,我们做到了,用runloop,监控卡顿,写入日志,然后压缩上传服务器,删除本地的过程。
详细代码见demo地址
参考资料 :