上篇文章說道,RunLoop總結與面試,搞懂了RunLoop底層原理,固然要寫東西練手嘍,參考以前同事寫的工具和一些文章,輸出此文。html
監控卡頓,說白了就是找到主線程都在幹些啥。 咱們知道一個線程的消息事件處理都是依賴於NSRunLoop來驅動,因此要知道線程正在調用什麼方法,就須要從NSRunLoop來入手。git
RunLoop的執行代碼大體以下:github
{
/// 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處理事件的時間主要出在兩個階段:面試
咱們可使用CFRunLoopObserverRef來監控NSRunLoop的狀態,經過它能夠實時得到這些狀態值的變化。swift
設置Runloop observer的運行環境
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
bash
建立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);
async
將observer從當前thread的runloop中移除 CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
ide
釋放 observer
CFRelease(_observer); _observer = NULL;
函數
//建立信號量,參數:信號量的初值,若是小於0則會返回NULL
dispatch_semaphore_create(信號量值)
//等待下降信號量
dispatch_semaphore_wait(信號量,等待時間)
//提升信號量
dispatch_semaphore_signal(信號量)
複製代碼
注意:正常的使用順序是先下降而後再提升,這兩個函數一般成對使用。
原理: 利用觀察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;
}
});
}
複製代碼
用一個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;
}
複製代碼
當檢測到卡頓時,抓取堆棧信息,而後在客戶端作一些過濾處理,(Debug)能夠保存在本地,(Release)能夠上傳服務器,經過收集必定量的卡頓數據後,通過分析便能準肯定位須要優化的地方。
獲取堆棧信息後,可使用Demo中MJCallStack類(參考:BSBacktraceLogger—輕量級調用棧分析器) 或 KSCrash、PLCrashReporter等來解析。
至此這個實時卡頓監控就大功告成了。
GitHub地址: MJRunLoopDemo
簡單監測iOS卡頓的demo
iOS實時卡頓監控
BSBacktraceLogger
RunLoop總結與面試
dispatch_semaphore(信號量)的理解及使用