/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
RunLoop内部的逻辑图:
RunLoop内部原理.png
如何检测卡顿
4.1 首先知道RunLoop的六个状态
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
kCFRunLoopAllActivities // loop所有状态改变
};
要想监听RunLoop,你就首先需要创建一个 CFRunLoopObserverContext 观察者,代码如下:
- (void)registerObserver {
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//创建Run loop observer对象
//第一个参数用于分配observer对象的内存
//第二个参数用以设置observer所要关注的事件,详见回调函数myRunLoopObserver中注释
//第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行
//第四个参数用于设置该observer的优先级
//第五个参数用于设置该observer的回调函数
//第六个参数用于设置该observer的运行环境
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
实时获取变化的回调的方法:
//每当runloop状态变化的触发这个回调方法
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
MyClass *object = (__bridge MyClass*)info;
object->activity = activity;
}
其中UI主要集中在
_CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION(source0)和CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION(source1)之前。
获取kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting的状态就可以知道是否有卡顿的情况。
4.2 检测卡顿的思路
只需要另外再开启一个线程,实时计算这两个状态区域之间的耗时是否到达某个阀值,便能揪出这些性能杀手。
监听runloop状态变化回调方法
// 就是runloop有一个状态改变 就记录一下
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
BGPerformanceMonitor *monitor = (__bridge BGPerformanceMonitor*)info;
// 记录状态值
monitor->activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = monitor->semaphore;
long st = dispatch_semaphore_signal(semaphore);
NSLog(@"dispatch_semaphore_signal:st=%ld,time:%@",st,[BGPerformanceMonitor getCurTime]);
/* Run Loop Observer Activities */
// typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// kCFRunLoopEntry = (1UL << 0), // 进入RunLoop循环(这里其实还没进入)
// kCFRunLoopBeforeTimers = (1UL << 1), // RunLoop 要处理timer了
// kCFRunLoopBeforeSources = (1UL << 2), // RunLoop 要处理source了
// kCFRunLoopBeforeWaiting = (1UL << 5), // RunLoop要休眠了
// kCFRunLoopAfterWaiting = (1UL << 6), // RunLoop醒了
// kCFRunLoopExit = (1UL << 7), // RunLoop退出(和kCFRunLoopEntry对应)
// kCFRunLoopAllActivities = 0x0FFFFFFFU //RunLoop状态变化
// };
if (activity == kCFRunLoopEntry) { // 即将进入RunLoop
NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopEntry");
} else if (activity == kCFRunLoopBeforeTimers) { // 即将处理Timer
NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopBeforeTimers");
} else if (activity == kCFRunLoopBeforeSources) { // 即将处理Source
NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopBeforeSources");
} else if (activity == kCFRunLoopBeforeWaiting) { //即将进入休眠
NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopBeforeWaiting");
} else if (activity == kCFRunLoopAfterWaiting) { // 刚从休眠中唤醒
NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopAfterWaiting");
} else if (activity == kCFRunLoopExit) { // 即将退出RunLoop
NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopExit");
} else if (activity == kCFRunLoopAllActivities) {
NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopAllActivities");
}
}
开启runloop监听
// 开始监听
- (void)startMonitor {
if (observer) {
return;
}
// 创建信号
semaphore = dispatch_semaphore_create(0);
NSLog(@"dispatch_semaphore_create:%@",[BGPerformanceMonitor getCurTime]);
// 注册RunLoop状态观察
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//创建Run loop observer对象
//第一个参数用于分配observer对象的内存
//第二个参数用以设置observer所要关注的事件,详见回调函数myRunLoopObserver中注释
//第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行
//第四个参数用于设置该observer的优先级
//第五个参数用于设置该observer的回调函数
//第六个参数用于设置该observer的运行环境
observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES) { // 有信号的话 就查询当前runloop的状态
// 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)
// 因为下面 runloop 状态改变回调方法runLoopObserverCallBack中会将信号量递增 1,所以每次 runloop 状态改变后,下面的语句都会执行一次
// dispatch_semaphore_wait:Returns zero on success, or non-zero if the timeout occurred.
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
NSLog(@"dispatch_semaphore_wait:st=%ld,time:%@",st,[self getCurTime]);
if (st != 0) { // 信号量超时了 - 即 runloop 的状态长时间没有发生变更,长期处于某一个状态下
if (!observer) {
timeoutCount = 0;
semaphore = 0;
activity = 0;
return;
}
NSLog(@"st = %ld,activity = %lu,timeoutCount = %d,time:%@",st,activity,timeoutCount,[self getCurTime]);
// kCFRunLoopBeforeSources - 即将处理source kCFRunLoopAfterWaiting - 刚从休眠中唤醒
// 获取kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting的状态就可以知道是否有卡顿的情况。
// kCFRunLoopBeforeSources:停留在这个状态,表示在做很多事情
if (activity == kCFRunLoopBeforeSources || activity == kCFRunLoopAfterWaiting) { // 发生卡顿,记录卡顿次数
if (++timeoutCount < 5) {
continue; // 不足 5 次,直接 continue 当次循环,不将timeoutCount置为0
}
// 收集Crash信息也可用于实时获取各线程的调用堆栈
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);
}
}
NSLog(@"dispatch_semaphore_wait timeoutCount = 0,time:%@",[self getCurTime]);
timeoutCount = 0;
}
});
}
记录卡顿的函数调用
监控到了卡顿现场,当然下一步便是记录此时的函数调用信息,此处可以使用一个第三方Crash 收集组件 PLCrashReporter,它不仅可以收集 Crash 信息也可用于实时获取各线程的调用堆栈,示例如下:
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);
当检测到卡顿时,抓取堆栈信息,然后在客户端做一些过滤处理,便可以上报到服务器,通过收集一定量的卡顿数据后经过分析便能准确定位需要优化的逻辑,这个实时卡顿监控就大功告成了!
结尾
通过 Runloop 来检测卡顿,还是很有必要的。对提高 app 的用户使用体验还是很有帮助的。毕竟卡顿是偶显的不容易复现。所以检测卡顿来来抓取堆栈信息,分析并解决卡顿,还是很有必要的。
☞2021微信大数据挑战赛正式启动报名!☞CTO 两年吃回扣上百万元,将面临数十年监禁☞Linux基金会要“下田”了!开源技术在农业领域能做什么?
所有评论(0)