如何利用 RunLoop 监控 app 卡顿
2019-12-08 本文已影响0人
王技术
本文意义在分析如何利用runloop监控卡顿。代码可以看戴铭大佬的代码
卡顿问题的几种原因
- 复杂 UI 、图文混排的绘制量过大
- 在主线程上做网络同步请求
- 在主线程做大量的 IO 操作
- 运算量过大,CPU 持续高占用
- 死锁和主子线程抢锁
Runloop 监控卡顿
runloop 工作流程
runloop 工作流程
首先明确了 Runloop的状态有六个
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry , // 进入 loop
kCFRunLoopBeforeTimers , // 触发 Timer 回调
kCFRunLoopBeforeSources , // 触发 Source0 回调
kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
kCFRunLoopAfterWaiting ), // 接收 mach_port 消息
kCFRunLoopExit , // 退出 loop
kCFRunLoopAllActivities // loop 所有状态改变
}
上图可以看出
Runloop 真正处理事务的状态区间是
KCFRunloopBeforeSources->KCFRunLoopBeforeWaiting
kCFRunLoopAfterWaiting-> kCFRunLoopBeforeTimers
所以我们监听 RunLoop 在进入睡眠之前和唤醒之后的两个状态,分别是 kCFRunLoopBeforeSources
和 kCFRunLoopAfterWaiting
也就是要触发 Source0 回调和接收 mach_port 消息两个状态。
大致思路
- 创建一个 CFRunLoopObserverContext 观察者
- 将创建好的观察者 runLoopObserver 添加到主线程 RunLoop 的 common 模式下观察
- 每当监听到 Observer 通知,使信号量的值 +1
- 创建一个持续的子线程使用信号量专门用来监控主线程的 RunLoop 状态,设置信号量的等待时间
- 如过等待时间内子线程还没有被唤醒,则认为发生了卡顿
上代码 :
#import <Foundation/Foundation.h>
@interface SMLagMonitor : NSObject
+ (instancetype)shareInstance;
- (void)beginMonitor; //开始监视卡顿
- (void)endMonitor; //停止监视卡顿
@end
#import "SMLagMonitor.h"
@interface SMLagMonitor() {
int timeoutCount;
CFRunLoopObserverRef runLoopObserver;
@public
dispatch_semaphore_t dispatchSemaphore;
CFRunLoopActivity runLoopActivity;
}
@property (nonatomic, strong) NSTimer *cpuMonitorTimer;
@end
@implementation SMLagMonitor
#pragma mark - Interface
+ (instancetype)shareInstance {
static id instance = nil;
static dispatch_once_t dispatchOnce;
dispatch_once(&dispatchOnce, ^{
instance = [[self alloc] init];
});
return instance;
}
- (void)beginMonitor {
//监测卡顿
if (runLoopObserver) {
return;
}
dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保证同步
//创建一个观察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
//将观察者添加到主线程runloop的common模式下的观察中
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
//创建子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子线程开启一个持续的loop用来进行监控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
// semaphoreWait 的值不为 0, 说明线程被堵塞
if (semaphoreWait != 0) {
if (!runLoopObserver) {
timeoutCount = 0;
dispatchSemaphore = 0;
runLoopActivity = 0;
return;
}
// BeforeSources和 AfterWaiting 这两个 runloop 状态的区间时间能够检测到是否卡顿
if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
// 将堆栈信息上报服务器的代码放到这里
if (++ timeoutCount < 5) { //连续5次就是250毫秒
continue;
} else {
NSLog(@"卡顿了");
}
} //end activity
}// end semaphore wait
timeoutCount = 0;
}// end while
});
}
- (void)endMonitor {
[self.cpuMonitorTimer invalidate];
if (!runLoopObserver) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
CFRelease(runLoopObserver);
runLoopObserver = NULL;
}
#pragma mark - Private
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
lagMonitor->runLoopActivity = activity;
dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}
@end