參考:http://www.tanhao.me/code/151113.html/
在移動(dòng)設(shè)備上開發(fā)軟件,性能一直是我們最為關(guān)心的話題之一,我們作為程序員除了需要努力提高代碼質(zhì)量之外,及時(shí)發(fā)現(xiàn)和監(jiān)控軟件中那些造成性能低下的”罪魁禍?zhǔn)住币彩俏覀兩袷サ穆氊?zé).
眾所周知,iOS平臺(tái)因?yàn)閁IKit本身的特性,需要將所有的UI操作都放在主線程執(zhí)行,所以也造成不少程序員都習(xí)慣將一些線程安全性不確定的邏輯,以及其它線程結(jié)束后的匯總工作等等放到了主線,所以主線程中包含的這些大量計(jì)算、IO赋访、繪制都有可能造成卡頓.
在Xcode中已經(jīng)集成了非常方便的調(diào)試工具Instruments,它可以幫助我們?cè)陂_發(fā)測(cè)試階段分析軟件運(yùn)行的性能消耗,但一款軟件經(jīng)過(guò)測(cè)試流程和實(shí)驗(yàn)室分析肯定是不夠的,在正式環(huán)境中由大量用戶在使用過(guò)程中監(jiān)控读慎、分析到的數(shù)據(jù)更能解決一些隱藏的問(wèn)題.
尋找卡頓的切入點(diǎn)
監(jiān)控卡頓,最直接就是找到主線程都在干些啥玩意兒.我們知道一個(gè)線程的消息事件處理都是依賴于NSRunLoop來(lái)驅(qū)動(dòng),所以要知道線程正在調(diào)用什么方法,就需要從NSRunLoop來(lái)入手.CFRunLoop的代碼是開源,可以在此處查閱到源代碼http://opensource.apple.com/source/CF/CF-1151.16/CFRunLoop.c,其中核心方法CFRunLoopRun簡(jiǎn)化后的主要邏輯大概是這樣的:
int32_t __CFRunLoopRun()
{//通知即將進(jìn)入runloop
__CFRunLoopDoObservers(KCFRunLoopEntry);
do
{
// 通知將要處理timer和source__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);? ? ? ? __CFRunLoopDoObservers(kCFRunLoopBeforeSources);? ? ? ? ? ? ? ? __CFRunLoopDoBlocks();//處理非延遲的主線程調(diào)用__CFRunLoopDoSource0();//處理UIEvent事件
//GCD dispatch main queueCheckIfExistMessagesInMainDispatchQueue();
// 即將進(jìn)入休眠_(dá)_CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
// 等待內(nèi)核mach_msg事件mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
// Zzz...
// 從等待中醒來(lái)__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// 處理因timer的喚醒if(wakeUpPort == timerPort)? ? ? ? ? ? __CFRunLoopDoTimers();
// 處理異步方法喚醒,如dispatch_asyncelseif(wakeUpPort == mainDispatchQueuePort)? ? ? ? ? ? __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
// UI刷新,動(dòng)畫顯示else__CFRunLoopDoSource1();
// 再次確保是否有同步的方法需要調(diào)用__CFRunLoopDoBlocks();
}while(!stop && !timeout);//通知即將退出runloop__CFRunLoopDoObservers(CFRunLoopExit);
}
不難發(fā)現(xiàn)NSRunLoop調(diào)用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間,還有kCFRunLoopAfterWaiting之后,也就是如果我們發(fā)現(xiàn)這兩個(gè)時(shí)間內(nèi)耗時(shí)太長(zhǎng),那么就可以判定出此時(shí)主線程卡頓.
量化卡頓的程度
要監(jiān)控NSRunLoop的狀態(tài),我們需要使用到CFRunLoopObserverRef,通過(guò)它可以實(shí)時(shí)獲得這些狀態(tài)值的變化,具體的使用如下:
staticvoidrunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity,void*info)
{
MyClass *object = (__bridge MyClass*)info;
object->activity = activity;
}
- (void)registerObserver
{
CFRunLoopObserverContext context = {0,(__bridgevoid*)self,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
只需要另外再開啟一個(gè)線程,實(shí)時(shí)計(jì)算這兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)是否到達(dá)某個(gè)閥值,便能揪出這些性能殺手.
為了讓計(jì)算更精確,需要讓子線程更及時(shí)的獲知主線程N(yùn)SRunLoop狀態(tài)變化,所以dispatch_semaphore_t是個(gè)不錯(cuò)的選擇,另外卡頓需要覆蓋到多次連續(xù)小卡頓和單次長(zhǎng)時(shí)間卡頓兩種情景,所以判定條件也需要做適當(dāng)優(yōu)化.將上面兩個(gè)方法添加計(jì)算的邏輯如下:
staticvoidrunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity,void*info)
{
MyClass *object = (__bridge MyClass*)info;
// 記錄狀態(tài)值
object->activity = activity;
// 發(fā)送信號(hào)
dispatch_semaphore_t semaphore = moniotr->semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver
{
CFRunLoopObserverContext context = {0,(__bridgevoid*)self,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 創(chuàng)建信號(hào)
semaphore = dispatch_semaphore_create(0);
// 在子線程監(jiān)控時(shí)長(zhǎng)
dispatch_async(dispatch_get_global_queue(0,0), ^{
while(YES)
{
// 假定連續(xù)5次超時(shí)50ms認(rèn)為卡頓(當(dāng)然也包含了單次超時(shí)250ms)
longst = 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(@"好像有點(diǎn)兒卡哦");
}
}
timeoutCount =0;
}
});
}
記錄卡頓的函數(shù)調(diào)用
監(jiān)控到了卡頓現(xiàn)場(chǎng),當(dāng)然下一步便是記錄此時(shí)的函數(shù)調(diào)用信息,此處可以使用一個(gè)第三方Crash收集組件PLCrashReporter,它不僅可以收集Crash信息也可用于實(shí)時(shí)獲取各線程的調(diào)用堆棧,使用示例如下:
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);
當(dāng)檢測(cè)到卡頓時(shí),抓取堆棧信息,然后在客戶端做一些過(guò)濾處理,便可以上報(bào)到服務(wù)器,通過(guò)收集一定量的卡頓數(shù)據(jù)后經(jīng)過(guò)分析便能準(zhǔn)確定位需要優(yōu)化的邏輯,至此這個(gè)實(shí)時(shí)卡頓監(jiān)控就大功告成了!