《iOS APP 性能檢測》
| 導語 最近組裡在做性能優化,既然要優化,就首先要有指標來描述性能水平,並且可以檢測到這些指標,通過指標值的變化來看優化效果,於是筆者調研了iOS APP性能檢測的一些方法,在此總結一下。
首先,要明確性能檢測都需要關注哪些指標,筆者列舉了以下幾個主要的,後面會詳細說:
啟動時間
內存佔用量,內存告警次數
CPU使用率
頁面渲染時間,刷新幀率
網路請求時間,流量消耗
UI阻塞次數,不可操作時長,主線程阻塞超過400毫秒次數
耗電功率
對於靜態頁面來講,頁面的渲染時間就是從viewDidLoad第一行到viewDidAppear最後一行代碼的時間。但是大多數頁面是需要網路請求回數據才能正常展示。
主線程阻塞超過400毫秒就會讓用戶感知到卡頓,跟用戶交互的操作如渲染,管理觸摸反應,回應輸入等都是在主線程的,所以不要讓主線程承擔過多耗時操作,耗時操作放到子線程中進行。
性能檢測的途徑主要分三大類:
Xcode自帶的Instrument
使用第三方SDK
自行開發檢測代碼
Instrument
Xcode自帶的Instrument工具是一個以獨立APP形式存在的工具集,包含了很多強大的檢測功能:其中包括在真機和模擬器上進行性能測試,對APP進行性能分析,檢查一個或多個應用或進程的行為。
檢查設備相關的功能,比如:Wi-Fi、藍牙等。 查找 App 中的內存問題,比如內存泄露(Leaked memory)、廢棄內存(Abandoned memory)、殭屍(zombies)等。
讓我們來大概看一下Instrument都可以做什麼
1.Blank(空模板):創建一個空的模板,可以從Library庫中添加其他模板
2.Activity Monitor(活動監視器):監控進程級別的CPU,內存,磁碟,網路使用情況,可以得到你的應用程序在手機運行時總共佔用的內存大小
3.Allocations(內存分配):跟蹤過程的匿名虛擬內存和堆的對象提供類名和可選保留/釋放歷史,可以檢測每一個堆對象的分配內存情況
4.Cocoa Layout :觀察NSLayoutConstraint對象的改變,幫助我們判斷什麼時間什麼地點的constraint是否合理。觀察約束變化,找出布局代碼的問題所在
5.Core Animation(圖形性能):這個模塊顯示程序顯卡性能以及CPU使用情況
6.CoreData:這個模塊跟蹤Core Data文件系統活動
7.Counters :收集使用時間或基於事件的抽樣方法的性能監控計數器(PMC)事件
8.Energy Log: 耗電量監控
9.File Activity :檢測文件創建,移動,變化,刪除等
10.Leaks(泄漏):一般的措施內存使用情況,檢查泄漏的內存,並提供了所有活動的分配和泄漏模塊的類對象分配統計信息以及內存地址歷史記錄;
11.Metal System Trace:Metal API是apple 2014年在ios平台上推出的高效底層的3D圖形API,它通過減少驅動層的API調用CPU的消耗提高渲染效率。
12.Network: 用鏈接工具分析你的程序如何使用TCP/IP和UDP/IP鏈接
13.System Trace:系統跟蹤,通過顯示當前被調度線程提供綜合的系統表現,顯示從用戶到系統的轉換代碼通過兩個系統調用或內存操作
14.System Usage: 這個模板記錄關於文件讀寫,sockets,I/O系統活動, 輸入輸出
15.Time Profiler(時間探查):執行對系統的CPU上運行的進程低負載時間為基礎採樣。
16.Zombies: 測量一般的內存使用,專註於檢測過度釋放的【野指針】對象,也提供對象分配統計,以及主動分配的內存地址歷史
下面這張圖把上面的工具按照不同類別的訴求分了類,但是這張圖比較早,有的工具被合併入上面的工具之中了。
Instrument還可以配合UI Test,通過腳本記錄一個用戶行為序列,這就為可重複多次的自動化測試提供了基礎。這個真的很神奇,因為這個腳本不是需要程序員來寫的,而是Xcode自動生成的!具體做法是這樣的。在工程項目中File→New→Target,選擇iOS UI Testing Bundle
打開生成的UITest文件,把游標放在-(void)testExample函數里,或者自己新建一個函數也可以,點擊下圖所示的紅點,應用程序就會以profile的模式運行,這個時候你的一系列操作都會有相應的代碼自動生成到這個函數中,操作結束之後點擊結束的按鈕。生成的代碼有可能會有報錯的地方,比如點擊了中文的按鈕,代碼中是顯示的是unicode轉義序列,需要手工改成中文才行。
代碼不報錯了以後,先編譯運行一遍,再通過Xcode的Product→perform action→profile testExample(如果是自己新建的函數就選擇對應的函數名),這時程序就會按照你剛剛的操作路徑進行一模一樣的操作了,包括你在某個頁面停留了多久,點擊的順序是如何的。我們在測試性能的時候,一般需要通過對比來說明優化的結果,然而對比就需要控制變數,兩次一模一樣的操作就很重要。需要說明的一點是,要保證很多其他因素都是相同的,比如兩次對比的應用中,一個是登錄態的,另一個沒有登錄,操作路徑記錄的包括了一些登錄態特有的操作,那麼當這個操作路徑運行在沒有登錄的版本上就會crash。
Instrument主要用於在調試過程中隨時發現問題,及時優化,但是這個工具只能供有應用源碼的程序員使用,無法測量用戶真實使用場景下的性能。
第三方SDK
有一些第三方的專門用於性能檢測和用戶行為、屬性分析的SDK,比如Bugly,OneAPM,聽雲,Firebase Analytics,把它們接入項目可以短期內達成性能檢測目標,這些第三方的工具原理都是類似的,利用 swizzle 的方法進行AOP(面向切面編程)處理,在關鍵函數之前和之後自動埋點記錄上報。有的平台也支持上傳符號表文件精確定位代碼執行位置以及以埋點的方式手工添加日誌記錄。使用起來還是比較方便的,基本上引入SDK和相關庫,在程序入口處啟動檢測即可。
然而使用第三方SDK的缺點也是非常明顯的,首先是缺乏定製性,我們需要的一些指標的統計SDK沒有,SDK有的我們又不完全需要,很有可能為了簡單的幾個值,讓安裝包增大許多。SDK具體統計了什麼有可能我們並不完全知道,這又涉及一個很重要的問題就是安全性,這些SDK涉及的統計數據都是APP的商業機密信息,對於有一定市場影響力的APP肯定會顧忌這一點。當然,一些小的創業公司剛剛起步時,人力相對不足,產品前景也未知的情況下,使用這類第三方SDK還是一個好的選擇。還有一點就是,這類產品是收費的,平時自己開發個demo練手也不適合連這種SDK,土豪請忽略。
自行添加檢測代碼
自行在項目中植入檢測代碼當然就安全可靠啦,而且想要什麼指標都可以定製化,有針對性。當然這麼做就免不了需要開發成本。而且還有一個問題,在代碼中檢測APP的性能本身可能也會帶來額外的性能損耗,這也是需要考慮和權衡的。
自行添加檢測代碼也大體分為兩類:
AOP:採用切面的方式,統一的為大量的類增加檢測代碼。具體做法是寫一個類作為UIViewController的分類,增加幾個方法如XXXviewdidload , XXXviewdidappear等,用swizzle替換一些對應的生命周期方法,塞入一些統計的代碼。示例代碼如下:
@implementation UIViewController (APMUIViewController) + (void) load { Class clz = [self class]; SEL oldSEL = @selector(viewDidLoad); SEL newSEL = @selector(newViewDidLoad); Method originalMethod = class_getInstanceMethod(clz, oldSEL); Method swizzledMethod = class_getInstanceMethod(clz, newSEL); BOOL didAddMethod =class_addMethod(clz, oldSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(clz, oldSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } - (void) newViewDidLoad { NSLog(@"start logging");//獲取性能的函數 [super viewDidLoad]; NSLog(@"end logging"); } @end
埋點:直接在想要的地方埋上你需要計算的性能指標、開始和結束時間的採集點,這種方式更加靈活,只關心自己關心的頁面。 AOP是「大鍋飯」,量大管飽,一次性為大量的類增加了檢測代碼,對原有代碼侵入性也較小;埋點是「開小灶」,隨心所欲,但是分散的代碼管理起來也是一個問題。
自行開發檢測代碼還需要考慮以下問題:
1.想獲取哪些指標,系統的API支持你獲取哪些值
2.合理的檢測時機是什麼地方,比如什麼樣的指標檢測代碼添加到什麼函數的哪一步中最合理
3.合理的上報策略和上報時機:我們不能每得到一個值就上報一次,這太消耗網路資源了。應該累積一段時間的數據,一次性上報。此外,上報的請求要錯開正常業務請求的高峰,可以給請求設定優先順序,業務請求的優先順序高於性能檢測的上報請求,如果有正常的業務請求在進行,就暫緩上報。以及,盡量在Wi-Fi環境下上傳。
4.如果必須獲取用戶在4G或3G環境下的性能指標,我們就要儘可能的少消耗用戶的流量,可以採用的方法有採用map關係,以簡短的代碼來代表一個複雜的意思;以及對上傳的內容進行壓縮
下面就每個指標詳細說一下檢測方法。
啟動時間
啟動時間可謂是用戶對你的APP的第一印象,用戶好不容易下載了APP,而且有興緻點開「寵幸」一下,啟動時間過長很可能會讓用戶直接把APP打入冷宮。就算用戶非常有耐心,蘋果的watch dog機制也會kill掉啟動時間過長的APP,這種情況下給用戶的感覺就是這APP怎麼一啟動就卡死然後崩潰了,不可用。這裡還要說一下,Xcode在debug模式下是沒有開啟watch dog的,所以不要以為調試時候沒問題就真的沒問題了,至少要在真機上試驗一下。
首先大概了解一下APP的啟動過程:
筆者在加斷點調試的時候得到的是下面的順序:
Launch頁
main()
UIApplicationMain()
willFinishLaunchingWithOptions()
didFinishLaunchingWithOptions()
loadView()
viewDidLoad()
applicationDidBecomeActive()
注意Launch頁是先於main函數出來的,main 函數就不說了,應用程序入口,裡面調用了UIApplicationMain。當App從didFinishLaunchingWithOptions返回的時候,實際的UI立刻開始載入。這裡的loadView是指你的app啟動後載入的第一個view,這個view會在其controller的viewDidLoad執行完後被載入,這也是頁面最終的初始化的時間。雖然UI 已經被初始化,但是在applicationDidBecomeActive這個回調完成之前UI仍舊被阻塞著。
我們要計算的啟動時間就是從main()到applicationDidBecomeActive()的時間,這個代碼很好加,分別在main的最開始和applicationDidBecomeActive的最後一行增加時間獲取的代碼即可。
還有一種使用環境變數的方法,在Xcode的Edit scheme中增加DYLD_PRINT_STATISTICS這個環境變數,如下圖所示:
運行項目後在控制台會列印出如下信息,每個階段都耗時多少。
這裡涉及到iOS APP首次載入時的幾個階段,本文就不詳細展開了,有興趣的可以參看http://www.jianshu.com/p/65901441903e。
通過Instrument的Time Profiler,找到包含-[UIApplication _reportAppLaunchFinished]的最後一幀,也可計算出啟動時間。
想得到應用程序的啟動時間還是很容易的,還是開頭那句話,啟動時間是用戶對APP的第一印象,盡量越快越好,在啟動階段(上述函數中)只進行必要的操作,盡量精簡邏輯,不要鏈接不必要的庫等等。
內存
Instrument裡面的內存測量相關的工具上面已經提過了,網上也有很多手把手的逐步截圖版教程,在這裡就不贅述了。貼一下獲取內存使用量的代碼:
#importach.h> #importask_info.h> - (unsigned long)memoryUsage { struct task_basic_info info; mach_msg_type_number_t size = sizeof(info); kern_return_t kr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size); if (kr != KERN_SUCCESS) { return -1; } unsigned long memorySize = info.resident_size >> 10;//10-KB 20-MB return memorySize; }
返回的數值單位是KB,如果想要MB的話把10改為20。
增加App的內存佔用的操作有創建對象,定義變數,調用函數的堆棧,多線程,密集的網路請求或長鏈接等等,我們可以對一些大的對象、view進行復用,懶載入資源,及時清理不再使用的資源(ARC下這個問題沒那麼嚴重)。
CPU使用率
同樣的Instrument的方式就不說了,直接貼代碼:
- (float)cpu_usage { kern_return_t kr = { 0 }; task_info_data_t tinfo = { 0 }; mach_msg_type_number_t task_info_count = TASK_INFO_MAX; kr = task_info( mach_task_self(), TASK_BASIC_INFO, (task_info_t)tinfo, &task_info_count ); if ( KERN_SUCCESS != kr ) return 0.0f; task_basic_info_t basic_info = { 0 }; thread_array_t thread_list = { 0 }; mach_msg_type_number_t thread_count = { 0 }; thread_info_data_t thinfo = { 0 }; thread_basic_info_t basic_info_th = { 0 }; basic_info = (task_basic_info_t)tinfo; // get threads in the task kr = task_threads( mach_task_self(), &thread_list, &thread_count ); if ( KERN_SUCCESS != kr ) return 0.0f; long tot_sec = 0; long tot_usec = 0; float tot_cpu = 0; for ( int i = 0; iflags & TH_FLAGS_IDLE) ) { tot_sec = tot_sec + basic_info_th->user_time.seconds + basic_info_th->system_time.seconds; tot_usec = tot_usec + basic_info_th->system_time.microseconds + basic_info_th->system_time.microseconds; tot_cpu = tot_cpu + basic_info_th->cpu_usage / (float)TH_USAGE_SCALE; } } kr = vm_deallocate( mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t) ); if ( KERN_SUCCESS != kr ) return 0.0f; return tot_cpu * 100.; // CPU 佔用百分比}
返回的是CPU佔用百分比。
大部分app都是在剛啟動不久內cpu佔用較大, 之後就漸漸趨於穩定,所以建議在剛開始採集間隔短一點比如1s,之後採集間隔逐漸加大,最後穩定到5分鐘獲取一次。此外,再有動畫的地方也要增加採集點。
影響CPU使用情況的主要是計算密集型的操作,比如動畫、布局計算和Autolayout、文本的計算和渲染、圖片的解碼和繪製。比較常見的一種優化方式就是緩存tableview的cell高度,避免每次計算。想要降低CPU的使用率就要盡量避免大量的計算,能緩存的緩存,不得不計算的,看看是否可以使用一些演算法進行優化,降低時間複雜度。
刷新幀率
刷新幀率可以通過Instrument里的Core Animation查看,也可以使用CADisplayLink,它是一個以和屏幕刷新率相同的頻率將內容畫到屏幕上的定時器,最快能每秒調用60次,在正常情況下會在每次刷新結束都被調用,精確度相當高。如果是CPU或是GPU某個步驟耗時導致渲染錯過了一次垂直信號,那這個方法就不會被調用了,之後統計的幀數也就隨之降低了。
下面是筆者在自選股項目中增加的一個實時顯示當前幀率的一個demo,在每個頁面都有這樣的一個彈窗,顯示在用戶進行操作時的刷新幀率,靜止不動時是60,展示動畫時這個值會掉的挺厲害。除了動畫之外,在頁面載入、tableview/scrollview滑動的時候也會明顯降低。
耗電功率
把耗電功率放到最後,是因為耗電功率是個比較綜合的指標,影響因素很多。跟性能相關的,密集的網路請求,長鏈接,密集的CPU操作(比如大量的複雜計算)都會使耗電功率增加。此外,耗電量還會被很多其他因素影響,比如用戶在不同光線下使用,iPhone會自動調整屏幕亮度,就會導致耗電量不同;網路狀況(流暢的Wi-Fi還是信號不好的3G)
由於耗電量的影響因素太多,統計出來並不能精準的反應你的APP的性能,所以筆者認為,一般的APP不必把耗電量當作一個優化指標,只要把可能影響耗電量的、可優化的部分盡量優化即可,比如網路請求和CPU操作。畢竟對於大多數APP來說,還談不上耗電太多的問題,需要重點考慮耗電問題的應該是像微信這種用戶重度依賴(人均使用時長)或者是視頻類應用這種耗電大戶。不是說不優化耗電量,而是優化了其他的,耗電量自然就會減少了,單純從這個值來講不好檢測。
首先測量耗電量的時候不能用模擬器,模擬器下得到的電量值是負數,也不能用真機連著電腦debug,因為這個過程本身就在給手機充電。正確的做法是在手機上設置Settings→developer→logging on your device→enable energy logging再開始記錄,一段時間以後再stop,再用手機連接到電腦的instrument上,import log記錄進行分析。
還有就是在代碼中獲取電量值,在特定場景之前、之後檢查電量使用情況,計算差值。電量的計算要有一定的時間長度才可以,不可能是一個函數的前後就有能看得見的變化(要是有這樣的函數也太恐怖了)。
UIDevice.currentDevice.batteryMonitoringEnabled = true; NSLog(@"電量:%f%%",[UIDevice currentDevice].batteryLevel * 100);
Last but not the least
做性能方面的檢測工作時,一定要在真機上測試,而不是模擬器。模擬器的性能是Mac的,跟iPhone不可同日而語,測出來的數據不準也就沒有了意義。比如電池電量這種指標,模擬器下是負數-.-!
還有性能測試要用發布配置,也就是說要用release包,而不是調試模式。因為當用發布環境打包的時候,編譯器會引入一系列提高性能的優化,例如去掉調試符號或者移除並重新組織代碼。想要測試用戶真實的使用情況還是要用跟真實包最最接近的release版。
最好在你支持的設備中性能最差的設備上測試
性能對比實驗要基於完全相同的實驗場景或是取大量真實數據的平均值,其實對於用戶的真實使用場景來說,很難做到完全一樣,可能的影響因素有很多:網路狀況,硬體,系統版本,是否越獄,設備上的可用空間,同時開著的其他app。
最後的最後,通過這次調研,筆者深深的受到教育,在寫代碼的時候一定要考慮對性能的影響,防患於未然。
如果您覺得我們的內容還不錯,就請轉發到朋友圈,和小夥伴一起分享吧~


※50元以內的平價飾品,以能讓你度過一個美美的國慶節!
※怎樣自製面膜補水 如何讓自己的皮膚看起來更水潤
※通過Crowd Layer,利用眾包標註數據集進行深度學習
※國慶快樂!中秋快樂!
※這些衣服便宜,同時時髦高級富有個性,真的
TAG:輕芒 |