RunLoop實時卡頓監控
原理
官方文件說明了RunLoop的執行順序:
1. Notify observers that the run loop has been entered.
2. Notify observers that any ready timers are about to fire.
3. Notify observers that any input sources that are not port based are about to fire.
4. Fire any non-port-based input sources that are ready to fire.
5. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
6. Notify observers that the thread is about to sleep.
7. Put the thread to sleep until one of the following events occurs:
* An event arrives for a port-based input source.
* A timer fires.
* The timeout value set for the run loop expires.
* The run loop is explicitly woken up.
8. Notify observers that the thread just woke up.
9. Process the pending event.
* If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
* If an input source fired, deliver the event.
* If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
10. Notify observers that the run loop has exited.
用虛擬碼來實現就是這樣的:
{
/// 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是在應用啟動時自動開啟的,也沒有超時時間,所以正常情況下,主執行緒的RunLoop 只會在 2---9 之間無限迴圈下去。
不難發現NSRunLoop呼叫方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間,還有kCFRunLoopAfterWaiting之後,也就是如果我們發現這兩個時間內耗時太長,那麼就可以判定出此時主執行緒卡頓.
要監控NSRunLoop的狀態,我們需要使用到CFRunLoopObserverRef,通過它可以實時獲得這些狀態值的變化,
只需要另外再開啟一個執行緒,實時計算這兩個狀態區域之間的耗時是否到達某個閥值,便能揪出這些效能殺手.
為了讓計算更精確,需要讓子執行緒更及時的獲知主執行緒NSRunLoop狀態變化,所以dispatch_semaphore_t是個不錯的選擇,另外卡頓需要覆蓋到多次連續小卡頓和單次長時間卡頓兩種情景,所以判定條件也需要做適當優化.將上面兩個方法新增計算的邏輯如下:
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
SeMonitorController *instrance = [SeMonitorController sharedInstance];
instrance->_activity = activity;
// 傳送訊號
dispatch_semaphore_t semaphore = instrance->_semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver
{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
// 建立訊號
_semaphore = dispatch_semaphore_create(0);
// 在子執行緒監控時長
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 假定連續5次超時50ms認為卡頓(當然也包含了單次超時250ms)
long st = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0)
{
if (_activity==kCFRunLoopBeforeSources || _activity==kCFRunLoopAfterWaiting)
{
if (++_countTime < 5)
continue;
[self logStack];
NSLog(@"something lag");
}
}
_countTime = 0;
}
});
}
參考文件:https://www.jianshu.com/p/71cfbcb15842