iOS 性能监控:Runloop 卡顿监控的坑
作者:酷酷的哀殿,百度 iOS 开发工程师
背景
前两天,一位朋友遇到一个问题,说自己无法使用 Runloop
监测到 -tableView:didSelectRowAtIndexPath:
场景的卡顿。
什么意思呢?就是监控不到下面这段代码的卡顿问题:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
int a = 8;
NSLog(@"调试:大量计算");
for (long i = 0; i < 999999999; i++) {
a = a + 1;
}
NSLog(@"调试:大量计算结束");
}
当用户点击 cell
时,上述代码会触发一次大量计算,具体的调用栈和调试日志如下所示:
从红框的 console
日志,我们可以发现上面卡顿监控代码,没有任何相关的卡顿提示。
出于好奇心,我就对这个问题研究了一番,找到的原因,在这里做一次总结和分享。
常规卡顿方案和问题
首先,问题的主要原因在于目前网上的「Runloop 卡顿监控」技术方案并不是完善的,存在一些漏洞,会导致丢失这类场景的监控。
如果我们用 Runloop 卡顿 为关键字,搜索相关技术方案,基本上都是下面这种方案:
// 注册
- (void)beginMonitor {
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
CFRunLoopObserverRef runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
//将观察者添加到主线程runloop的common模式下的观察中
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
//创建子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
int i=0;
//子线程开启一个持续的loop用来进行监控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 80 * NSEC_PER_MSEC));
NSLog(@"while%@",@(i++));
printAct(self->runLoopActivity);
if (semaphoreWait != 0) {
if (!self->runLoopObserver) {
self->timeoutCount = 0;
self->dispatchSemaphore = 0;
self->runLoopActivity = 0;
return;
}
//两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
//出现三次出结果
if (++self->timeoutCount < 3) {
continue;
}
NSLog(@"调试:监测到卡顿");
} //end activity
}// end semaphore wait
self->timeoutCount = 0;
}// end while
});
}
// 记录状态
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
lagMonitor->runLoopActivity = activity;
printAct(activity);
dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}
简单的整理一下上述代码的思路:
创建一个 CFRunLoopObserverRef
,并提供runLoopObserverCallBack
记录Runloop
的CFRunLoopActivity
变化向 main Runloop
添加该observer
开启异步线程,并以指定间隔持续监测 CFRunLoopActivity
连续 3 次检测到 kCFRunLoopBeforeSources
或者kCFRunLoopAfterWaiting
时,认为当前处于卡顿状态,触发 卡顿 的数据收集
卡顿监控失效分析
1、代码执行顺序
首先,我们先将监控代码与 Runloop
的执行顺序合并到一起进行分析:
Runloop
通知 卡顿检测代码 进入kCFRunLoopBeforeWaiting
状态Runloop
执行UIKit
的 点击事件 逻辑Runloop
进入 休眠状态
值得重点关注的是上图两个回调的执行顺序:卡顿监控
比 点击事件
更早接收到 kCFRunLoopBeforeWaiting
事件。
当 点击事件
执行时,异步线程会因为 卡顿监控
先接到 kCFRunLoopBeforeWaiting
状态,导致错误认为 Runloop
处于睡眠状态。
所以,为了解决卡顿监控
代码无法检测 tableView:didSelectRowAtIndexPath:
的现象,我们需要将 kCFRunLoopBeforeWaiting_卡顿监控
调用时机进行调整。
2、 __CFRunLoopDoObservers
函数的执行逻辑
为了调整回调的执行顺序,我们需要先了解 __CFRunLoopDoObservers
函数的执行逻辑。
/* rl is locked, rlm is locked on entrance and exit */
static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) __attribute__((noinline));
static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) { /* DOES CALLOUT */
cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_OBSERVERS | DBG_FUNC_START, rl, rlm, activity, 0);
CHECK_FOR_FORK();
// 获取 runLoopMode 的 observer 数量,如果小于1,则直接返回
CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0;
if (cnt < 1) return;
/* Fire the observers */
STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1);
CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef));
CFIndex obs_cnt = 0;
// 1、顺序遍历 _observers,
// 因为每个 observer 可以观察不同的 activity,所以,需要通过 & 操作符过滤需要触发的 observer
// 并组成新的数组 collectedObservers
for (CFIndex idx = 0; idx < cnt; idx++) {
CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
// 【避免递归】1、通过 __CFRunLoopObserverIsFiring 判断是否处于执行状态
if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) {
collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo);
}
}
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
// 2、顺序遍历 collectedObservers
for (CFIndex idx = 0; idx < obs_cnt; idx++) {
CFRunLoopObserverRef rlo = collectedObservers[idx];
__CFRunLoopObserverLock(rlo);
if (__CFIsValid(rlo)) {
// 【非重复 observer】1、记录是否属于非重复 observer
Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo);
// 【避免递归】2、回调前,通过 __CFRunLoopObserverSetFiring 记录执行的状态
__CFRunLoopObserverSetFiring(rlo);
__CFRunLoopObserverUnlock(rlo);
CFRunLoopObserverCallBack callout = rlo->_callout;
void *info = rlo->_context.info;
cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_OBSERVER | DBG_FUNC_START, callout, rlo, activity, info);
// 3、执行 observer 的回调
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(callout, rlo, activity, info);
cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_OBSERVER | DBG_FUNC_END, callout, rlo, activity, info);
// 【非重复 observer】2、非重复 observer,在回调完毕后,直接销毁
if (doInvalidate) {
CFRunLoopObserverInvalidate(rlo);
}
// 【避免递归】3、回调后,通过 __CFRunLoopObserverUnsetFiring 恢复状态
__CFRunLoopObserverUnsetFiring(rlo);
} else {
__CFRunLoopObserverUnlock(rlo);
}
CFRelease(rlo);
}
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
if (collectedObservers != buffer)
free(collectedObservers);
cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_OBSERVERS | DBG_FUNC_END, rl, rlm, activity, 0);
}
值得注意的是,__CFRunLoopMode
持有一个数组类型的结构成员:_observers
__CFRunLoopDoObservers
会先遍历_observers
,并根据各种条件组成一个新的数组collectedObservers
新的数组生成后,会再次遍历 collectedObservers
,并通过__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
回调监控函数
所以,我们可以得到第一个重要的结论:通过控制 _observers
数组的排列顺序,能够改变调用时机。
3、CFRunLoopAddObserver 函数的执行逻辑
为了控制 _observers
数组的排列顺序,我们还需要先看看 CFRunLoopAddObserver
函数的执行逻辑。
如下,创建 CFRunLoopObserverRef
时,开发者可以传入 CFIndex order
参数
CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
CFRunLoopAddObserver
函数内部会根据 CFRunLoopObserverRef
的 _order
逆序遍历 CFRunLoopRef
的 _observers
,并找到合适的位置进行插入
具体的源码如下所示:
// 添加 observer
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName) {
CHECK_FOR_FORK();
CFRunLoopModeRef rlm;
// 如果 runloop 处于销毁状态,直接返回
if (__CFRunLoopIsDeallocating(rl)) return;
// 如果主线程已经停止执行,则直接返回
if (__CFMainThreadHasExited && rl == CFRunLoopGetMain()) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CFLog(kCFLogLevelError, CFSTR("Attempting to add observer to main runloop, but the main thread has exited. This message will only log once. Break on _CFRunLoopError_MainThreadHasExited to debug."));
});
_CFRunLoopError_MainThreadHasExited();
return;
}
// 合规性校验 & 防止重入
if (!__CFIsValid(rlo) || (NULL != rlo->_runLoop && rlo->_runLoop != rl)) return;
__CFRunLoopLock(rl);
// 1、如果监听 kCFRunLoopCommonModes,则遍历 _commonModes,并进行监听
if (modeName == kCFRunLoopCommonModes) {
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
CFSetAddValue(rl->_commonModeItems, rlo);
if (NULL != set) {
CFTypeRef context[2] = {rl, rlo};
/* add new item to all common-modes */
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else {
rlm = __CFRunLoopFindMode(rl, modeName, true);
if (NULL != rlm && NULL == rlm->_observers) {
rlm->_observers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks);
}
if (NULL != rlm && !CFArrayContainsValue(rlm->_observers, CFRangeMake(0, CFArrayGetCount(rlm->_observers)), rlo)) {
Boolean inserted = false;
// 2、逆序遍历 _observers,并找到合适的位置进行插入
for (CFIndex idx = CFArrayGetCount(rlm->_observers); idx--; ) {
CFRunLoopObserverRef obs = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
if (obs->_order <= rlo->_order) {
CFArrayInsertValueAtIndex(rlm->_observers, idx + 1, rlo);
inserted = true;
break;
}
}
if (!inserted) {
CFArrayInsertValueAtIndex(rlm->_observers, 0, rlo);
}
rlm->_observerMask |= rlo->_activities;
__CFRunLoopObserverSchedule(rlo, rl, rlm);
}
if (NULL != rlm) {
__CFRunLoopModeUnlock(rlm);
}
}
__CFRunLoopUnlock(rl);
}
为了方便读者理解上面的逻辑,我们通过一个具体的示例进行讲解。
如下,假设现有的 observers
的 order
是 0
、8
、 12
,新插入的 observers
的 order
分别是 0
和 10
;
则,两个 observers
会分别插入到 stub0
和 stub1
位置。
所以,我们可以得到第二个重要结论:通过调整 CFRunLoopObserverCreate
的 order
参数,可以调整两个回调的执行顺序。
高可用的 Runloop 卡顿监测方案
根据前面的两个结论,我们可以采用将 order
调整到 LONG_MAX
的方式改变调用顺序:
1、优化方案
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
LONG_MAX,
&runLoopObserverCallBack,
&context);
重新编译&运行APP后,我们可以发现 console
的内容变成如下:
2、高可用方案
相信聪明的读者很容易发现上面的优化方案仍然存在下面的badcase。
当 kCFRunLoopAfterWaiting_其它阻塞事件
位置发生卡顿时,新方案因为执行顺序比较晚,卡顿监控代码仍然认为当前处于休眠状态,导致无法进行卡顿监控。
针对上面的情况,我们可以使用的双 Observer
的方式处理:
第一个 Observer
的order
调整到LONG_MIN
进入 kCFRunLoopAfterWaiting
状态时,第一个被调用,用于监控Runloop
处于 运行状态第二个 Observer
的order
调整到LONG_MAX
进入 kCFRunLoopBeforeWaiting
状态时,最后一个被调用,用于判断Runloop
处于 睡眠状态
如下图所示,通过 双 Observer
,我们可以更加准确的判断Runloop
的运行状态,从而对卡顿进行更加有效的监控。
- (void)addRunLoopObserver
{
NSRunLoop *curRunLoop = [NSRunLoop currentRunLoop];
// 第一个监控,监控是否处于 **运行状态**
CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};
CFRunLoopObserverRef beginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &myRunLoopBeginCallback, &context);
CFRetain(beginObserver);
m_runLoopBeginObserver = beginObserver;
// 第二个监控,监控是否处于 **睡眠状态**
CFRunLoopObserverRef endObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &myRunLoopEndCallback, &context);
CFRetain(endObserver);
m_runLoopEndObserver = endObserver;
CFRunLoopRef runloop = [curRunLoop getCFRunLoop];
CFRunLoopAddObserver(runloop, beginObserver, kCFRunLoopCommonModes);
CFRunLoopAddObserver(runloop, endObserver, kCFRunLoopCommonModes);
}
// 第一个监控,监控是否处于 **运行状态**
void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
g_runLoopActivity = activity;
g_runLoopMode = eRunloopDefaultMode;
switch (activity) {
case kCFRunLoopEntry:
g_bRun = YES;
break;
case kCFRunLoopBeforeTimers:
if (g_bRun == NO) {
gettimeofday(&g_tvRun, NULL);
}
g_bRun = YES;
break;
case kCFRunLoopBeforeSources:
if (g_bRun == NO) {
gettimeofday(&g_tvRun, NULL);
}
g_bRun = YES;
break;
case kCFRunLoopAfterWaiting:
if (g_bRun == NO) {
gettimeofday(&g_tvRun, NULL);
}
g_bRun = YES;
break;
case kCFRunLoopAllActivities:
break;
default:
break;
}
}
// 第二个监控,监控是否处于 **睡眠状态**
void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
g_runLoopActivity = activity;
g_runLoopMode = eRunloopDefaultMode;
switch (activity) {
case kCFRunLoopBeforeWaiting:
gettimeofday(&g_tvRun, NULL);
g_bRun = NO;
break;
case kCFRunLoopExit:
g_bRun = NO;
break;
case kCFRunLoopAllActivities:
break;
default:
break;
}
}
总结
本文通过分析 __CFRunLoopDoObservers
函数 和 CFRunLoopAddObserver
函数的内部逻辑,分析了网络上广泛流传的 Runloop 卡顿监测方案 存在低可用性问题的原因,并给出了一份高可用的 Runloop 卡顿监测方案 。
完整代码,可以访问 腾讯 的 matrix[1] 仓库获取
推荐阅读
✨ iOS 性能优化:使用 MetricKit 2.0 收集数据
✨ iOS 性能优化:用 Xcode Organizer 诊断性能问题
关注我们
我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。
关注有礼,关注【老司机技术周报】,回复「2020」,领取学习大礼包。
参考资料
matrix: https://github.com/Tencent/matrix/blob/master/matrix/matrix-iOS/Matrix/WCCrashBlockMonitor/CrashBlockPlugin/Main/BlockMonitor/WCBlockMonitorMgr.mm#L815-L844