檢測(cè)iOS的APP 性能的一些方法
首先如果遇到應(yīng)用卡頓或者因?yàn)閮?nèi)存占用過多時(shí)一般使用Instruments里的來進(jìn)行檢測(cè)。但對(duì)于復(fù)雜情況可能就需要用到子線程監(jiān)控主線程的方式來了,下面我對(duì)這些方法做些介紹:
Time Profiler
可以查看多個(gè)線程里那些方法費(fèi)時(shí)過多的方法。先將右側(cè)Hide System Libraries打上勾,這樣能夠過濾信息。然后在Call Tree上會(huì)默認(rèn)按照費(fèi)時(shí)的線程進(jìn)行排序,單個(gè)線程中會(huì)也會(huì)按照對(duì)應(yīng)的費(fèi)時(shí)方法排序,選擇方法后能夠通過右側(cè)Heaviest Stack Trace里雙擊查看到具體的費(fèi)時(shí)操作代碼,從而能夠有針對(duì)性的優(yōu)化,而不需要在一些本來就不會(huì)怎么影響性能的地方過度優(yōu)化。
Allocations
這里可以對(duì)每個(gè)動(dòng)作的前后進(jìn)行Generations,對(duì)比內(nèi)存的增加,查看使內(nèi)存增加的具體的方法和代碼所在位置。具體操作是在右側(cè)Generation Analysis里點(diǎn)擊Mark Generation,這樣會(huì)產(chǎn)生一個(gè)Generation,切換到其他頁面或一段時(shí)間產(chǎn)生了另外一個(gè)事件時(shí)再點(diǎn)Mark Generation來產(chǎn)生一個(gè)新的Generation,這樣反復(fù),生成多個(gè)Generation,查看這幾個(gè)Generation會(huì)看到Growth的大小,如果太大可以點(diǎn)進(jìn)去查看相應(yīng)占用較大的線程里右側(cè)Heaviest Stack Trace里查看對(duì)應(yīng)的代碼塊,然后進(jìn)行相應(yīng)的處理。
Leak
可以在上面區(qū)域的Leaks部分看到對(duì)應(yīng)的時(shí)間點(diǎn)產(chǎn)生的溢出,選擇后在下面區(qū)域的Statistics>Allocation Summary能夠看到泄漏的對(duì)象,同樣可以通過Stack Trace查看到具體對(duì)應(yīng)的代碼區(qū)域。
開發(fā)時(shí)需要注意如何避免一些性能問題
NSDateFormatter
通過Instruments的檢測(cè)會(huì)發(fā)現(xiàn)創(chuàng)建NSDateFormatter或者設(shè)置NSDateFormatter的屬性的耗時(shí)總是排在前面,如何處理這個(gè)問題呢,比較推薦的是添加屬性或者創(chuàng)建靜態(tài)變量,這樣能夠使得創(chuàng)建初始化這個(gè)次數(shù)降到***。還有就是可以直接用C,或者這個(gè)NSData的Category來解決https://github.com/samsoffes/sstoolkit/blob/master/SSToolkit/NSData%2BSSToolkitAdditions.m
UIImage
這里要主要是會(huì)影響內(nèi)存的開銷,需要權(quán)衡下imagedNamed和imageWithContentsOfFile,了解兩者特性后,在只需要顯示一次的圖片用后者,這樣會(huì)減少內(nèi)存的消耗,但是頁面顯示會(huì)增加Image IO的消耗,這個(gè)需要注意下。由于imageWithContentsOfFile不緩存,所以需要在每次頁面顯示前加載一次,這個(gè)IO的操作也是需要考慮權(quán)衡的一個(gè)點(diǎn)。
頁面加載
如果一個(gè)頁面內(nèi)容過多,view過多,這樣將長(zhǎng)頁面中的需要滾動(dòng)才能看到的那個(gè)部分視圖內(nèi)容通過開啟新的線程同步的加載。
優(yōu)化***加載時(shí)間
通過Time Profier可以查看到啟動(dòng)所占用的時(shí)間,如果太長(zhǎng)可以通過Heaviest Stack Trace找到費(fèi)時(shí)的方法進(jìn)行改造。
監(jiān)控卡頓的方法
還有種方法是在程序里去監(jiān)控性能問題??梢韵瓤纯催@個(gè)Demo,地址https://github.com/ming1016/DecoupleDemo。 這樣在上線后可以通過這個(gè)程序?qū)⒂脩舻目D操作記錄下來,定時(shí)發(fā)到自己的服務(wù)器上,這樣能夠更大范圍的收集性能問題。眾所周知,用戶層面感知的卡頓都是來自處理所有UI的主線程上,包括在主線程上進(jìn)行的大計(jì)算,大量的IO操作,或者比較重的繪制工作。如何監(jiān)控主線程呢,首先需要知道的是主線程和其它線程一樣都是靠NSRunLoop來驅(qū)動(dòng)的??梢韵瓤纯碈FRunLoopRun的大概的邏輯
- int32_t __CFRunLoopRun()
 - {
 - __CFRunLoopDoObservers(KCFRunLoopEntry);
 - do
 - {
 - __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
 - __CFRunLoopDoObservers(kCFRunLoopBeforeSources); //這里開始到kCFRunLoopBeforeWaiting之間處理時(shí)間是感知卡頓的關(guān)鍵地方
 - __CFRunLoopDoBlocks();
 - __CFRunLoopDoSource0(); //處理UI事件
 - //GCD dispatch main queue
 - CheckIfExistMessagesInMainDispatchQueue();
 - //休眠前
 - __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
 - //等待msg
 - mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
 - //等待中
 - //休眠后,喚醒
 - __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
 - //定時(shí)器喚醒
 - if (wakeUpPort == timerPort)
 - __CFRunLoopDoTimers();
 - //異步處理
 - else if (wakeUpPort == mainDispatchQueuePort)
 - __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
 - //UI,動(dòng)畫
 - else
 - __CFRunLoopDoSource1();
 - //確保同步
 - __CFRunLoopDoBlocks();
 - } while (!stop && !timeout);
 - //退出RunLoop
 - __CFRunLoopDoObservers(CFRunLoopExit);
 - }
 
根據(jù)這個(gè)RunLoop我們能夠通過CFRunLoopObserverRef來度量。用GCD里的dispatch_semaphore_t開啟一個(gè)新線程,設(shè)置一個(gè)極限值和出現(xiàn)次數(shù)的值,然后獲取主線程上在kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting兩個(gè)狀態(tài)之間的超過了極限值和出現(xiàn)次數(shù)的場(chǎng)景,將堆棧dump下來,***發(fā)到服務(wù)器做收集,通過堆棧能夠找到對(duì)應(yīng)出問題的那個(gè)方法。
- static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
 - {
 - MyClass *object = (__bridge MyClass*)info;
 - object->activity = activity;
 - }
 - static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
 - SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
 - lagMonitor->runLoopActivity = activity;
 - dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
 - dispatch_semaphore_signal(semaphore);
 - }
 - - (void)endMonitor {
 - if (!runLoopObserver) {
 - return;
 - }
 - CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
 - CFRelease(runLoopObserver);
 - runLoopObserver = NULL;
 - }
 - - (void)beginMonitor {
 - if (runLoopObserver) {
 - return;
 - }
 - dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保證同步
 - //創(chuàng)建一個(gè)觀察者
 - CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
 - runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
 - kCFRunLoopAllActivities,
 - YES,
 - 0,
 - &runLoopObserverCallBack,
 - &context);
 - //將觀察者添加到主線程runloop的common模式下的觀察中
 - CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
 - //創(chuàng)建子線程監(jiān)控
 - dispatch_async(dispatch_get_global_queue(0, 0), ^{
 - //子線程開啟一個(gè)持續(xù)的loop用來進(jìn)行監(jiān)控
 - while (YES) {
 - long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 30*NSEC_PER_MSEC));
 - if (semaphoreWait != 0) {
 - if (!runLoopObserver) {
 - timeoutCount = 0;
 - dispatchSemaphore = 0;
 - runLoopActivity = 0;
 - return;
 - }
 - //兩個(gè)runloop的狀態(tài),BeforeSources和AfterWaiting這兩個(gè)狀態(tài)區(qū)間時(shí)間能夠檢測(cè)到是否卡頓
 - if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
 - //出現(xiàn)三次出結(jié)果
 - if (++timeoutCount 3) {
 - continue;
 - }
 - //將堆棧信息上報(bào)服務(wù)器的代碼放到這里
 - } //end activity
 - }// end semaphore wait
 - timeoutCount = 0;
 - }// end while
 - });
 - }
 
有時(shí)候造成卡頓是因?yàn)閿?shù)據(jù)異常,過多,或者過大造成的,亦或者是操作的異常出現(xiàn)的,這樣的情況可能在平時(shí)日常開發(fā)測(cè)試中難以遇到,但是在真實(shí)的特別是用戶受眾廣的情況下會(huì)有人出現(xiàn),這樣這種收集卡頓的方式還是有價(jià)值的。
堆棧dump的方法
***種是直接調(diào)用系統(tǒng)函數(shù)獲取棧信息,這種方法只能夠獲得簡(jiǎn)單的信息,沒法配合dSYM獲得具體哪行代碼出了問題,類型也有限。這種方法的主要思路是signal進(jìn)行錯(cuò)誤信號(hào)的獲取。代碼如下
- static int s_fatal_signals[] = {
 - SIGABRT,
 - SIGBUS,
 - SIGFPE,
 - SIGILL,
 - SIGSEGV,
 - SIGTRAP,
 - SIGTERM,
 - SIGKILL,
 - };
 - static int s_fatal_signal_num = sizeof(s_fatal_signals) / sizeof(s_fatal_signals[0]);
 - void UncaughtExceptionHandler(NSException *exception) {
 - NSArray *exceptionArray = [exception callStackSymbols]; //得到當(dāng)前調(diào)用棧信息
 - NSString *exceptionReason = [exception reason]; //非常重要,就是崩潰的原因
 - NSString *exceptionName = [exception name]; //異常類型
 - }
 - void SignalHandler(int code)
 - {
 - NSLog(@"signal handler = %d",code);
 - }
 - void InitCrashReport()
 - {
 - //系統(tǒng)錯(cuò)誤信號(hào)捕獲
 - for (int i = 0; i signal(s_fatal_signals[i], SignalHandler);
 - }
 - //oc未捕獲異常的捕獲
 - NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
 - }
 - int main(int argc, char * argv[]) {
 - @autoreleasepool {
 - InitCrashReport();
 - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
 - }
 - }
 
使用PLCrashReporter的話出的報(bào)告看起來能夠定位到問題代碼的具體位置了。
- NSData *lagData = [[[PLCrashReporter alloc]
 - initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];
 - PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];
 - NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];
 - //將字符串上傳服務(wù)器
 - NSLog(@"lag happen, detail below:
 - %@",lagReportString);
 
測(cè)試Demo里堆棧中的內(nèi)容,超過了微信正文字?jǐn)?shù),所以本文省略了















 
 
 




 
 
 
 