iOS效能最佳化:Instruments使用實戰
採用Instruments 來分析整個應用程式的效能.發現很多有意思的點,以及效能最佳化和一些分析效能消耗的技巧,小結如下。
Instruments提示
關於Instruments官方有一個很有用的使用者使用Guide,當然如果不習慣官方英文可以在這裡找到中文本翻譯版本PDF參閱.Instruments 確實是一個很強大的工具,用它來收集關於一個或多個系統進程的效能和行為的資料極為方便,並能及時跟蹤隨著時間產生的資料.還可以廣泛收集不同類型的資料.關於Instrument工具基本使用不在贅述.如下重點說明一些提示.
1.概覽
工具通過Xcode工具列中Product->Profile可以啟動,啟動後介面如下:
Instrument概覽[via by chenkai]
當點擊Time Profiler應用程式開始運行後.就能擷取到整個應用程式運行消耗時間分布和百分比.為了保證資料分析在統一使用情境真實行有如下點需要注意:
在開始進行應用程式效能分析的時候,一定要使用真機,模擬器運行在Mac上,然而Mac上的CPU往往比iOS裝置要快。相反,Mac上的GPU和iOS裝置的完全不一樣,模擬器不得已要在軟體層面(CPU)類比裝置的GPU,這意味著GPU相關的操作在模擬器上啟動並執行更慢,尤其是使用CAEAGLLayer來寫一些OpenGL的代碼時候. 這就導致模擬器效能資料和使用者真機使用效能資料相去甚運.
另外在開始效能分析前另外一件重要的事情是,應用程式運行一定要發布配置 而不是調試配置.
在發布環境打包的時候,編譯器會引入一系列提高效能的最佳化,例如去掉偵錯符號或者移除並重新組織代碼.另iOS引入一種"Watch Dog"[看門狗]機制.不同的情境下,“看門狗”會監測應用的效能。如果超出了該情境所規定的已耗用時間,“看門狗”就會強制終結這個應用的進程.開發人員可以crashlog看到對應的日誌.但Xcode在調試配置下會禁用"Watch Dog".
下圖為兩種調試方式,一般來說,我們會用前者調試CPU的健全狀態
2.Time Profiler
選擇Time Profiler啟動.
time profile時間分析工具用來檢測應用CPU的使用方式.可以看到應用程式中各個方法正在消耗CPU時間.使用大量CPU不一定是個問題.類似我們用戶端中不同情境的天氣動畫[類似大雨]的路徑就對CPU依賴就非常高,動畫本身也是非常苛刻且耗費資源較多的任務.
點擊Record 開始運行.
Time Profile 分析介面[via by chenkai]
剛開始我們拿到分析資料時往往是這樣的:
效能資料[via by chenkai]
這裡顯示的是執行代碼完整路徑,其中系統和應用本身一些調用路徑完全揉捏在一起.完全看不到我們關心的應用程式中實際代碼執行耗時和代碼路徑實際所在位置.簡單的方式可以快速勾選右邊Call Tree中Separate Thread和Hide System Libraries兩個選項[後面會解釋選項作用]:
拆分後效能資料[via by chenkai]
可以看到直接能夠看到應用程式各個方法調用耗時直接路徑,剔除掉了系統相關方法和反向調用樹路徑.清爽很多.如果覺得這還不夠直觀,選擇任意一個耗時方法分支[這裡選擇WeatherViewController viewDidLoad]雙擊進入會看到:
代碼&耗時詳情
可以直接定位到viewDidLoad的代碼,也可以直觀的看到改方法下調用其他方法耗時的時間.類似[self loadCityWeatherScroollerView]耗時是121x,x既耗時單位這裡為ms毫秒.當然如果直接在Instrument找到問題覺得不方便修改,可以直接點擊右上方Xcode按鈕會直接定位Xcode對應調用方法入口.這樣很容易能夠快速定位代碼佔用CPU最多的方法.也可以開啟Xcode快速修改並重新運行Profile來看修改後耗時前後對比.簡單便捷.
這裡對右側call tree選項有必要做一下說明[官方user guide翻譯]:
Separate By Thread:線程分離,只有這樣才能在調用路徑中能夠清晰看到佔用CPU最大的線程.
Invert Call Tree:從上到下跟蹤堆棧資訊.這個選項可以快捷的看到方法調用路徑最深方法佔用CPU耗時,比如FuncA{FunB{FunC}},勾選後堆棧以C->B->A把調用層級最深的C顯示最外面.
Hide Missing Symbols:如果dSYM無法找到你的APP或者調用系統架構的話,那麼表中將看到調用方法名只能看到16進位的數值,勾選這個選項則可以隱藏這些符號,便於簡化分析資料.
Hide System Libraries:這個就更有用了,勾選後耗時調用路徑只會顯示app耗時的代碼,效能分析普遍我們都比較關係自己代碼的耗時而不是系統的.基本是必選項.注意有些代碼耗時也會納入系統層級,可以進行勾選前後前後對執行路徑進行比對會非常有用.
關於其他方法不再贅述.
效能分析&代碼最佳化
我們這次效能最佳化主要針對如下兩個使用情境:
A:應用程式第一次啟動到進入天氣首頁的時間.
B:從後台切到前台天氣首頁佔用時間.
在還沒有拿到效能分析資料之前,一直認為第一次啟動耗時主要浪費AppDelegate中第三方架構初始化上[類似WeiBo&WeChat 相關SDK初始化調用].當我們拿到實際效能資料耗時佔用比時發現實際情況並非如此:
啟動耗時
如上可以看到應用程式啟動初始化工作主要會在MJAppDelegate如下兩個方法展開:willFinishLaunchingWithOptions和didFinishLaunchingWithOptions,其中第三方架構初始化工作主要是willFinishLaunchingWithOptions中完成的.而實際情況耗時佔比非常小.基本可以忽略不計.
而我們要最佳化兩個啟動時間情境,不同在於.第一次進入應用需要經過新手教程、添加城市、請求城市資料、解析資料、初始化天氣首頁UI元素並載入情境動畫. 而從後台進入時則從本機存放區DT檔案中解析天氣資料、初始化天氣首頁UI元素並載入天氣動畫.
1.NSDateFormatter問題凸顯
針對這點重點分析應用啟動&天氣首頁耗時. 在AB兩個情境均發現載入首頁元素髮現如下問題:
NSDate(TimeAgo)getDateStrByTimeZone耗時
繼續跟蹤發現:
NSDate耗時
在AB兩個情境裡均出現載入MJLineChartView 和 TendencyChartView 時擷取時區對應時間上耗時較大.而耗時主要在getDateStrByTimeZone這個方法調用上.
其中建立一個NSDateFormatter對象平均耗時33ms左右 而設定NSDateFormatter的3個屬性平均耗時也在30ms左右.因為首頁24小時天氣和未來幾天預報中.需要for迴圈中遍曆資料,導致這個方法別重複調用多次,則消耗時間不斷疊加.
針對這個問題:
NSDateFormatter對象本身初始化很慢,同樣還有NSCalendar也是如此.然而在一些使用情境中不可避免要使用他們,比如Json資料解析中.使用這個對象同時避免其效能開銷帶來效能開銷,一般比較好的方式是通過添加屬性(推薦)或建立靜態變數保持該對象只被初始化一次,而被多次複用.不得不值得一提的是設定一個NSDateFormatter屬性速度差不多是和建立新的執行個體對象一樣慢。
針對NSDateFormatter時間開銷出了重用對象外,盡量避免採用其處理多個日期格式.當然針對日期格式處理如果需要提高更多速度,可以直接採用C,可以採用第三方庫來規避這個問題..
2.UIImage緩衝取捨
UIImage使用
在Main Thread中發現不同動畫情境中Image IO 開銷和耗時所佔比例均不一,在UIImage元素較多總體疊加耗時也會佔用一定比例.記憶體開銷也會明顯增高.
UIImage載入圖片方式一般有兩種:
A:imagedNamed初始化
B:imageWithContentsOfFile初始化
二者不同之處在於,imageNamed預設載入圖片成功後會記憶體中緩衝圖片,這個方法用一個指定的名字在系統緩衝中尋找並返回一個圖片對象.如果緩衝中沒有找到相應的圖片對象,則從指定地方載入圖片然後緩衝對象,並返回這個圖片對象.而imageWithContentsOfFile則僅只載入圖片,不緩衝.
大量使用imageNamed方式會在不需要緩衝的地方額外增加開銷CPU的時間來做這件事.當應用程式需要載入一張比較大的圖片並且使用一次性,那麼其實是沒有必要去緩衝這個圖片的,用imageWithContentsOfFile是最為經濟的方式,這樣不會因為UIImage元素較多情況下,CPU會被逐個分散在不必要緩衝上浪費過多時間.
使用情境需要編程時,應該根據實際應用情境加以區分,UIimage雖小,但使用元素較多問題會有所凸顯.
3.天氣首頁載入策略
在AB兩種情境把效能資料對比分析發現:
天氣首頁WeatherView初始化耗時一直300ms-450ms之間,佔據首頁耗時很大一部分.且一直固定的開銷.佔據Main Thread3分之一.
而使用者進入最先看到是天氣首頁上半部分:
而下半部分需要滾動才能看到下半部分.且不一定觸發:
而現在整個首頁View的初始化和更新全部放到主線程來做.其中WeatherInfoView updateAllInfo方法更新耗時最長.更多的view意味著更多的渲染,也就是意味更多的CPU和記憶體消耗,對於我們天氣首頁在UIScrollView裡邊嵌套了很多view更是如此。
而針對這種情況不要在主線程承載過多的操作.uikit渲染,使用者輸入回應都需要主進程上完成.主線程被意外block或者載入響應耗時過多都會影響到使用者體驗.而針對資源消耗過大操作,處理原則是最小化主線程的CPU佔用,將工作“搬離”主線程, 不要阻塞主線程.類似本地一些IO完全移到其他線程來做.
調試time profiler過程中發現,即使佔用了很少的CPU時間(如果你在Time Profiler中看到這些的資料),也可能會阻塞主線程。磁碟、網路、Lock、dispatch_sync以及向其它進程/線程發送訊息都會阻塞主線 程。Time Profiler只能檢測出佔用CPU過多的堆棧,但檢測不了這些IO的問題.很奇怪.在System Trace裡面突然發現了CPU Time很低,但Wait Time很高的調用,說明在主線程處理I/O已經嚴重損害了app的效能,這個時候考慮把這個操作最佳化了.
而針對我們應用首頁ui中多個view,在載入策略完全可以採用多線程進行同步載入,只把上半部分放在主線程中載入,下班可以同時開一個線程進行同步載入.這樣可以大大降低組線程初始化和更新時間,當首頁初始化完畢已經呈現是,下半部分其實已經另外一個線程處理完畢.
另外針對單個view 盡量不要在viewWillAppear費時的操作,viewWillAppear在 view 顯示之前被調用,出於效率考慮,在這個方法中不要處理複雜費時的事情;只應該在這個方法設定 view 的顯示內容之類的簡單事情,比如背景色,字型等。不然,使用者會明顯感覺到 view 顯示遲鈍.
4:應用首次載入時間
應用初次開機載入操作:
首次載入坐了如下操作:
A: 連結和載入:可以在Time Profile中顯示dyld載入庫函數,庫會被映射到地址空間,同時完成綁定以及靜態初始化.
B: UIKit初始化:如果應用的Root View Controller是由XIB實現的,也會在啟動時被初始化.
C: 應用回調:調用UIApplicationDeleagte的回調:application:didFinishLaunchingWithOptions.
D: 第一次Core Animation調用:在啟動後的方法-[UIApplication _resportAppLaunchFinished]中調用CA::Transaction::commit實現第一幀畫面的繪製.
應用程式首次載入中啟動方法willFinishLaunchingWithOptions和didFinishLaunchingWithOptions只做應用程式初次開機必須的要操作,而針對_dyid_start在初始化庫framework函數的操作.不必要的Framework不要連結,避免首次載入耗時.
小結如上.很多地方代碼調用和底層機制看的不是特別明白,整理總結關於最佳化部分實在有限,如上僅供各位參考.另外Instruments確實是把分析代碼利器.目前沒有任何一個第三方工具可以去替代.推薦各位使用.