檢測iOS的APP 性能的一些方法
首先如果遇到應(yīng)用卡頓或者因為內(nèi)存占用過多時一般使用Instruments里的來進(jìn)行檢測。但對于復(fù)雜情況可能就需要用到子線程監(jiān)控主線程的方式來了,下面我對這些方法做些介紹:
Time Profiler
可以查看多個線程里那些方法費(fèi)時過多的方法。先將右側(cè)Hide System Libraries打上勾,這樣能夠過濾信息。然后在Call Tree上會默認(rèn)按照費(fèi)時的線程進(jìn)行排序,單個線程中會也會按照對應(yīng)的費(fèi)時方法排序,選擇方法后能夠通過右側(cè)Heaviest Stack Trace里雙擊查看到具體的費(fèi)時操作代碼,從而能夠有針對性的優(yōu)化,而不需要在一些本來就不會怎么影響性能的地方過度優(yōu)化。
Allocations
這里可以對每個動作的前后進(jìn)行Generations,對比內(nèi)存的增加,查看使內(nèi)存增加的具體的方法和代碼所在位置。具體操作是在右側(cè)Generation Analysis里點擊Mark Generation,這樣會產(chǎn)生一個Generation,切換到其他頁面或一段時間產(chǎn)生了另外一個事件時再點Mark Generation來產(chǎn)生一個新的Generation,這樣反復(fù),生成多個Generation,查看這幾個Generation會看到Growth的大小,如果太大可以點進(jìn)去查看相應(yīng)占用較大的線程里右側(cè)Heaviest Stack Trace里查看對應(yīng)的代碼塊,然后進(jìn)行相應(yīng)的處理。
Leak
可以在上面區(qū)域的Leaks部分看到對應(yīng)的時間點產(chǎn)生的溢出,選擇后在下面區(qū)域的Statistics>Allocation Summary能夠看到泄漏的對象,同樣可以通過Stack Trace查看到具體對應(yīng)的代碼區(qū)域。
開發(fā)時需要注意如何避免一些性能問題
NSDateFormatter
通過Instruments的檢測會發(fā)現(xiàn)創(chuàng)建NSDateFormatter或者設(shè)置NSDateFormatter的屬性的耗時總是排在前面,如何處理這個問題呢,比較推薦的是添加屬性或者創(chuàng)建靜態(tài)變量,這樣能夠使得創(chuàng)建初始化這個次數(shù)降到***。還有就是可以直接用C,或者這個NSData的Category來解決https://github.com/samsoffes/sstoolkit/blob/master/SSToolkit/NSData%2BSSToolkitAdditions.m
UIImage
這里要主要是會影響內(nèi)存的開銷,需要權(quán)衡下imagedNamed和imageWithContentsOfFile,了解兩者特性后,在只需要顯示一次的圖片用后者,這樣會減少內(nèi)存的消耗,但是頁面顯示會增加Image IO的消耗,這個需要注意下。由于imageWithContentsOfFile不緩存,所以需要在每次頁面顯示前加載一次,這個IO的操作也是需要考慮權(quán)衡的一個點。
頁面加載
如果一個頁面內(nèi)容過多,view過多,這樣將長頁面中的需要滾動才能看到的那個部分視圖內(nèi)容通過開啟新的線程同步的加載。
優(yōu)化***加載時間
通過Time Profier可以查看到啟動所占用的時間,如果太長可以通過Heaviest Stack Trace找到費(fèi)時的方法進(jìn)行改造。
監(jiān)控卡頓的方法
還有種方法是在程序里去監(jiān)控性能問題。可以先看看這個Demo,地址https://github.com/ming1016/DecoupleDemo。 這樣在上線后可以通過這個程序?qū)⒂脩舻目D操作記錄下來,定時發(fā)到自己的服務(wù)器上,這樣能夠更大范圍的收集性能問題。眾所周知,用戶層面感知的卡頓都是來自處理所有UI的主線程上,包括在主線程上進(jìn)行的大計算,大量的IO操作,或者比較重的繪制工作。如何監(jiān)控主線程呢,首先需要知道的是主線程和其它線程一樣都是靠NSRunLoop來驅(qū)動的??梢韵瓤纯碈FRunLoopRun的大概的邏輯
- int32_t __CFRunLoopRun()
- {
- __CFRunLoopDoObservers(KCFRunLoopEntry);
- do
- {
- __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
- __CFRunLoopDoObservers(kCFRunLoopBeforeSources); //這里開始到kCFRunLoopBeforeWaiting之間處理時間是感知卡頓的關(guān)鍵地方
- __CFRunLoopDoBlocks();
- __CFRunLoopDoSource0(); //處理UI事件
- //GCD dispatch main queue
- CheckIfExistMessagesInMainDispatchQueue();
- //休眠前
- __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
- //等待msg
- mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
- //等待中
- //休眠后,喚醒
- __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
- //定時器喚醒
- if (wakeUpPort == timerPort)
- __CFRunLoopDoTimers();
- //異步處理
- else if (wakeUpPort == mainDispatchQueuePort)
- __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
- //UI,動畫
- else
- __CFRunLoopDoSource1();
- //確保同步
- __CFRunLoopDoBlocks();
- } while (!stop && !timeout);
- //退出RunLoop
- __CFRunLoopDoObservers(CFRunLoopExit);
- }
根據(jù)這個RunLoop我們能夠通過CFRunLoopObserverRef來度量。用GCD里的dispatch_semaphore_t開啟一個新線程,設(shè)置一個極限值和出現(xiàn)次數(shù)的值,然后獲取主線程上在kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting兩個狀態(tài)之間的超過了極限值和出現(xiàn)次數(shù)的場景,將堆棧dump下來,***發(fā)到服務(wù)器做收集,通過堆棧能夠找到對應(yīng)出問題的那個方法。
- 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)建一個觀察者
- 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), ^{
- //子線程開啟一個持續(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;
- }
- //兩個runloop的狀態(tài),BeforeSources和AfterWaiting這兩個狀態(tài)區(qū)間時間能夠檢測到是否卡頓
- if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
- //出現(xiàn)三次出結(jié)果
- if (++timeoutCount 3) {
- continue;
- }
- //將堆棧信息上報服務(wù)器的代碼放到這里
- } //end activity
- }// end semaphore wait
- timeoutCount = 0;
- }// end while
- });
- }
有時候造成卡頓是因為數(shù)據(jù)異常,過多,或者過大造成的,亦或者是操作的異常出現(xiàn)的,這樣的情況可能在平時日常開發(fā)測試中難以遇到,但是在真實的特別是用戶受眾廣的情況下會有人出現(xiàn),這樣這種收集卡頓的方式還是有價值的。
堆棧dump的方法
***種是直接調(diào)用系統(tǒng)函數(shù)獲取棧信息,這種方法只能夠獲得簡單的信息,沒法配合dSYM獲得具體哪行代碼出了問題,類型也有限。這種方法的主要思路是signal進(jìn)行錯誤信號的獲取。代碼如下
- 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)錯誤信號捕獲
- 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的話出的報告看起來能夠定位到問題代碼的具體位置了。
- 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);
測試Demo里堆棧中的內(nèi)容,超過了微信正文字?jǐn)?shù),所以本文省略了