檢測iOS的APP 性能的一些方法
首先如果遇到應用卡頓或者因為內存占用過多時一般使用Instruments里的來進行檢測。但對于復雜情況可能就需要用到子線程監控主線程的方式來了,下面我對這些方法做些介紹:
Time Profiler
可以查看多個線程里那些方法費時過多的方法。先將右側Hide System Libraries打上勾,這樣能夠過濾信息。然后在Call Tree上會默認按照費時的線程進行排序,單個線程中會也會按照對應的費時方法排序,選擇方法后能夠通過右側Heaviest Stack Trace里雙擊查看到具體的費時操作代碼,從而能夠有針對性的優化,而不需要在一些本來就不會怎么影響性能的地方過度優化。
Allocations
這里可以對每個動作的前后進行Generations,對比內存的增加,查看使內存增加的具體的方法和代碼所在位置。具體操作是在右側Generation Analysis里點擊Mark Generation,這樣會產生一個Generation,切換到其他頁面或一段時間產生了另外一個事件時再點Mark Generation來產生一個新的Generation,這樣反復,生成多個Generation,查看這幾個Generation會看到Growth的大小,如果太大可以點進去查看相應占用較大的線程里右側Heaviest Stack Trace里查看對應的代碼塊,然后進行相應的處理。
Leak
可以在上面區域的Leaks部分看到對應的時間點產生的溢出,選擇后在下面區域的Statistics>Allocation Summary能夠看到泄漏的對象,同樣可以通過Stack Trace查看到具體對應的代碼區域。
開發時需要注意如何避免一些性能問題
NSDateFormatter
通過Instruments的檢測會發現創建NSDateFormatter或者設置NSDateFormatter的屬性的耗時總是排在前面,如何處理這個問題呢,比較推薦的是添加屬性或者創建靜態變量,這樣能夠使得創建初始化這個次數降到***。還有就是可以直接用C,或者這個NSData的Category來解決https://github.com/samsoffes/sstoolkit/blob/master/SSToolkit/NSData%2BSSToolkitAdditions.m
UIImage
這里要主要是會影響內存的開銷,需要權衡下imagedNamed和imageWithContentsOfFile,了解兩者特性后,在只需要顯示一次的圖片用后者,這樣會減少內存的消耗,但是頁面顯示會增加Image IO的消耗,這個需要注意下。由于imageWithContentsOfFile不緩存,所以需要在每次頁面顯示前加載一次,這個IO的操作也是需要考慮權衡的一個點。
頁面加載
如果一個頁面內容過多,view過多,這樣將長頁面中的需要滾動才能看到的那個部分視圖內容通過開啟新的線程同步的加載。
優化***加載時間
通過Time Profier可以查看到啟動所占用的時間,如果太長可以通過Heaviest Stack Trace找到費時的方法進行改造。
監控卡頓的方法
還有種方法是在程序里去監控性能問題。可以先看看這個Demo,地址https://github.com/ming1016/DecoupleDemo。 這樣在上線后可以通過這個程序將用戶的卡頓操作記錄下來,定時發到自己的服務器上,這樣能夠更大范圍的收集性能問題。眾所周知,用戶層面感知的卡頓都是來自處理所有UI的主線程上,包括在主線程上進行的大計算,大量的IO操作,或者比較重的繪制工作。如何監控主線程呢,首先需要知道的是主線程和其它線程一樣都是靠NSRunLoop來驅動的。可以先看看CFRunLoopRun的大概的邏輯
- int32_t __CFRunLoopRun()
- {
- __CFRunLoopDoObservers(KCFRunLoopEntry);
- do
- {
- __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
- __CFRunLoopDoObservers(kCFRunLoopBeforeSources); //這里開始到kCFRunLoopBeforeWaiting之間處理時間是感知卡頓的關鍵地方
- __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);
- }
根據這個RunLoop我們能夠通過CFRunLoopObserverRef來度量。用GCD里的dispatch_semaphore_t開啟一個新線程,設置一個極限值和出現次數的值,然后獲取主線程上在kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting兩個狀態之間的超過了極限值和出現次數的場景,將堆棧dump下來,***發到服務器做收集,通過堆棧能夠找到對應出問題的那個方法。
- 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保證同步
- //創建一個觀察者
- CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
- runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
- kCFRunLoopAllActivities,
- YES,
- 0,
- &runLoopObserverCallBack,
- &context);
- //將觀察者添加到主線程runloop的common模式下的觀察中
- CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
- //創建子線程監控
- dispatch_async(dispatch_get_global_queue(0, 0), ^{
- //子線程開啟一個持續的loop用來進行監控
- 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的狀態,BeforeSources和AfterWaiting這兩個狀態區間時間能夠檢測到是否卡頓
- if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
- //出現三次出結果
- if (++timeoutCount 3) {
- continue;
- }
- //將堆棧信息上報服務器的代碼放到這里
- } //end activity
- }// end semaphore wait
- timeoutCount = 0;
- }// end while
- });
- }
有時候造成卡頓是因為數據異常,過多,或者過大造成的,亦或者是操作的異常出現的,這樣的情況可能在平時日常開發測試中難以遇到,但是在真實的特別是用戶受眾廣的情況下會有人出現,這樣這種收集卡頓的方式還是有價值的。
堆棧dump的方法
***種是直接調用系統函數獲取棧信息,這種方法只能夠獲得簡單的信息,沒法配合dSYM獲得具體哪行代碼出了問題,類型也有限。這種方法的主要思路是signal進行錯誤信號的獲取。代碼如下
- 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]; //得到當前調用棧信息
- NSString *exceptionReason = [exception reason]; //非常重要,就是崩潰的原因
- NSString *exceptionName = [exception name]; //異常類型
- }
- void SignalHandler(int code)
- {
- NSLog(@"signal handler = %d",code);
- }
- void InitCrashReport()
- {
- //系統錯誤信號捕獲
- 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];
- //將字符串上傳服務器
- NSLog(@"lag happen, detail below:
- %@",lagReportString);
測試Demo里堆棧中的內容,超過了微信正文字數,所以本文省略了