上篇文章說(shuō)道,RunLoop總結(jié)與面試吗货,搞懂了RunLoop底層原理泳唠,當(dāng)然要寫(xiě)東西練手嘍,參考之前同事寫(xiě)的工具和一些文章宙搬,輸出此文笨腥。
1.尋找卡頓切入點(diǎn)
監(jiān)控卡頓,說(shuō)白了就是找到主線(xiàn)程都在干些啥勇垛。 我們知道一個(gè)線(xiàn)程的消息事件處理都是依賴(lài)于NSRunLoop來(lái)驅(qū)動(dòng),所以要知道線(xiàn)程正在調(diào)用什么方法脖母,就需要從NSRunLoop來(lái)入手。
RunLoop的執(zhí)行代碼大致如下:
{
/// 1. 通知Observers闲孤,即將進(jìn)入RunLoop
/// 此處有Observer會(huì)創(chuàng)建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即將觸發(fā) Timer 回調(diào)谆级。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即將觸發(fā) Source (非基于port的,Source0) 回調(diào)。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 觸發(fā) Source0 (非基于port的) 回調(diào)讼积。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers肥照,即將進(jìn)入休眠
/// 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,線(xiàn)程被喚醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer喚醒的币砂,回調(diào)Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch喚醒的建峭,執(zhí)行所有調(diào)用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件喚醒了玻侥,處理這個(gè)事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers决摧,即將退出RunLoop
/// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
從上可以看出RunLoop處理事件的時(shí)間主要出在兩個(gè)階段:
- kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間
- kCFRunLoopAfterWaiting之后
2.RunLoop 函數(shù)
我們可以使用CFRunLoopObserverRef來(lái)監(jiān)控NSRunLoop的狀態(tài),通過(guò)它可以實(shí)時(shí)獲得這些狀態(tài)值的變化。
設(shè)置Runloop observer的運(yùn)行環(huán)境
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
創(chuàng)建Runloop observer對(duì)象
第一個(gè)參數(shù):用于分配observer對(duì)象的內(nèi)存
第二個(gè)參數(shù):用以設(shè)置observer所要關(guān)注的事件凑兰,詳見(jiàn)回調(diào)函數(shù)myRunLoopObserver中注釋
第三個(gè)參數(shù):用于標(biāo)識(shí)該observer是在第一次進(jìn)入runloop時(shí)執(zhí)行還是每次進(jìn)入runloop處理時(shí)均執(zhí)行
第四個(gè)參數(shù):用于設(shè)置該observer的優(yōu)先級(jí)
第五個(gè)參數(shù):用于設(shè)置該observer的回調(diào)函數(shù)
第六個(gè)參數(shù):用于設(shè)置該observer的運(yùn)行環(huán)境
CFRunLoopObserverCreate(<#CFAllocatorRef allocator#>, <#CFOptionFlags activities#>, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>)
將新建的observer加入到當(dāng)前thread的runloop
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
將observer從當(dāng)前thread的runloop中移除
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
釋放 observer
CFRelease(_observer); _observer = NULL;
3.信號(hào)量
//創(chuàng)建信號(hào)量掌桩,參數(shù):信號(hào)量的初值,如果小于0則會(huì)返回NULL
dispatch_semaphore_create(信號(hào)量值)
//等待降低信號(hào)量
dispatch_semaphore_wait(信號(hào)量姑食,等待時(shí)間)
//提高信號(hào)量
dispatch_semaphore_signal(信號(hào)量)
注意:正常的使用順序是先降低然后再提高波岛,這兩個(gè)函數(shù)通常成對(duì)使用融击。
4.量化卡頓的程度
原理:
利用觀(guān)察Runloop各種狀態(tài)變化的持續(xù)時(shí)間來(lái)檢測(cè)計(jì)算是否發(fā)生卡頓
一次有效卡頓采用了“N次卡頓超過(guò)閾值T”的判定策略屑咳,即一個(gè)時(shí)間段內(nèi)卡頓的次數(shù)累計(jì)大于N時(shí)才觸發(fā)采集和上報(bào):舉例,卡頓閾值T=500ms、卡頓次數(shù)N=1券躁,可以判定為單次耗時(shí)較長(zhǎng)的一次有效卡頓;而卡頓閾值T=50ms址否、卡頓次數(shù)N=5贬派,可以判定為頻次較快的一次有效卡頓
實(shí)踐:
我們需要開(kāi)啟一個(gè)子線(xiàn)程,實(shí)時(shí)計(jì)算兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)是否到達(dá)某個(gè)閥值。另外卡頓需要覆蓋到多次連續(xù)小卡頓和單次長(zhǎng)時(shí)間卡頓兩種情景坛善。
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
MJMonitorRunloop *instance = [MJMonitorRunloop sharedInstance];
// 記錄狀態(tài)值
instance->_activity = activity;
// 發(fā)送信號(hào)
dispatch_semaphore_t semaphore = instance->_semaphore;
dispatch_semaphore_signal(semaphore);
}
// 注冊(cè)一個(gè)Observer來(lái)監(jiān)測(cè)Loop的狀態(tài),回調(diào)函數(shù)是runLoopObserverCallBack
- (void)registerObserver
{
// 設(shè)置Runloop observer的運(yùn)行環(huán)境
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
// 創(chuàng)建Runloop observer對(duì)象
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
// 將新建的observer加入到當(dāng)前thread的runloop
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
// 創(chuàng)建信號(hào)
_semaphore = dispatch_semaphore_create(0);
__weak __typeof(self) weakSelf = self;
// 在子線(xiàn)程監(jiān)控時(shí)長(zhǎng)
dispatch_async(dispatch_get_global_queue(0, 0), ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
while (YES) {
if (strongSelf.isCancel) {
return;
}
// N次卡頓超過(guò)閾值T記錄為一次卡頓
long dsw = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, strongSelf.limitMillisecond * NSEC_PER_MSEC));
if (dsw != 0) {
if (self->_activity == kCFRunLoopBeforeSources || self->_activity == kCFRunLoopAfterWaiting) {
if (++strongSelf.countTime < strongSelf.standstillCount){
NSLog(@"%ld",strongSelf.countTime);
continue;
}
[strongSelf logStack];
[strongSelf printLogTrace];
NSString *backtrace = [MJCallStack mj_backtraceOfMainThread];
NSLog(@"++++%@",backtrace);
if (strongSelf.callbackWhenStandStill) {
strongSelf.callbackWhenStandStill();
}
}
}
strongSelf.countTime = 0;
}
});
}
5.測(cè)試用例
用一個(gè)tableView視圖晾蜘,上下拖動(dòng),人為設(shè)置卡頓(休眠)眠屎,來(lái)測(cè)試我們實(shí)時(shí)監(jiān)控困頓的代碼是否有效剔交。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *identify =@"cellIdentify";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identify];
if(!cell) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identify];
}
if (indexPath.row % 10 == 0) {
usleep(1 * 1000 * 1000); // 1秒
cell.textLabel.text = @"卡咯";
}else{
cell.textLabel.text = [NSString stringWithFormat:@"%ld",indexPath.row];
}
return cell;
}
6.記錄卡頓數(shù)據(jù)
當(dāng)檢測(cè)到卡頓時(shí),抓取堆棧信息,然后在客戶(hù)端做一些過(guò)濾處理,(Debug)可以保存在本地,(Release)可以上傳服務(wù)器改衩,通過(guò)收集一定量的卡頓數(shù)據(jù)后岖常,經(jīng)過(guò)分析便能準(zhǔn)確定位需要優(yōu)化的地方。
獲取堆棧信息后葫督,可以使用Demo中MJCallStack類(lèi)(參考:BSBacktraceLogger—輕量級(jí)調(diào)用棧分析器) 或 KSCrash腥椒、PLCrashReporter等來(lái)解析。
至此這個(gè)實(shí)時(shí)卡頓監(jiān)控就大功告成了候衍。GitHub地址:
MJRunLoopDemo
參考文章:
簡(jiǎn)單監(jiān)測(cè)iOS卡頓的demo
iOS實(shí)時(shí)卡頓監(jiān)控
BSBacktraceLogger
RunLoop總結(jié)與面試
dispatch_semaphore(信號(hào)量)的理解及使用