上篇文章說道,RunLoop總結與面試,搞懂了RunLoop底層原理,當然要寫東西練手嘍,參考之前同事寫的工具和一些文章,輸出此文。
1.尋找卡頓切入點
監控卡頓,說白了就是找到主執行緒都在幹些啥。 我們知道一個執行緒的訊息事件處理都是依賴於NSRunLoop來驅動,所以要知道執行緒正在呼叫什麼方法,就需要從NSRunLoop來入手。
RunLoop的執行程式碼大致如下:
{
/// 1. 通知Observers,即將進入RunLoop
/// 此處有Observer會建立AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即將觸發 Timer 回撥。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即將觸發 Source (非基於port的,Source0) 回撥。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 觸發 Source0 (非基於port的) 回撥。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即將進入休眠
/// 此處有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,執行緒被喚醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer喚醒的,回撥Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch喚醒的,執行所有呼叫 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基於port的) 的事件喚醒了,處理這個事件
__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處理事件的時間主要出在兩個階段:
- kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間
- kCFRunLoopAfterWaiting之後
2.RunLoop 函式
我們可以使用CFRunLoopObserverRef來監控NSRunLoop的狀態,通過它可以實時獲得這些狀態值的變化。
-
設定Runloop observer的執行環境
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
-
建立Runloop observer物件
第一個引數:用於分配observer物件的記憶體 第二個引數:用以設定observer所要關注的事件,詳見回撥函式myRunLoopObserver中註釋 第三個引數:用於標識該observer是在第一次進入runloop時執行還是每次進入runloop處理時均執行 第四個引數:用於設定該observer的優先順序 第五個引數:用於設定該observer的回撥函式 第六個引數:用於設定該observer的執行環境
CFRunLoopObserverCreate(<#CFAllocatorRef allocator#>, <#CFOptionFlags activities#>, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>)
-
將新建的observer加入到當前thread的runloop
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
-
將observer從當前thread的runloop中移除
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
-
釋放 observer
CFRelease(_observer); _observer = NULL;
3.訊號量
//建立訊號量,引數:訊號量的初值,如果小於0則會返回NULL
dispatch_semaphore_create(訊號量值)
//等待降低訊號量
dispatch_semaphore_wait(訊號量,等待時間)
//提高訊號量
dispatch_semaphore_signal(訊號量)
複製程式碼
注意:正常的使用順序是先降低然後再提高,這兩個函式通常成對使用。
4.量化卡頓的程度
原理: 利用觀察Runloop各種狀態變化的持續時間來檢測計算是否發生卡頓 一次有效卡頓採用了“N次卡頓超過閾值T”的判定策略,即一個時間段內卡頓的次數累計大於N時才觸發採集和上報:舉例,卡頓閾值T=500ms、卡頓次數N=1,可以判定為單次耗時較長的一次有效卡頓;而卡頓閾值T=50ms、卡頓次數N=5,可以判定為頻次較快的一次有效卡頓
實踐: 我們需要開啟一個子執行緒,實時計算兩個狀態區域之間的耗時是否到達某個閥值。另外卡頓需要覆蓋到多次連續小卡頓和單次長時間卡頓兩種情景。
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
MJMonitorRunloop *instance = [MJMonitorRunloop sharedInstance];
// 記錄狀態值
instance->_activity = activity;
// 傳送訊號
dispatch_semaphore_t semaphore = instance->_semaphore;
dispatch_semaphore_signal(semaphore);
}
// 註冊一個Observer來監測Loop的狀態,回撥函式是runLoopObserverCallBack
- (void)registerObserver
{
// 設定Runloop observer的執行環境
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
// 建立Runloop observer物件
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
// 將新建的observer加入到當前thread的runloop
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
// 建立訊號
_semaphore = dispatch_semaphore_create(0);
__weak __typeof(self) weakSelf = self;
// 在子執行緒監控時長
dispatch_async(dispatch_get_global_queue(0, 0), ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
while (YES) {
if (strongSelf.isCancel) {
return;
}
// N次卡頓超過閾值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.測試用例
用一個tableView檢視,上下拖動,人為設定卡頓(休眠),來測試我們實時監控困頓的程式碼是否有效。
- (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.記錄卡頓資料
當檢測到卡頓時,抓取堆疊資訊,然後在客戶端做一些過濾處理,(Debug)可以儲存在本地,(Release)可以上傳伺服器,通過收集一定量的卡頓資料後,經過分析便能準確定位需要優化的地方。
獲取堆疊資訊後,可以使用Demo中MJCallStack類(參考:BSBacktraceLogger—輕量級呼叫棧分析器) 或 KSCrash、PLCrashReporter等來解析。
至此這個實時卡頓監控就大功告成了。
GitHub地址:
MJRunLoopDemo
參考文章:
簡單監測iOS卡頓的demo
iOS實時卡頓監控
BSBacktraceLogger
RunLoop總結與面試
dispatch_semaphore(訊號量)的理解及使用