作者:酷酷的哀殿,百度 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
进入 休眠状态image-17000108309
值得重点关注的是上图两个回调的执行顺序:卡顿监控
比 点击事件
更早接收到 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」,领取学习大礼包。
参考资料
[1]
matrix: https://github.com/Tencent/matrix/blob/master/matrix/matrix-iOS/Matrix/WCCrashBlockMonitor/CrashBlockPlugin/Main/BlockMonitor/WCBlockMonitorMgr.mm#L815-L844
转载:https://blog.csdn.net/sinat_35969632/article/details/111351356