在移動設備上開發軟件,性能一直是我們最為關心的話題之一,我們作為程序員除了需要努力提高代碼質量之外,及時發現和監控軟件中那些造成性能低下的”罪魁禍首”也是我們神聖的職責.
眾所周知,iOS平台因為UIKit本身的特性,需要將所有的UI操作都放在主線程執行,所以也造成不少程序員都習慣將一些線程安全性不確定的邏輯,以及其它線程結束後的匯總工作等等放到了主線,所以主線程中包含的這些大量計算、IO、繪制都有可能造成卡頓.
在Xcode中已經集成了非常方便的調試工具Instruments,它可以幫助我們在開發測試階段分析軟件運行的性能消耗,但一款軟件經過測試流程和實驗室分析肯定是不夠的,在正式環境中由大量用戶在使用過程中監控、分析到的數據更能解決一些隱藏的問題.
尋找卡頓的切入點
監控卡頓,最直接就是找到主線程都在干些啥玩意兒.我們知道一個線程的消息事件處理都是依賴於NSRunLoop來驅動,所以要知道線程正在調用什麼方法,就需要從NSRunLoop來入手.CFRunLoop的代碼是開源,可以在此處查閱到源代碼http://opensource.apple.com/source/CF/CF-1151.16/CFRunLoop.c,其中核心方法CFRunLoopRun簡化後的主要邏輯大概是這樣的:
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,通過它可以實時獲得這些狀態值的變化,具體的使用如下:
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是個不錯的選擇,另外卡頓需要覆蓋到多次連續小卡頓和單次長時間卡頓兩種情景,所以判定條件也需要做適當優化.將上面兩個方法添加計算的邏輯如下:
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信息也可用於實時獲取各線程的調用堆棧,使用示例如下:
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