2015年伊始,Google發布了關于Android性能優化典范的專題,1共16個短視頻,每一個3⑸分鐘,幫助開發者創建更快更優秀的Android App。課程專題不單單介紹了Android系統中有關性能問題的底層工作原理,同時也介紹了如何通過工具來找出性能問題和提升性能的建議。主要從3個方面展開,Android的渲染機制,內存與GC,電量優化。下面是對這些問題和建議的總結梳理。
1) Render Performance
大多數用戶感知到的卡頓等性能問題的最主要本源都是由于渲染性能。從設計師的角度,他們希望App能夠有更多的動畫,圖片等時尚元夙來實現流暢的用戶體驗。但是Android系統很有可能沒法及時完成那些復雜的界面渲染操作。Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,如果每次渲染都成功,這樣就可以夠到達流暢的畫面所需要的60fps,為了能夠實現60fps,這意味著程序的大多數操作都必須在16ms內完成。
如果你的某個操作花費時間是24ms,系統在得到VSYNC信號的時候就沒法進行正常渲染,這樣就產生了丟幀現象。那末用戶在32ms內看到的會是同1幀畫面。
用戶容易在UI履行動畫或滑動ListView的時候感知到卡頓不流暢,是由于這里的操作相對復雜,容易產生丟幀的現象,從而感覺卡頓。有很多緣由可以致使丟幀,或許是由于你的layout太過復雜,沒法在16ms內完成渲染,有多是由于你的UI上有層疊太多的繪制單元,還有多是由于動畫履行的次數過量。這些都會致使CPU或GPU負載太重。
我們可以通過1些工具來定位問題,比如可使用HierarchyViewer來查找Activity中的布局是不是過于復雜,也能夠使用手機設置里面的開發者選項,打開Show GPU Overdraw等選項進行視察。你還可使用TraceView來視察CPU的履行情況,更加快捷的找到性能瓶頸。
2) Understanding Overdraw
Overdraw(過度繪制)描寫的是屏幕上的某個像素在同1幀的時間內被繪制了屢次。在多層次的UI結構里面,如果不可見的UI也在做繪制的操作,這就會致使某些像素區域被繪制了屢次。這就浪費大量的CPU和GPU資源。
當設計上尋求更華麗的視覺效果的時候,我們就容易墮入采取愈來愈多的層疊組件來實現這類視覺效果的怪圈。這很容易致使大量的性能問題,為了取得最好的性能,我們必須盡可能減少Overdraw的情況產生。
榮幸的是,我們可以通過手機設置里面的開發者選項,打開Show GPU Overdraw的選項,可以視察UI上的Overdraw情況。
藍色,淡綠,淡紅,深紅代表了4種不同程度的Overdraw情況,我們的目標就是盡可能減少紅色Overdraw,看到更多的藍色區域。
Overdraw有時候是由于你的UI布局存在大量堆疊的部份,還有的時候是由于非必須的堆疊背景。例如某個Activity有1個背景,然后里面的Layout又有自己的背景,同時子View又分別有自己的背景。僅僅是通過移除非必須的背景圖片,這就可以夠減少大量的紅色Overdraw區域,增加藍色區域的占比。這1措施能夠顯著提升程序性能。
3) Understanding VSYNC
為了理解App是如何進行渲染的,我們必須了解手機硬件是如何工作,那末就必須理解甚么是VSYNC。
在講授VSYNC之前,我們需要了解兩個相干的概念:
GPU會獲得圖形數據進行渲染,然后硬件負責把渲染后的內容顯現到屏幕上,他們二者不停的進行協作。
不幸的是,刷新頻率和幀率其實不是總能夠保持相同的節奏。如果產生幀率與刷新頻率不1致的情況,就會容易出現Tearing的現象(畫面上下兩部份顯示內容產生斷裂,來自不同的兩幀數據產生堆疊)。
理解圖象渲染里面的兩重與3重緩存機制,這個概念比較復雜,請移步查看Graphics和《了解Android 4.1之3:黃油項目 ―― 運作機理及新鮮玩意》。
通常來講,幀率超過刷新頻率只是1種理想的狀態,在超過60fps(fps――每秒傳輸幀數(Frames Per Second))的情況下,GPU所產生的幀數據會由于等待VSYNC的刷新信息而被Hold住,這樣能夠保持每次刷新都有實際的新的數據可以顯示。但是我們遇到更多的情況是幀率小于刷新頻率。
在這類情況下,某些幀顯示的畫面內容就會與上1幀的畫面相同。糟的事情是,幀率從超過60fps突然掉到60fps以下,這樣就會產生LAG,JANK,HITCHING等卡頓掉幀的不順滑的情況。這也是用戶感受不好的緣由所在。
4) Tool:Profile GPU Rendering
性能問題如此的麻煩,幸虧我們可以有工具來進行調試。打開手機里面的開發者選項,選擇Profile GPU Rendering,選中On screen as bars的選項。
選擇了這樣以后,我們可以在手機畫面上看到豐富的GPU繪制圖形信息,分別關于StatusBar,NavBar,激活的程序Activity區域的GPU Rending信息。
隨著界面的刷新,界面上會轉動顯示垂直的柱狀圖來表示每幀畫面所需要渲染的時間,柱狀圖越高表示花費的渲染時間越長。
中間有1根綠色的橫線,代表16ms,我們需要確保每幀花費的總時間都低于這條橫線,這樣才能夠避免出現卡頓的問題。
每條柱狀線都包括3部份,藍色代表丈量繪制Display List的時間,紅色代表OpenGL渲染Display List所需要的時間,黃色代表CPU等待GPU處理的時間。
5) Why 60fps?
我們通常都會提到60fps與16ms,可是知道為什么會是以程序是不是到達60fps來作為App性能的衡量標準嗎?這是由于人眼與大腦之間的協作沒法感知超過60fps的畫面更新。
12fps大概類似手動快速翻動書籍的幀率,這明顯是可以感知到不夠順滑的。24fps使得人眼感知的是連續線性的運動,這實際上是歸功于運動模糊的效果。24fps是電影膠圈通常使用的幀率,由于這個幀率已足夠支持大部份電影畫面需要表達的內容,同時能夠最大的減少費用支出。但是低于30fps是沒法順暢表現絢麗的畫面內容的,此時就需要用到60fps來到達想要的效果,固然超過60fps是沒有必要的。
開發app的性能目標就是保持60fps,這意味著每幀你只有16ms=1000/60的時間來處理所有的任務。
6) Android, UI and the GPU
了解Android是如何利用GPU進行畫面渲染有助于我們更好的理解性能問題。那末1個最實際的問題是:activity的畫面是如何繪制到屏幕上的?那些復雜的XML布局文件又是如何能夠被辨認并繪制出來的?
Resterization柵格化是繪制那些Button,Shape,Path,String,Bitmap等組件最基礎的操作。它把那些組件拆分到不同的像素上進行顯示。這是1個很費時的操作,GPU的引入就是為了加快柵格化的操作。
CPU負責把UI組件計算成Polygons,Texture紋理,然后交給GPU進行柵格化渲染。
但是每次從CPU轉移到GPU是1件很麻煩的事情,所幸的是OpenGL ES可以把那些需要渲染的紋理Hold在GPU Memory里面,在下次需要渲染的時候直接進行操作。所以如果你更新了GPU所hold住的紋理內容,那末之前保存的狀態就丟失了。
在Android里面那些由主題所提供的資源,例如Bitmaps,Drawables都是1起打包到統1的Texture紋應當中,然后再傳遞到GPU里面,這意味著每次你需要使用這些資源的時候,都是直接從紋理里面進行獲得渲染的。固然隨著UI組件的愈來愈豐富,有了更多演化的形態。例如顯示圖片的時候,需要先經過CPU的計算加載到內存中,然后傳遞給GPU進行渲染。文字的顯示更加復雜,需要先經過CPU換算成紋理,然后再交給GPU進行渲染,回到CPU繪制單個字符的時候,再重新援用經過GPU渲染的內容。動畫則是1個更加復雜的操作流程。
為了能夠使得App流暢,我們需要在每幀16ms之內處理完所有的CPU與GPU計算,繪制,渲染等等操作。
7) Invalidations, Layouts, and Performance
順滑精巧的動畫是app設計里面最重要的元素之1,這些動畫能夠顯著提升用戶體驗。下面會講授Android系統是如何處理UI組件的更新操作的。
通常來講,Android需要把XML布局文件轉換成GPU能夠辨認并繪制的對象。這個操作是在DisplayList的幫助下完成的。DisplayList持有所有將要交給GPU繪制到屏幕上的數據信息。
在某個View第1次需要被渲染時,DisplayList會因此而被創建,當這個View要顯示到屏幕上時,我們會履行GPU的繪制指令來進行渲染。如果你在后續有履行類似移動這個View的位置等操作而需要再次渲染這個View時,我們就僅僅需要額外操作1次渲染指令就夠了。但是如果你修改了View中的某些可見組件,那末之前的DisplayList就沒法繼續使用了,我們需要回頭重新創建1個DisplayList并且重新履行渲染指令并更新到屏幕上。
需要注意的是:任什么時候候View中的繪制內容產生變化時,都會重新履行創建DisplayList,渲染DisplayList,更新到屏幕上等1系列操作。這個流程的表現性能取決于你的View的復雜程度,View的狀態變化和渲染管道的履行性能。舉個例子,假定某個Button的大小需要增大到目前的兩倍,在增大Button大小之前,需要通過父View重新計算并擺放其他子View的位置。修改View的大小會觸發全部HierarcyView的重新計算大小的操作。如果是修改View的位置則會觸發HierarchView重新計算其他View的位置。如果布局很復雜,這就會很容易致使嚴重的性能問題。我們需要盡可能減少Overdraw。
我們可以通過前面介紹的Monitor GPU Rendering來查看渲染的表現性能如何,另外也能夠通過開發者選項里面的Show GPU view updates來查看視圖更新的操作,最后我們還可以通過HierarchyViewer這個工具來查看布局,使得布局盡可能扁平化,移除非必須的UI組件,這些操作能夠減少Measure,Layout的計算時間。
8) Overdraw, Cliprect, QuickReject
引發性能問題的1個很重要的方面是由于過量復雜的繪制操作。我們可以通過工具來檢測并修復標準UI組件的Overdraw問題,但是針對高度自定義的UI組件則顯得有些力不從心。
有1個訣竅是我們可以通過履行幾個APIs方法來顯著提升繪制操作的性能。前面有提到過,非可見的UI組件進行繪制更新會致使Overdraw。例如Nav Drawer從前置可見的Activity滑出以后,如果還繼續繪制那些在Nav Drawer里面不可見的UI組件,這就致使了Overdraw。為了解決這個問題,Android系統會通過避免繪制那些完全不可見的組件來盡可能減少Overdraw。那些Nav Drawer里面不可見的View就不會被履行浪費資源。
但是不幸的是,對那些過于復雜的自定義的View(重寫了onDraw方法),Android系統沒法檢測具體在onDraw里面會履行甚么操作,系統沒法監控并自動優化,也就沒法避免Overdraw了。但是我們可以通過canvas.clipRect()來幫助系統辨認那些可見的區域。這個方法可以指定1塊矩形區域,只有在這個區域內才會被繪制,其他的區域會被忽視。這個API可以很好的幫助那些有多組堆疊組件的自定義View來控制顯示的區域。同時clipRect方法還可以幫助節儉CPU與GPU資源,在clipRect區域以外的繪制指令都不會被履行,那些部份內容在矩形區域內的組件,依然會得到繪制。
除clipRect方法以外,我們還可使用canvas.quickreject()來判斷是不是沒和某個矩形相交,從而跳過那些非矩形區域內的繪制操作。做了那些優化以后,我們可以通過上面介紹的Show GPU Overdraw來查看效果。
9) Memory Churn and performance
雖然Android有自動管理內存的機制,但是對內存的不恰當使用依然容易引發嚴重的性能問題。在同1幀里面創建過量的對象是件需要特別引發注意的事情。
Android系統里面有1個Generational Heap Memory的模型,系統會根據內存中不同的內存數據類型分別履行不同的GC操作。例如,最近剛分配的對象會放在Young Generation區域,這個區域的對象通常都是會快速被創建并且很快被燒毀回收的,同時這個區域的GC操作速度也是比Old Generation區域的GC操作速度更快的。
除速度差異以外,履行GC操作的時候,任何線程的任何操作都會需要暫停,等待GC操作完成以后,其他操作才能夠繼續運行。
通常來講,單個的GC其實不會占用太多時間,但是大量不停的GC操作則會顯著占用幀間隔時間(16ms)。如果在幀間隔時間里面做了過量的GC操作,那末自然其他類似計算,渲染等操作的可用時間就變得少了。
致使GC頻繁履行有兩個緣由:
解決上面的問題有簡潔直觀方法,如果你在Memory Monitor里面查看到短時間產生了屢次內存的漲跌,這意味著很有可能產生了內存抖動。
同時我們還可以通過Allocation Tracker來查看在短時間內,同1個棧中不斷進出的相同對象。這是內存抖動的典型信號之1。
當你大致定位問題以后,接下去的問題修復也就顯得相對直接簡單了。例如,你需要避免在for循環里面分配對象占用內存,需要嘗試把對象的創建移到循環體以外,自定義View中的onDraw方法也需要引發注意,每次屏幕產生繪制和動畫履行進程中,onDraw方法都會被調用到,避免在onDraw方法里面履行復雜的操作,避免創建對象。對那些沒法避免需要創建對象的情況,我們可以斟酌對象池模型,通過對象池來解決頻繁創建與燒毀的問題,但是這里需要注意結束使用以后,需要手動釋放對象池中的對象。
10) Garbage Collection in Android
JVM的回收機制給開發人員帶來很大的好處,不用時刻處理對象的分配與回收,可以更加專注于更加高級的代碼實現。相比起Java,C與C++等語言具有更高的履行效力,他們需要開發人員自己關注對象的分配與回收,但是在1個龐大的系統當中,還是免不了常常產生部份對象忘記回收的情況,這就是內存泄漏。
原始JVM中的GC機制在Android中得到了很大程度上的優化。Android里面是1個3級Generation的內存模型,最近分配的對象會寄存在Young Generation區域,當這個對象在這個區域停留的時間到達1定程度,它會被移動到Old Generation,最后到Permanent Generation區域。
每個級別的內存區域都有固定的大小,爾后不斷有新的對象被分配到此區域,當這些對象總的大小快到達這1級別內存區域的閥值時,會觸發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工具來查看此時內存中分配的到底有哪些對象。
11) Performance Cost of Memory Leaks
雖然Java有自動回收的機制,可是這不意味著Java中不存在內存泄漏的問題,而內存泄漏會很容易致使嚴重的性能問題。
內存泄漏指的是那些程序不再使用的對象沒法被GC辨認,這樣就致使這個對象1直留在內存當中,占用了寶貴的內存空間。明顯,這還使得每級Generation的內存區域可用空間變小,GC就會更容易被觸發,從而引發性能問題。
尋覓內存泄漏并修復這個漏洞是件很辣手的事情,你需要對履行的代碼很熟習,清楚的知道在特定環境下是如何運行的,然后仔細排查。例如,你想知道程序中的某個activity退出的時候,它之前所占用的內存是不是有完全的釋放干凈了?首先你需要在activity處于前臺的時候使用Heap Tool獲得1份當前狀態的內存快照,然后你需要創建1個幾近不這么占用內存的空白activity用來給前1個Activity進行跳轉,其次在跳轉到這個空白的activity的時候主動調用System.gc()方法來確保觸發1個GC操作。最后,如果前面這個activity的內存都有全部正確釋放,那末在空白activity被啟動以后的內存快照中應當不會有前面那個activity中的任何對象了。
如果你發現在空白activity的內存快照中有1些可疑的沒有被釋放的對象存在,那末接下去就應當使用Alocation Track Tool來仔細查找具體的可疑對象。我們可以從空白activity開始監聽,啟動到視察activity,然后再回到空白activity結束監聽。這樣操作以后,我們可以仔細視察那些對象,找出內存泄漏的真兇。
12) Memory Performance
通常來講,Android對GC做了大量的優化操作,雖然履行GC操作的時候會暫停其他任務,可是大多數情況下,GC操作還是相對很安靜并且高效的。但是如果我們對內存的使用不恰當,致使GC頻繁履行,這樣就會引發不小的性能問題。
為了尋覓內存的性能問題,Android Studio提供了工具來幫助開發者。
13) Tool - Memory Monitor
Android Studio中的Memory Monitor可以很好的幫組我們查看程序的內存使用情況。
14) Battery Performance
電量實際上是目前手持裝備最寶貴的資源之1,大多數裝備都需要不斷的充電來保持繼續使用。不幸的是,對開發者來講,電量優化是他們最后才會斟酌的的事情。但是可以肯定的是,千萬不能讓你的利用成為消耗電量的大戶。
Purdue University研究了最受歡迎的1些利用的電量消耗,平均只有30%左右的電量是被程序最核心的方法例如繪制圖片,擺放布局等等所使用掉的,剩下的70%左右的電量是被上報數據,檢查位置信息,定時檢索后臺廣告信息所使用掉的。如何平衡這二者的電量消耗,就顯得非常重要了。
有下面1些措施能夠顯著減少電量的消耗:
我們可以通過手機設置選項找到對應App的電量消耗統計數據。我們還可以通過Battery Historian Tool來查看詳細的電量消耗。
如果發現我們的App有電量消耗過量的問題,我們可使用JobScheduler API來對1些任務進行定時處理,例如我們可以把那些任務重的操作等得手機處于充電狀態,或是連接到WiFi的時候來處理。
15) Understanding Battery Drain on Android
電量消耗的計算與統計是1件麻煩而且矛盾的事情,記錄電量消耗本身也是1個費電量的事情。唯1可行的方案是使用第3方監測電量的裝備,這樣才能夠獲得到真實的電量消耗。
當裝備處于待機狀態時消耗的電量是極少的,以N5為例,打開飛行模式,可以待機接近1個月。可是點亮屏幕,硬件各個模塊就需要開始工作,這會需要消耗很多電量。
使用WakeLock或JobScheduler喚醒裝備處理定時的任務以后,1定要及時讓裝備回到初始狀態。每次喚醒無線信號進行數據傳遞,都會消耗很多電量,它比WiFi等操作更加的耗電。修復電量的消耗是另外1個很大的課題,這里就不展開繼續了。
16) Battery Drain and WakeLocks
高效的保存更多的電量與不斷促使用戶使用你的App來消耗電量,這是矛盾的選擇題。不過我們可使用1些更好的辦法來平衡二者。
假定你的手機里面裝了大量的社交類利用,即便手機處于待機狀態,也會常常被這些利用喚醒用來檢查同步新的數據信息。Android會不斷關閉各種硬件來延長手機的待機時間,首先屏幕會逐步變暗直相當閉,然后CPU進入眠眠,這1切操作都是為了節儉寶貴的電量資源。但是即便在這類睡眠狀態下,大多數利用還是會嘗試進行工作,他們將不斷的喚醒手機。1個最簡單的喚醒手機的方法是使用PowerManager.WakeLock的API來保持CPU工作并避免屏幕變暗關閉。這使得手機可以被喚醒,履行工作,然后回到睡眠狀態。知道如何獲得WakeLock是簡單的,可是及時釋放WakeLock也是非常重要的,不恰當的使用WakeLock會致使嚴重毛病。例如網絡要求的數據返回時間不肯定,致使本來只需要10s的事情1直等待了1個小時,這樣會使得電量白白浪費了。這也是為什么使用帶超時參數的wakelock.acquice()方法是很關鍵的。但是僅僅設置超時其實不足夠解決問題,例如設置多長的超時比較適合?甚么時候進行重試等等?
解決上面的問題,正確的方式多是使用非精準定時器。通常情況下,我們會設定1個時間進行某個操作,但是動態修改這個時間或許會更好。例如,如果有另外1個程序需要比你設定的時間晚5分鐘喚醒,最好能夠等到那個時候,兩個任務捆綁1起同時進行,這就是非精肯定時器的核心工作原理。我們可以定制計劃的任務,可是系統如果檢測到1個更好的時間,它可以推延你的任務,以節省電量消耗。
這正是JobScheduler API所做的事情。它會根據當前的情況與任務,組合出理想的喚醒時間,例如等到正在充電或連接到WiFi的時候,或集中任務1起履行。我們可以通過這個API實現很多免費的調度算法。
從Android 5.0開始發布了Battery History Tool,它可以查看程序被喚醒的頻率,又誰喚醒的,延續了多長的時間,這些信息都可以獲得到。
請關注程序的電量消耗,用戶可以通過手機的設置選項視察到那些耗電量大戶,并可能決定卸載他們。所以盡可能減少程序的電量消耗是非常有必要的。
原文出自:http://www.csdn.net/article/2015-01⑵0/2823621-android-performance-patterns/3