在移動裝置上開發軟體,效能一直是我們最為關心的話題之一,我們作為程式設計師除了需要努力提高程式碼質量之外,及時發現和監控軟體中那些造成效能低下的”罪魁禍首”也是我們神聖的職責.
眾所周知,iOS平臺因為UIKit本身的特性,需要將所有的UI操作都放在主執行緒執行,所以也造成不少程式設計師都習慣將一些執行緒安全性不確定的邏輯,以及其它執行緒結束後的彙總工作等等放到了主線,所以主執行緒中包含的這些大量計算、IO、繪製都有可能造成卡頓.
在Xcode中已經整合了非常方便的除錯工具Instruments,它可以幫助我們在開發測試階段分析軟體執行的效能消耗,但一款軟體經過測試流程和實驗室分析肯定是不夠的,在正式環境中由大量使用者在使用過程中監控、分析到的資料更能解決一些隱藏的問題.
尋找卡頓的切入點
監控卡頓,最直接就是找到主執行緒都在幹些啥玩意兒.我們知道一個執行緒的訊息事件處理都是依賴於NSRunLoop來驅動,所以要知道執行緒正在呼叫什麼方法,就需要從NSRunLoop來入手.CFRunLoop的程式碼是開源,可以在此處查閱到原始碼http://opensource.apple.com/source/CF/CF-1151.16/CFRunLoop.c,其中核心方法CFRunLoopRun簡化後的主要邏輯大概是這樣的:
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 |
int32_t __CFRunLoopRun() { //通知即將進入runloop __CFRunLoopDoObservers(KCFRunLoopEntry); do { // 通知將要處理timer和source __CFRunLoopDoObservers(kCFRunLoopBeforeTimers); __CFRunLoopDoObservers(kCFRunLoopBeforeSources); __CFRunLoopDoBlocks(); //處理非延遲的主執行緒呼叫 __CFRunLoopDoSource0(); //處理UIEvent事件 //GCD dispatch main queue CheckIfExistMessagesInMainDispatchQueue(); // 即將進入休眠 __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); // 等待核心mach_msg事件 mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts(); // Zzz... // 從等待中醒來 __CFRunLoopDoObservers(kCFRunLoopAfterWaiting); // 處理因timer的喚醒 if (wakeUpPort == timerPort) __CFRunLoopDoTimers(); // 處理非同步方法喚醒,如dispatch_async else if (wakeUpPort == mainDispatchQueuePort) __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() // UI重新整理,動畫顯示 else __CFRunLoopDoSource1(); // 再次確保是否有同步的方法需要呼叫 __CFRunLoopDoBlocks(); } while (!stop && !timeout); //通知即將退出runloop __CFRunLoopDoObservers(CFRunLoopExit); } |
不難發現NSRunLoop呼叫方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間,還有kCFRunLoopAfterWaiting之後,也就是如果我們發現這兩個時間內耗時太長,那麼就可以判定出此時主執行緒卡頓.
量化卡頓的程度
要監控NSRunLoop的狀態,我們需要使用到CFRunLoopObserverRef,通過它可以實時獲得這些狀態值的變化,具體的使用如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { MyClass *object = (__bridge MyClass*)info; object->activity = activity; } - (void)registerObserver { CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL}; CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context); CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); } |
只需要另外再開啟一個執行緒,實時計算這兩個狀態區域之間的耗時是否到達某個閥值,便能揪出這些效能殺手.
為了讓計算更精確,需要讓子執行緒更及時的獲知主執行緒NSRunLoop狀態變化,所以dispatch_semaphore_t是個不錯的選擇,另外卡頓需要覆蓋到多次連續小卡頓和單次長時間卡頓兩種情景,所以判定條件也需要做適當優化.將上面兩個方法新增計算的邏輯如下:
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 |
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { MyClass *object = (__bridge MyClass*)info; // 記錄狀態值 object->activity = activity; // 傳送訊號 dispatch_semaphore_t semaphore = moniotr->semaphore; dispatch_semaphore_signal(semaphore); } - (void)registerObserver { CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL}; CFRunLoopObserverRef 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 (++timeoutCount < 5) continue; NSLog(@"好像有點兒卡哦"); } } timeoutCount = 0; } }); } |
記錄卡頓的函式呼叫
監控到了卡頓現場,當然下一步便是記錄此時的函式呼叫資訊,此處可以使用一個第三方Crash收集元件PLCrashReporter,它不僅可以收集Crash資訊也可用於實時獲取各執行緒的呼叫堆疊,使用示例如下:
1 2 3 4 5 6 7 8 9 10 |
PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]; PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config]; NSData *data = [crashReporter generateLiveReport]; PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL]; NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter withTextFormat:PLCrashReportTextFormatiOS]; NSLog(@"------------\n%@\n------------", report); |
當檢測到卡頓時,抓取堆疊資訊,然後在客戶端做一些過濾處理,便可以上報到伺服器,通過收集一定量的卡頓資料後經過分析便能準確定位需要優化的邏輯,至此這個實時卡頓監控就大功告成了!
文章示例程式碼下載:PerformanceMonitor.zip