最近採用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”。
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這個方法呼叫上。
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更新耗時
天氣首頁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確實是把分析程式碼利器。目前沒有任何一個第三方工具可以去替代。推薦各位使用。