前言
本文的demo程式碼也會更新到github上。
做這個demo思路來源於微信team的:微信iOS卡頓監控系統。
主要思路:通過監測Runloop的kCFRunLoopAfterWaiting,用一個子執行緒去檢查,一次迴圈是否時間太長。
其中主要涉及到了runloop的原理。關於整個原理:深入理解RunLoop講解的比較仔細。
以下就是runloop大概的執行方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
/// 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); /// 5. GCD處理main block __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); } |
其中UI主要集中在__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
和__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
之前。
獲取kCFRunLoopBeforeSources
到kCFRunLoopBeforeWaiting
再到kCFRunLoopAfterWaiting
的狀態就可以知道是否有卡頓的情況。
NSTimer的實現
具體程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// // MonitorController.h // RunloopMonitorDemo // // Created by game3108 on 16/4/13. // Copyright © 2016年 game3108. All rights reserved. // #import @interface MonitorController : NSObject + (instancetype) sharedInstance; - (void) startMonitor; - (void) endMonitor; - (void) printLogTrace; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
// // MonitorController.m // RunloopMonitorDemo // // Created by game3108 on 16/4/13. // Copyright © 2016年 game3108. All rights reserved. // #import "MonitorController.h" #include #include @interface MonitorController(){ CFRunLoopObserverRef _observer; double _lastRecordTime; NSMutableArray *_backtrace; } @end @implementation MonitorController static double _waitStartTime; + (instancetype) sharedInstance{ static dispatch_once_t once; static id sharedInstance; dispatch_once(&once, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } - (void) startMonitor{ [self addMainThreadObserver]; [self addSecondaryThreadAndObserver]; } - (void) endMonitor{ if (!_observer) { return; } CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes); CFRelease(_observer); _observer = NULL; } #pragma mark printLogTrace - (void)printLogTrace{ NSLog(@"====================堆疊\n %@ \n",_backtrace); } #pragma mark addMainThreadObserver - (void) addMainThreadObserver { dispatch_async(dispatch_get_main_queue(), ^{ //建立自動釋放池 @autoreleasepool { //獲得當前thread的Run loop NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop]; //設定Run loop observer的執行環境 CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL}; //建立Run loop observer物件 //第一個引數用於分配observer物件的記憶體 //第二個引數用以設定observer所要關注的事件,詳見回撥函式myRunLoopObserver中註釋 //第三個引數用於標識該observer是在第一次進入run loop時執行還是每次進入run loop處理時均執行 //第四個引數用於設定該observer的優先順序 //第五個引數用於設定該observer的回撥函式 //第六個引數用於設定該observer的執行環境 _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context); if (_observer) { //將Cocoa的NSRunLoop型別轉換成Core Foundation的CFRunLoopRef型別 CFRunLoopRef cfRunLoop = [myRunLoop getCFRunLoop]; //將新建的observer加入到當前thread的run loop CFRunLoopAddObserver(cfRunLoop, _observer, kCFRunLoopDefaultMode); } } }); } void myRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { switch (activity) { //The entrance of the run loop, before entering the event processing loop. //This activity occurs once for each call to CFRunLoopRun and CFRunLoopRunInMode case kCFRunLoopEntry: NSLog(@"run loop entry"); break; //Inside the event processing loop before any timers are processed case kCFRunLoopBeforeTimers: NSLog(@"run loop before timers"); break; //Inside the event processing loop before any sources are processed case kCFRunLoopBeforeSources: NSLog(@"run loop before sources"); break; //Inside the event processing loop before the run loop sleeps, waiting for a source or timer to fire. //This activity does not occur if CFRunLoopRunInMode is called with a timeout of 0 seconds. //It also does not occur in a particular iteration of the event processing loop if a version 0 source fires case kCFRunLoopBeforeWaiting:{ _waitStartTime = 0; NSLog(@"run loop before waiting"); break; } //Inside the event processing loop after the run loop wakes up, but before processing the event that woke it up. //This activity occurs only if the run loop did in fact go to sleep during the current loop case kCFRunLoopAfterWaiting:{ _waitStartTime = [[NSDate date] timeIntervalSince1970]; NSLog(@"run loop after waiting"); break; } //The exit of the run loop, after exiting the event processing loop. //This activity occurs once for each call to CFRunLoopRun and CFRunLoopRunInMode case kCFRunLoopExit: NSLog(@"run loop exit"); break; /* A combination of all the preceding stages case kCFRunLoopAllActivities: break; */ default: break; } } #pragma mark addSecondaryThreadAndObserver - (void) addSecondaryThreadAndObserver{ NSThread *thread = [self secondaryThread]; [self performSelector:@selector(addSecondaryTimer) onThread:thread withObject:nil waitUntilDone:YES]; } - (NSThread *)secondaryThread { static NSThread *_secondaryThread = nil; static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ _secondaryThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_secondaryThread start]; }); return _secondaryThread; } - (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"monitorControllerThread"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes]; [runLoop run]; } } - (void) addSecondaryTimer{ NSTimer *myTimer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode]; } - (void)timerFired:(NSTimer *)timer{ if ( _waitStartTime 2.0){ if (_lastRecordTime - _waitStartTime |
主要內容是首先在主執行緒註冊了runloop observer的回撥myRunLoopObserver
每次小迴圈都會記錄一下kCFRunLoopAfterWaiting
的時間_waitStartTime
,並且在kCFRunLoopBeforeWaiting
制空。
另外開了一個子執行緒並開啟他的runloop(模仿了AFNetworking的方式),並加上一個timer每隔1秒去進行監測。
如果當前時長與_waitStartTime
差距大於2秒,則認為有卡頓情況,並記錄了當前堆疊資訊。
PS:整個demo寫的比較簡單,最後獲取堆疊也僅獲取了當前執行緒的堆疊資訊([NSThread callStackSymbols]
有同樣效果),也在尋找獲取所有執行緒堆疊的方法,歡迎指點一下。
更新:
瞭解到 plcrashreporter (github地址) 可以做到獲取所有執行緒堆疊。
更新2:
這篇文章也介紹了監測卡頓的方法:檢測iOS的APP效能的一些方法
通過Dispatch Semaphore保證同步這裡記錄一下。
寫一個Semaphore版本的程式碼,也放在github上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// // SeMonitorController.h // RunloopMonitorDemo // // Created by game3108 on 16/4/14. // Copyright © 2016年 game3108. All rights reserved. // #import @interface SeMonitorController : NSObject + (instancetype) sharedInstance; - (void) startMonitor; - (void) endMonitor; - (void) printLogTrace; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
// // SeMonitorController.m // RunloopMonitorDemo // // Created by game3108 on 16/4/14. // Copyright © 2016年 game3108. All rights reserved. // #import "SeMonitorController.h" #import #import @interface SeMonitorController(){ CFRunLoopObserverRef _observer; dispatch_semaphore_t _semaphore; CFRunLoopActivity _activity; NSInteger _countTime; NSMutableArray *_backtrace; } @end @implementation SeMonitorController + (instancetype) sharedInstance{ static dispatch_once_t once; static id sharedInstance; dispatch_once(&once, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } - (void) startMonitor{ [self registerObserver]; } - (void) endMonitor{ if (!_observer) { return; } CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes); CFRelease(_observer); _observer = NULL; } - (void) printLogTrace{ NSLog(@"====================堆疊\n %@ \n",_backtrace); } 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 |
用Dispatch Semaphore簡化了程式碼複雜度,更加簡潔。
參考資料
1.微信iOS卡頓監控系統
2. iphone——使用run loop物件
3.深入理解RunLoop
4.檢測iOS的APP效能的一些方法
5.iOS實時卡頓監控