Android性能優化(5-10)

2015年伊始,Google發佈了關於Android性能優化典範的專題, 一共16個短視頻,每個3-5分鐘,幫助開發者創建更快更優秀的Android App。課程專題不僅僅介紹了Android系統中有關性能問題的底層工作原理,同時也介紹瞭如何通過工具來找出性能問題以及提升性能的建議。主要從三個 方面展開,Android的渲染機制,內存與GC,電量優化。下面是對這些問題和建議的總結梳理

Why 60fps?

我們通常都會提到60fps與16ms,可是知道爲何會是以程序是否達到60fps來作爲App性能的衡量標準嗎?這是因爲人眼與大腦之間的協作無法感知超過60fps的畫面更新。

12fps大概類似手動快速翻動書籍的幀率,這明顯是可以感知到不夠順滑的。24fps使得人眼感知的是連續線性的運動,這其實是歸功於運動模糊的效果。24fps是電影膠圈通常使用的幀率,因爲這個幀率已經足夠支撐大部分電影畫面需要表達的內容,同時能夠最大的減少費用支出。但是低於30fps是無法順暢表現絢麗的畫面內容的,此時就需要用到60fps來達到想要的效果,當然超過60fps是沒有必要的。

開發app的性能目標就是保持60fps,這意味着每一幀你只有16ms=1000/60的時間來處理所有的任務。

Android, UI and the GPU

瞭解Android是如何利用GPU進行畫面渲染有助於我們更好的理解性能問題。那麼一個最實際的問題是:activity的畫面是如何繪製到屏幕上的?那些複雜的XML佈局文件又是如何能夠被識別並繪製出來的?
這裏寫圖片描述
Resterization柵格化是繪製那些Button,Shape,Path,String,Bitmap等組件最基礎的操作。它把那些組件拆分到不同的像素上進行顯示。這是一個很費時的操作,GPU的引入就是爲了加快柵格化的操作

CPU負責把UI組件計算成Polygons,Texture紋理,然後交給GPU進行柵格化渲染。
這裏寫圖片描述
然而每次從CPU轉移到GPU是一件很麻煩的事情,所幸的是OpenGL ES可以把那些需要渲染的紋理Hold在GPU Memory裏面,在下次需要渲染的時候直接進行操作。所以如果你更新了GPU所hold住的紋理內容,那麼之前保存的狀態就丟失了。

在Android裏面那些由主題所提供的資源,例如Bitmaps,Drawables都是一起打包到統一的Texture紋理當中,然後再傳遞到GPU裏面,這意味着每次你需要使用這些資源的時候,都是直接從紋理裏面進行獲取渲染的。當然隨着UI組件的越來越豐富,有了更多演變的形態。例如顯示圖片的時候,需要先經過CPU的計算加載到內存中,然後傳遞給GPU進行渲染。文字的顯示更加複雜,需要先經過CPU換算成紋理,然後再交給GPU進行渲染,回到CPU繪製單個字符的時候,再重新引用經過GPU渲染的內容。動畫則是一個更加複雜的操作流程。

爲了能夠使得App流暢,我們需要在每一幀16ms以內處理完所有的CPU與GPU計算,繪製,渲染等等操作。

Invalidations, Layouts, and Performance

順滑精妙的動畫是app設計裏面最重要的元素之一,這些動畫能夠顯著提升用戶體驗。下面會講解Android系統是如何處理UI組件的更新操作的。

通常來說,Android需要把XML佈局文件轉換成GPU能夠識別並繪製的對象。這個操作是在DisplayList的幫助下完成的。DisplayList持有所有將要交給GPU繪製到屏幕上的數據信息。

在某個View第一次需要被渲染時,DisplayList會因此而被創建,當這個View要顯示到屏幕上時,我們會執行GPU的繪製指令來進行渲染。如果你在後續有執行類似移動這個View的位置等操作而需要再次渲染這個View時,我們就僅僅需要額外操作一次渲染指令就夠了。然而如果你修改了View中的某些可見組件,那麼之前的DisplayList就無法繼續使用了,我們需要回頭重新創建一個DisplayList並且重新執行渲染指令並更新到屏幕上。

需要注意的是:任何時候View中的繪製內容發生變化時,都會重新執行創建DisplayList,渲染DisplayList,更新到屏幕上等一系列操作。這個流程的表現性能取決於你的View的複雜程度,View的狀態變化以及渲染管道的執行性能。舉個例子,假設某個Button的大小需要增大到目前的兩倍,在增大Button大小之前,需要通過父View重新計算並擺放其他子View的位置。修改View的大小會觸發整個HierarcyView的重新計算大小的操作。如果是修改View的位置則會觸發HierarchView重新計算其他View的位置。如果佈局很複雜,這就會很容易導致嚴重的性能問題。我們需要儘量減少Overdraw。
這裏寫圖片描述
我們可以通過前面介紹的Monitor GPU Rendering來查看渲染的表現性能如何,另外也可以通過開發者選項裏面的Show GPU view updates來查看視圖更新的操作,最後我們還可以通過HierarchyViewer這個工具來查看佈局,使得佈局儘量扁平化,移除非必需的UI組件,這些操作能夠減少Measure,Layout的計算時間。

Overdraw, Cliprect, QuickReject

引起性能問題的一個很重要的方面是因爲過多複雜的繪製操作。我們可以通過工具來檢測並修復標準UI組件的Overdraw問題,但是針對高度自定義的UI組件則顯得有些力不從心。

有一個竅門是我們可以通過執行幾個APIs方法來顯著提升繪製操作的性能。前面有提到過,非可見的UI組件進行繪製更新會導致Overdraw。例如Nav Drawer從前置可見的Activity滑出之後,如果還繼續繪製那些在Nav Drawer裏面不可見的UI組件,這就導致了Overdraw。爲了解決這個問題,Android系統會通過避免繪製那些完全不可見的組件來儘量減少Overdraw。那些Nav Drawer裏面不可見的View就不會被執行浪費資源。
這裏寫圖片描述
但是不幸的是,對於那些過於複雜的自定義的View(重寫了onDraw方法),Android系統無法檢測具體在onDraw裏面會執行什麼操作,系統無法監控並自動優化,也就無法避免Overdraw了。但是我們可以通過canvas.clipRect()來幫助系統識別那些可見的區域。這個方法可以指定一塊矩形區域,只有在這個區域內纔會被繪製,其他的區域會被忽視。這個API可以很好的幫助那些有多組重疊組件的自定義View來控制顯示的區域。同時clipRect方法還可以幫助節約CPU與GPU資源,在clipRect區域之外的繪製指令都不會被執行,那些部分內容在矩形區域內的組件,仍然會得到繪製。
這裏寫圖片描述
除了clipRect方法之外,我們還可以使用canvas.quickreject()來判斷是否沒和某個矩形相交,從而跳過那些非矩形區域內的繪製操作。做了那些優化之後,我們可以通過上面介紹的Show GPU Overdraw來查看效果

Memory Churn and performance

雖然Android有自動管理內存的機制,但是對內存的不恰當使用仍然容易引起嚴重的性能問題。在同一幀裏面創建過多的對象是件需要特別引起注意的事情。

Android系統裏面有一個Generational Heap Memory的模型,系統會根據內存中不同的內存數據類型分別執行不同的GC操作。例如,最近剛分配的對象會放在Young Generation區域,這個區域的對象通常都是會快速被創建並且很快被銷燬回收的,同時這個區域的GC操作速度也是比Old Generation區域的GC操作速度更快的。
這裏寫圖片描述
除了速度差異之外,執行GC操作的時候,任何線程的任何操作都會需要暫停,等待GC操作完成之後,其他操作才能夠繼續運行。
這裏寫圖片描述
通常來說,單個的GC並不會佔用太多時間,但是大量不停的GC操作則會顯著佔用幀間隔時間(16ms)。如果在幀間隔時間裏面做了過多的GC操作,那麼自然其他類似計算,渲染等操作的可用時間就變得少了。

導致GC頻繁執行有兩個原因:

  • Memory Churn內存抖動,內存抖動是因爲大量的對象被創建又在短時間內馬上被釋放。
  • 瞬間產生大量的對象會嚴重佔用Young Generation的內存區域,當達到閥值,剩餘空間不夠的時候,也會觸發GC。即使每次分配的對象佔用了很少的內存,但是他們疊加在一起會增加Heap的壓力,從而觸發更多其他類型的GC。這個操作有可能會影響到幀率,並使得用戶感知到性能問題。

這裏寫圖片描述
解決上面的問題有簡潔直觀方法,如果你在Memory Monitor裏面查看到短時間發生了多次內存的漲跌,這意味着很有可能發生了內存抖動
這裏寫圖片描述
同時我們還可以通過Allocation Tracker來查看在短時間內,同一個棧中不斷進出的相同對象。這是內存抖動的典型信號之一。

當你大致定位問題之後,接下去的問題修復也就顯得相對直接簡單了。例如,

你需要避免在for循環裏面分配對象佔用內存,需要嘗試把對象的創建移到循環體之外,自定義View中的onDraw方法也需要引起注意,每次屏幕發生繪製以及動畫執行過程中,onDraw方法都會被調用到,避免在onDraw方法裏面執行復雜的操作,避免創建對象。對於那些無法避免需要創建對象的情況,我們可以考慮對象池模型,通過對象池來解決頻繁創建與銷燬的問題,但是這裏需要注意結束使用之後,需要手動釋放對象池中的對象。

Garbage Collection in Android

JVM的回收機制給開發人員帶來很大的好處,不用時刻處理對象的分配與回收,可以更加專注於更加高級的代碼實現。相比起Java,C與C++等語言具備更高的執行效率,他們需要開發人員自己關注對象的分配與回收,但是在一個龐大的系統當中,還是免不了經常發生部分對象忘記回收的情況,這就是內存泄漏。

原始JVM中的GC機制在Android中得到了很大程度上的優化。Android裏面是一個三級Generation的內存模型,最近分配的對象會存放在Young Generation區域,當這個對象在這個區域停留的時間達到一定程度,它會被移動到Old Generation,最後到Permanent Generation區域。
這裏寫圖片描述
每一個級別的內存區域都有固定的大小,此後不斷有新的對象被分配到此區域,當這些對象總的大小快達到這一級別內存區域的閥值時,會觸發GC的操作,以便騰出空間來存放其他新的對象。
這裏寫圖片描述
前面提到過每次GC發生的時候,所有的線程都是暫停狀態的。GC所佔用的時間和它是哪一個Generation也有關係,Young Generation的每次GC操作時間是最短的,Old Generation其次,Permanent Generation最長。執行時間的長短也和當前Generation中的對象數量有關,遍歷查找20000個對象比起遍歷50個對象自然是要慢很多的。

雖然Google的工程師在儘量縮短每次GC所花費的時間,但是特別注意GC引起的性能問題還是很有必要。如果不小心在最小的for循環單元裏面執行了創建對象的操作,這將很容易引起GC並導致性能問題。通過Memory Monitor我們可以查看到內存的佔用情況,每一次瞬間的內存降低都是因爲此時發生了GC操作,如果在短時間內發生大量的內存上漲與降低的事件,這說明很有可能這裏有性能問題。我們還可以通過Heap and Allocation Tracker工具來查看此時內存中分配的到底有哪些對象

鳴謝

巴山夜雨
MrDogy的生活意見
MrDogy的生活意見

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