原文地址:https://www.csdn.net/article/2015-01-20/2823621-android-performance-patterns/1
最近閱讀了胡凱的Android效能優化典範,做了一些筆記;還有一部分自己在開發的時候遇到的問題。
繪製問題
畫面渲染
-
Android系統每隔16ms發出
VSYNC
訊號,觸發對UI進行渲染。如果每次都能夠渲染成功,則能夠達到60fps,即達到人肉眼能夠接受的流暢畫面。為了達到60fps,需要程式的大多數操作要在16ms內完成。 -
如果你的某個操作時間超過16ms,那麼在系統發出
VSYNC
訊號的時候無法進行正常渲染,這樣就會發生丟幀現象。使用者會在後面看到同一幀畫面
60fps:16ms重新整理一次,能夠達到60fps。60fps能夠達到流暢的原因,是因為人眼與大腦之間的協作無法感知超過60fps的畫面更新
這點,在我最近的畢設專案裡,利用Hander向自己不停傳送sendEmptyMessageDelayed
請求的時候,就深有體會。當我的delay時長越接近16ms時,seekbar的處理就越流暢;而delay時長越長,畫面就越不流暢。
Android系統的觸發渲染也一樣。
程式碼如下:
static class VideoProgressHandler extends Handler {
// WeakReference to the outer class's instance.
private WeakReference<NeroVideoView> mVideoView;
private WeakReference<SeekBar> mSbProgress;
public VideoProgressHandler(NeroVideoView videoView, SeekBar seekbar) {
mVideoView = new WeakReference<>(videoView);
mSbProgress = new WeakReference<>(seekbar);
}
@Override
public void handleMessage(Message msg) {
NeroVideoView videoView = mVideoView.get();
SeekBar sbProgress = mSbProgress.get();
switch (msg.what) {
case UPDATE_PROGRESS: {
//當前時間
if (mVideoView != null) {
int currentPosition = videoView.getCurrentPosition();
sbProgress.setProgress(currentPosition);
sendEmptyMessageDelayed(UPDATE_PROGRESS, 40);
}
break;
}
default: {
break;
}
}
}
}
複製程式碼
話說回來: 使用者容易在UI執行動畫或者滑動ListView的時候感到卡頓;這是因為這裡的操作相對複雜。有可能是由於佈局
太過複雜,無法再16ms內完成渲染。有可能是因為你的UI上有層疊太多的繪製單元,還有可能是因為動畫執行的次數過多。
工具: 可以使用HierarchyViewer來檢視佈局是否太過複雜;也可以通過開發者選項裡的Show GPU Overdraw來觀察。
過度繪製
- 過度繪製指的是:螢幕上某個畫素在同一幀內被繪製了多次。(一層一層又一層)而覆蓋在下面的層繪製了,但卻被新繪製的層覆蓋了。這樣浪費了大量的CPU以及GPU資源。(看不到還繪製,當然浪費資源了對不對)
Overdraw有時候是因為你的UI佈局存在大量重疊的部分,還有的時候是因為非必須的重疊背景。例如某個Activity有一個背景,然後裡面的Layout又有自己的背景,同時子View又分別有自己的背景。僅僅是通過移除非必須的背景圖片,這就能夠減少大量的紅色Overdraw區域,增加藍色區域的佔比。這一措施能夠顯著提升程式效能。
- 為了提升效能,我們需要儘可能減少過度繪製的發生。
在出現複雜佈局的情況下,就會很容易發生過度繪製的現象。 個人認為:ConstraintLayout是一個處理複雜佈局非常棒的layout;但是僅在複雜佈局下才能發揮其最大的效能,因為ConstraintLayout的繪製效率比LinearLayout、FrameLayout、RelativeLayout低好多,原因可以自行體會。
結論: 複雜佈局使用ConstraintLayout,簡單佈局不使用。
- 開發者選項的Show GPU Overdraw 能夠幫助我們觀察UI上的過度繪製情況。 開啟這個功能,會把我們的手機螢幕上的元素標記為藍色、綠色、淡紅、深紅;我們的目標就是儘量減少紅色的發生。
Android的渲染原理
1.Resterization
(柵格化):將繪製內容拆分為不同畫素上顯示。GPU就使用來加快柵格化操作的。
2.CPU負責把UI元件(繪製內容)計算成Polygons
(多邊形),Texture
(紋理),然後交給GPU進行柵格化渲染。
每次將資料從CPU轉移到GPU是很費事的。 OpenGL ES能夠把那些需要渲染的紋理放在GPU Memory中,比較省事;
所以,在處理比較麻煩的動畫和互動的時候,也有人用OpenGL ES來處理繪製
在Android裡面那些由主題所提供的資源,例如Bitmaps,Drawables都是一起打包到統一的Texture紋理當中,然後再傳遞到GPU裡面,這意味著每次你需要使用這些資源的時候,都是直接從紋理裡面進行獲取渲染的。當然隨著UI元件的越來越豐富,有了更多演變的形態。例如顯示圖片的時候,需要先經過CPU的計算載入到記憶體中,然後傳遞給GPU進行渲染。文字的顯示更加複雜,需要先經過CPU換算成紋理,然後再交給GPU進行渲染,回到CPU繪製單個字元的時候,再重新引用經過GPU渲染的內容。動畫則是一個更加複雜的操作流程。
Android處理UI元件更新
Android需要將XML佈局檔案轉為GPU能夠識別並繪製的物件。 Displaylist用來處理這個事項。Displaylist持有所有將要遞交給GPU繪製到螢幕上的資料資訊。
過程: 當某個View第一次需要被渲染的時候,它的DisplayList被建立,當這個View將要顯示到螢幕上時,會執行GPU的繪製指令來進行渲染。 後續,如果執行移動這個View的位置等操作而需要再次繪製這個View時,我們就僅需要再執行一次繪製指令就夠了。 而,如果修改了View中的某個可見元件(繪製內容發生改變),那麼之前的DisplayList就不能使用了,需要重新建立一個DisplayList兵重新執行渲染指令更新到螢幕上。
建立DisplayList,渲染DisplayList,更新到螢幕上等一系列操作挺耗時的,如果佈局太過複雜,會導致嚴重的效能問題。
可以使用Monitor GPU Rendering來檢視渲染的表現效能,也可以通過Show GPU view updates來檢視檢視更新操作。還可以使用HierarchyViewer來檢視檢視。
使佈局儘量扁平化,移除非必須的UI元件能夠減少Measure,Layout的計算時間。
ClipRect,QuickReject
針對自定義View:
我們可以通過canvas.clipRect()來幫助系統識別那些可見的區域。這個方法可以指定一塊矩形區域,只有在這個區域內才會被繪製,其他的區域會被忽視。 可以使用canvas.quickreject()來判斷是否沒和某個矩形相交,從而跳過那些非矩形區域內的繪製操作。
效能除錯工具
- 1.
Profile GPU Rendering
(GPU呈現模式分析) 開啟On Screen As Bars
(在螢幕上顯示為條形圖)進行除錯。
選擇之後就能夠在手機畫面上看到GPU繪製圖形資訊。分別關於StatusBar,NavBar,啟用的程式Activity區域的GPU Rending資訊。 隨著介面的重新整理,介面上會滾動顯示垂直的柱狀圖來表示每幀畫面所需要渲染的時間,柱狀圖越高表示花費的渲染時間越長。
中間橫著的橫線,代表16ms。如果我們確保每一幀花費的時間都小於16ms,就能避免出現卡頓現象。
每一條柱狀線都包含三部分,藍色代表測量繪製Display List的時間,紅色代表OpenGL渲染Display List所需要的時間,黃色代表CPU等待GPU處理的時間。
記憶體問題
簡單說說GC
Android系統有自動管理記憶體的機制。系統會根據記憶體中不同的記憶體資料型別分別執行不同的GC操作。記憶體塊分為Young Generation
(新生代)、Old Generation
(老生代)、Permanent Generation
(持久代)。
瞭解過JVM的話,一定知道這三者的區別;最近分配的物件會存放在Young Generation區域,當這個物件在這個區域停留的時間達到一定程度,它會被移動到Old Generation,最後到Permanent Generation區域。每隔記憶體區域都有固定大小,此後不斷有新的物件被分配到此區域,當這些物件總的大小達到這些區域的閾值的時候,會出發GC。
Android是沒有標記整理的: 這意味著在GC的時候,Android不會講記憶體中的資源整理為連續的記憶體單元。所以說,大量建立物件後,部分物件如果被回收了,今後建立的物件只能儲存在較小塊的零碎空白區域,導致下一次觸發GC的時間就會縮短。
新生代的物件通常會被快速建立並且很快被銷燬回收,新生代的GC操作速度也比老生代快。
在執行GC操作的時候,執行緒的所有操作都將被暫停,等待GC結束後,才能繼續。通常GC操作不會佔用太長時間,但是大量的GC就會佔用幀間隔的時間了,那麼在使用者就可能感到明顯的卡頓。
頻繁執行GC可能是由於:1.記憶體抖動,大量物件唄建立後有馬上被釋放。2.瞬間產生大量物件佔用新生代區域,達到閾值後出發GC。
記憶體洩露
不再使用的物件無法被GC識別,這樣就導致這個物件一直留在記憶體當中,佔用了寶貴的記憶體空間。使得每級Generation的記憶體區域可用空間變小,GC就會更容易被觸發,從而引起效能問題
關於記憶體洩漏:https://blog.csdn.net/anxpp/article/details/51325838
工具
- Memory Monitor: 檢視整個app所佔用的記憶體,以及發生GC的時刻,短時間內發生大量的GC操作是一個危險的訊號。
- Allocation Tracker: 使用此工具來追蹤記憶體的分配,前面有提到過。
- Heap Tool: 檢視當前記憶體快照,便於對比分析哪些物件有可能是洩漏了的,請參考前面的Case。
電池問題
電池問題不太想展開寫……因為電池問題通常都是通過一些定時操作來進行處理的。比較不那麼重要。
Purdue University研究了最受歡迎的一些應用的電量消耗,平均只有30%左右的電量是被程式最核心的方法例如繪製圖片,擺放佈局等等所使用掉的,剩下的70%左右的電量是被上報資料,檢查位置資訊,定時檢索後臺廣告資訊所使用掉的。如何平衡這兩者的電量消耗,就顯得非常重要了。
- 應該減少喚醒螢幕的次數和持續時間;
- 某些非必須馬上執行的操作,例如上傳歌曲,圖片處理等,可以等到裝置處於充電狀態或者電量充足的時候才進行
- 觸發網路請求的操作,每次都會保持無線訊號持續一段時間,我們可以把零散的網路請求打包進行一次操作,避免過多的無線訊號引起的電量消耗。
建議一些非必須執行的操作,可以配合Android的JobScheduler進行處理
好睏了……要去睡了