iOS性能優化之內存管理:Analyze、Leaks、Allocations的使用和案例代碼

最近採用Instruments 來分析整個應用程序的性能.發現很多有意思的點,以及性能優化和一些分析性能消耗的技巧.小結如下.

Instruments使用技巧

關於Instruments官方有一個很有用的用戶使用Guide,當然如果不習慣官方英文可以在這裏找到中文本翻譯版本PDF參閱.Instruments 確實是一個很強大的工具,用它來收集關於一個或多個系統進程的性能和行爲的數據極爲方便,並能及時跟蹤隨着時間產生的數據.還可以廣泛收集不同類型的數據.關於Instrument工具基本使用不在贅述.如下重點說明一些使用技巧.

1.概覽

工具通過Xcode工具欄中Product->Profile可以啓動,啓動後界面如下:

100

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 開始運行.

100

Time Profile 分析界面[via by chenkai]

剛開始我們拿到分析數據時往往是這樣的:

100

性能數據[via by chenkai]

這裏顯示的是執行代碼完整路徑,其中系統和應用本身一些調用路徑完全揉捏在一起.完全看不到我們關心的應用程序中實際代碼執行耗時和代碼路徑實際所在位置.簡單的方式可以快速勾選右邊Call Tree中Separate Thread和Hide System Libraries兩個選項[後面會解釋選項作用]:

100

拆分後性能數據[via by chenkai]

可以看到直接能夠看到應用程序各個方法調用耗時直接路徑,剔除掉了系統相關方法和反向調用樹路徑.清爽很多.如果覺得這還不夠直觀,選擇任意一個耗時方法分支[這裏選擇WeatherViewController viewDidLoad]雙擊進入會看到:

100

代碼&耗時詳情

可以直接定位到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初始化調用].當我們拿到實際性能數據耗時佔用比時發現實際情況並非如此:

100

啓動耗時

如上可以看到應用程序啓動初始化工作主要會在MJAppDelegate如下兩個方法展開:willFinishLaunchingWithOptionsdidFinishLaunchingWithOptions,其中第三方框架初始化工作主要是willFinishLaunchingWithOptions中完成的.而實際情況耗時佔比非常小.基本可以忽略不計.

而我們要優化兩個啓動時間場景,不同在於.第一次進入應用需要經過新手教程、添加城市、請求城市數據、解析數據、初始化天氣首頁UI元素並加載場景動畫. 而從後臺進入時則從本地存儲DT文件中解析天氣數據、初始化天氣首頁UI元素並加載天氣動畫.

1.NSDateFormatter問題凸顯

針對這點重點分析應用啓動&天氣首頁耗時. 在AB兩個場景均發現加載首頁元素髮現如下問題:

100

NSDate(TimeAgo)getDateStrByTimeZone耗時

繼續跟蹤發現:

100

NSDate耗時

在AB兩個場景裏均出現加載MJLineChartView 和 TendencyChartView 時獲取時區對應時間上耗時較大.而耗時主要在getDateStrByTimeZone這個方法調用上.

100

getDateStrByTimeZone方法

其中創建一個NSDateFormatter對象平均耗時33ms左右 而設置NSDateFormatter的3個屬性平均耗時也在30ms左右.因爲首頁24小時天氣和未來幾天預報中.需要for循環中遍歷數據,導致這個方法別重複調用多次,則消耗時間不斷疊加.

針對這個問題:

NSDateFormatter對象本身初始化很慢,同樣還有NSCalendar也是如此.然而在一些使用場景中不可避免要使用他們,比如Json數據解析中.使用這個對象同時避免其性能開銷帶來性能開銷,一般比較好的方式是通過添加屬性(推薦)或創建靜態變量保持該對象只被初始化一次,而被多次複用.不得不值得一提的是設置一個NSDateFormatter屬性速度差不多是和創建新的實例對象一樣慢!

添加屬性方式如下:

100

屬性方式

針對NSDateFormatter時間開銷出了重用對象外,儘量避免採用其處理多個日期格式.當然針對日期格式處理如果需要提高更多速度,可以直接採用C,可以採用第三方庫來規避這個問題..

2.UIImage緩存取捨

在項目代碼中看到大量使用如下代碼:

100

UIImage使用

在Main Thread中發現不同動畫場景中Image IO 開銷和耗時所佔比例均不一,在UIImage元素較多總體疊加耗時也會佔用一定比例.內存開銷也會明顯增高.

UIImage加載圖片方式一般有兩種:

A:imagedNamed初始化

B:imageWithContentsOfFile初始化

二者不同之處在於,imageNamed默認加載圖片成功後會內存中緩存圖片,這個方法用一個指定的名字在系統緩存中查找並返回一個圖片對象.如果緩存中沒有找到相應的圖片對象,則從指定地方加載圖片然後緩存對象,並返回這個圖片對象.

而imageWithContentsOfFile則僅只加載圖片,不緩存.

大量使用imageNamed方式會在不需要緩存的地方額外增加開銷CPU的時間來做這件事.當應用程序需要加載一張比較大的圖片並且使用一次性,那麼其實是沒有必要去緩存這個圖片的,用imageWithContentsOfFile是最爲經濟的方式,這樣不會因爲UIImage元素較多情況下,CPU會被逐個分散在不必要緩存上浪費過多時間.

使用場景需要編程時,應該根據實際應用場景加以區分,UIimage雖小,但使用元素較多問題會有所凸顯.

3.天氣首頁加載策略

在AB兩種場景把性能數據對比分析發現:

100

天氣首頁WeatherView更新耗時

天氣首頁WeatherView初始化耗時一直300ms-450ms之間,佔據首頁耗時很大一部分.且一直固定的開銷.佔據Main Thread3分之一.

而用戶進入最先看到是天氣首頁上半部分:

100

上半部分

而下半部分需要滾動才能看到下半部分.且不一定觸發:

100

下半部分

而現在整個首頁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:應用首次加載時間

應用首次啓動加載操作:

100

首次加載

首次加載坐了如下操作:

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確實是把分析代碼利器.目前沒有任何一個第三方工具可以去替代.推薦各位使用.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章