優(yōu)化性能一般從渲染,運算與內(nèi)存,電量三個方面進行谨娜,今天開始說聊一聊Android的渲染機制,我們要知道Android系統(tǒng)每隔16ms就重新繪制一次Activity磺陡,也就是說趴梢,我們的應用必須在16ms內(nèi)完成屏幕刷新的全部邏輯操作,即每一幀只能停留16ms币他,渲染機制說完之后坞靶,然后在說如何去優(yōu)化UI。
1蝴悉、為什么是16ms
16ms意味著1000/60hz彰阴,相當于60fps。這是因為人眼與大腦之間的協(xié)作無法感知超過60fps的畫面更新拍冠。12fps大概類似手動快速翻動書籍的幀率尿这, 這明顯是可以感知到不夠順滑的。24fps使得人眼感知的是連續(xù)線性的運動庆杜,這其實是歸功于運動模糊的效果射众。 24fps是電影膠圈通常使用的幀率,因為這個幀率已經(jīng)足夠支撐大部分電影畫面需要表達的內(nèi)容晃财,同時能夠最大的減少費用支出叨橱。 但是低于30fps是 無法順暢表現(xiàn)絢麗的畫面內(nèi)容的,此時就需要用到60fps來達到想要的效果断盛,超過60fps就沒有必要了罗洗。如果我們的應用沒有在16ms內(nèi)完成屏幕刷新的全部邏輯操作,就會發(fā)生卡頓钢猛。**
2伙菜、為什么16ms沒完成繪制就會卡頓
Android系統(tǒng)每隔16ms發(fā)出VSYNC信號,觸發(fā)對UI進行渲染厢洞,VSync是Vertical Synchronization(垂直同步)的縮寫仇让,是一種在PC上很早就廣泛使用的技術典奉,可以簡單的把它認為是一種定時中斷。而在Android 4.1(JB)中已經(jīng)開始引入VSync機制丧叽。
上圖所示是VSync機制下的繪制過程卫玖。從上圖可以看出,CPU和GPU的處理時間都少于一個VSync的間隔踊淳,即16.6ms假瞬。如果每個間隔都有繪制的情況下,當前的FPS即為60幀迂尝。
當CPU和GPU處理時間都很慢脱茉,或因為其他的原因,如在主線程中干活太多垄开,那么就會出現(xiàn)如下圖這樣的狀況琴许。
從上圖可以看到,CPU和GPU的處理時間因為各種原因都大于一個VSync的間隔(16.6ms)溉躲,所以在第二個VSync還在處理1區(qū)域的繪制時榜田,不可能實現(xiàn)理論上的FPS60,同時也出現(xiàn)了丟幀(SF: Skipped Frame)情況锻梳。試想用戶盯著同一張圖看了32ms而不是16ms箭券,當然很容易察覺出卡頓感,哪怕僅僅出現(xiàn)一次掉幀疑枯,用戶都會發(fā)現(xiàn)動畫不是很順暢辩块,大家在察覺到APP卡頓的時候,可以看看logcat控制臺荆永,會有drop frames類似的警告废亭,那么是什么原因?qū)е?6ms沒能完成繪制的操作呢?
3屁魏、渲染原理
上面說了CPU和GPU的處理時間因為各種原因都大于一個VSync的間隔(16.6ms)滔以,導致了卡頓。渲染操作通常依賴于兩個核心組件:CPU與GPU氓拼。CPU負責包括Measure,Layout抵碟,Record桃漾,Execute的計算操作,GPU 負責Rasterization(柵格化)操作拟逮。何為柵格化撬统,我也是第一次聽到這詞,看下圖敦迄。
[圖片上傳失敗...(image-688e9f-1525510460536)]
所謂的柵格化就是繪制那些Button恋追,Shape凭迹,Path,String苦囱,Bitmap等組件最基礎的操作嗅绸。它把那些組件拆分到不同的像素上進行顯示,說的俗氣一點撕彤,就是解決那些復雜的XML布局文件和標記語言鱼鸠,使之轉(zhuǎn)化成用戶能看懂的圖像,但是這不是直接轉(zhuǎn)換的羹铅,XML布局文件需要在CPU中首先轉(zhuǎn)換為多邊形或者紋理蚀狰,然后再傳遞給GPU進行格柵化,對于柵格化职员,跟OpenGL有關麻蹋,格柵化是一個特別費時的操作。
分析到這里焊切,16毫秒的時間主要被兩件事情所占用扮授,第一件:將UI對象轉(zhuǎn)換為一系列多邊形和紋理;第二件:CPU傳遞處理數(shù)據(jù)到GPU蛛蒙。所以很明顯糙箍,我們要縮短這兩部分的時間,也就是說需要盡量減少對象轉(zhuǎn)換的次數(shù)牵祟,以及上傳數(shù)據(jù)的次數(shù)深夯,對否?
我們再看一圖诺苹,這圖簡單說明CPU和GPU的職責工作咕晋,以及可能發(fā)生的問題和解決方案。
[圖片上傳失敗...(image-75f3e7-1525510821340)]
列名 | 解釋 |
---|---|
PIPELINE | 管道 |
PROBLEM | 發(fā)生的問題 |
TOOLS | 用什么工具來解決 |
SOLUTION | 解決方案時什么 |
在CPU方面收奔,最常見的性能問題是不必要的布局和失效掌呜,這些內(nèi)容必須在視圖層次結構中進行測量、清除并重新創(chuàng)建坪哄,引發(fā)這種問題通常有兩個原因:一是重建顯示列表的次數(shù)太多质蕉,二是花費太多時間作廢視圖層次并進行不必要的重繪,這兩個原因在更新顯示列表或者其他緩存GPU資源時導致CPU工作過度翩肌。在GPU方面模暗,最常見的問題是我們所說的過度繪制(overdraw),通常是在像素著色過程中念祭,通過其他工具進行后期著色時浪費了GPU處理時間兑宇。下面我們對GPU和CPU產(chǎn)生的兩大問題進行優(yōu)化。
- CPU產(chǎn)生的問題:不必要的布局和失效
- GPU產(chǎn)生的問題:過度繪制(overdraw)
4粱坤、過度繪制(overdraw)*檢測
Overdraw(過度繪制)描述的是屏幕上的某個像素在同一幀的時間內(nèi)被繪制了多次隶糕。在多層次的UI結構里面瓷产, 如果不可見的UI也在做繪制的操作,這就會導致某些像素區(qū)域被繪制了多次枚驻。這就浪費大量的CPU以及GPU資源濒旦。
按照以下步驟打開Show GPU Overrdraw的選項:設置 -> 開發(fā)者選項 -> 調(diào)試GPU過度繪制 -> 顯示GPU過度繪制
[圖片上傳失敗...(image-3ea5ed-1525510460536)]
藍色,淡綠测秸,淡紅疤估,深紅代表了4種不同程度的Overdraw情況,
- 藍色: 意味著overdraw 1倍霎冯。像素繪制了兩次铃拇。大片的藍色還是可以接受的(若整個窗口是藍色的,可以擺脫一層)沈撞。
- 綠色: 意味著overdraw 2倍慷荔。像素繪制了三次。中等大小的綠色區(qū)域是可以接受的但你應該嘗試優(yōu)化缠俺、減少它們显晶。
- 淡紅: 意味著overdraw 3倍。像素繪制了四次壹士,小范圍可以接受磷雇。
- 深紅: 意味著overdraw 4倍。像素繪制了五次或者更多躏救。這是錯誤的唯笙,要修復它們。
我們的目標就是盡量減少紅色Overdraw盒使,看到更多的藍色區(qū)域崩掘。
5、Overdraw 的處理方案
Overdraw 的處理方案一:去掉window的默認背景
當我們使用了Android自帶的一些主題時少办,window會被默認添加一個純色的背景苞慢,這個背景是被DecorView持有的。當我們的自定義布局時又添加了一張背景圖或者設置背景色英妓,那么DecorView的background此時對我們來說是無用的挽放,但是它會產(chǎn)生一次Overdraw,帶來繪制性能損耗蔓纠。去掉window的背景可以在onCreate()中setContentView()之后調(diào)用getWindow().setBackgroundDrawable(null);或者在theme中添加android:windowbackground="null"骂维;Overdraw 的處理方案二:去掉其他不必要的背景
有時候為了方便會先給Layout設置一個整體的背景,再給子View設置背景贺纲,這里也會造成重疊,如果子View寬度mach_parent褪测,可以看到完全覆蓋了Layout的一部分猴誊,這里就可以通過分別設置背景來減少重繪潦刃。再比如如果采用的是selector的背景,將normal狀態(tài)的color設置為“@android:color/transparent”,也同樣可以解決問題懈叹。這里只簡單舉兩個例子乖杠,我們在開發(fā)過程中的一些習慣性思維定式會帶來不經(jīng)意的Overdraw,所以開發(fā)過程中我們?yōu)槟硞€View或者ViewGroup設置背景的時候澄成,先思考下是否真的有必要胧洒,或者思考下這個背景能不能分段設置在子View上,而不是圖方便直接設置在根View上墨状。Overdraw 的處理方案三:clipRect的使用
我們可以通過canvas.clipRect()來 幫助系統(tǒng)識別那些可見的區(qū)域卫漫。這個方法可以指定一塊矩形區(qū)域,只有在這個區(qū)域內(nèi)才會被繪制肾砂,其他的區(qū)域會被忽視列赎。這個API可以很好的幫助那些有多組重疊 組件的自定義View來控制顯示的區(qū)域。同時clipRect方法還可以幫助節(jié)約CPU與GPU資源镐确,在clipRect區(qū)域之外的繪制指令都不會被執(zhí)行包吝,那些部分內(nèi)容在矩形區(qū)域內(nèi)的組件,仍然會得到繪制源葫。Overdraw 的處理方案四:ViewStub
ViewStub稱之為“延遲化加載”诗越,在教多數(shù)情況下,程序無需顯示ViewStub所指向的布局文件息堂,只有在特定的某些較少條件下嚷狞,此時ViewStub所指向的布局文件才需要被inflate,且此布局文件直接將當前ViewStub替換掉储矩,具體是通過viewStub.infalte()或viewStub.setVisibility(View.VISIBLE)來完成感耙;
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ViewStub
android:id="@+id/network_error_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/empty_view" />
</RelativeLayout>
private void showNetError() {
// not repeated infalte
if (networkErrorView != null) {
networkErrorView.setVisibility(View.VISIBLE);
return;
}
ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
networkErrorView = stub.inflate();
Button networkSetting = (Button)networkErrorView.findViewById(R.id.network_setting);
Button refresh = (Button)findViewById(R.id.network_refresh);
}
private void showNormal() {
if (networkErrorView != null) {
networkErrorView.setVisibility(View.GONE);
}
}
-
Overdraw 的處理方案五:Merge標簽
MMerge標簽可以干掉一個view層級。Merge的作用很明顯持隧,但是也有一些使用條件的限制即硼。有兩種情況下我們可以使用Merge標簽來做容器控件。第一種子視圖不需要指定任何針對父視圖的布局屬性屡拨,就是說父容器僅僅是個容器只酥,子視圖只需要直接添加到父視圖上用于顯示就行。另外一種是假如需要在LinearLayout里面嵌入一個布局(或者視圖)呀狼,而恰恰這個布局(或者視圖)的根節(jié)點也是LinearLayout裂允,這樣就多了一層沒有用的嵌套,無疑這樣只會拖慢程序速度哥艇。而這個時候如果我們使用merge根標簽就可以避免那樣的問題绝编。另外Merge只能作為XML布局的根標簽使用,當Inflate以開頭的布局文件時,必須指定一個父ViewGroup十饥,并且必須設定attachToRoot為true窟勃。
6、減少不必要的層次:巧用Hierarchy Viewer
Hierarchy Viewer接觸過Android的人估計都用過逗堵,如果在真機上可以
使用ViewServer這個第三方庫:https://github.com/romainguy/ViewServer秉氧,配置步驟比較簡單,主要分為如下三步:
第一步蜒秤,在根build.gradle文件中加入
allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}
第二步汁咏,在Module的build.gradle文件中加入
dependencies {
...................................
compile 'com.github.romainguy:ViewServer:017c01cd512cac3ec054d9eee05fc48c5a9d2de'
}
第三步,加上訪問網(wǎng)絡權限作媚,在Activity添加下列代碼
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set content view, etc.
ViewServer.get(this).addWindow(this);
}
public void onDestroy() {
super.onDestroy();
ViewServer.get(this).removeWindow(this);
}
public void onResume() {
super.onResume();
ViewServer.get(this).setFocusedWindow(this);
}
它只能在root過的機器才能使用攘滩,可以幫我們減少View的層,在Hierarchy Viewer窗口中掂骏,所有的子View上面都有了3個圈圈轰驳, (取色范圍為紅、黃弟灼、綠色)级解,這三個圈圈分別代表measure 、layout田绑、draw的速度勤哗,并且你也可以看到實際的運行的速度,如果你發(fā)現(xiàn)某個View上的圈是紅色掩驱,那么說明這個View相對其他的View芒划,該操作運行最慢,注意只是相對別的View欧穴,并不是說就一定很慢民逼。
布局常見問題與優(yōu)化建議
- 沒有用的父布局時指沒有背景繪制或者沒有大小限制的父布局,這樣的布局不會對UI效果產(chǎn)生任何影響涮帘。我們可以把沒有用的父布局拼苍,通過<merge/>標簽合并來減少UI的層次;
- 使用線性布局LinearLayout排版導致UI層次變深调缨,如果有這類問題疮鲫,我們就使用相對布局RelativeLayout代替LinearLayout,減少UI的層次;
- 不常用的UI被設置成GONE,比如異常的錯誤頁面弦叶,如果有這類問題俊犯,我們需要用<ViewStub/>標簽,代替GONE提高UI性能伤哺。
作者:LooperJing
鏈接:http://www.reibang.com/p/9ac245657127
來源:簡書
著作權歸作者所有燕侠。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權者祖,非商業(yè)轉(zhuǎn)載請注明出處。