目錄
一、View的過度繪制(OverDraw)
二、View的繪制流程
三、三種常用布局的比較
四、RecyclerView VS ListView 之View層級關(guān)系
五、高效布局標(biāo)簽
六、去掉window的背景
七、去掉其他不必要的背景
八、ClipRect & QuickReject
九、善用draw9patch
十、慎用Alpha
十一、應(yīng)該早點(diǎn)知道的API
十二、其他
本文是有心課堂-性能優(yōu)化合輯 視頻的學(xué)習(xí)筆記,也翻閱過網(wǎng)上的相關(guān)資料,整理了個人認(rèn)為比較重要的知識點(diǎn)。
一、View的過度繪制(OverDraw)
OverDraw,是指在一幀的時間內(nèi)(16.67ms)像素被繪制了多次,理論上一個像素只繪制一次是最優(yōu)的,但由于重疊的布局導(dǎo)致一些像素被重復(fù)繪制多次,而每次繪制都會對應(yīng)到CPU的一組繪圖命令和GPU的一些操作,當(dāng)這個操作超過16.67ms時就會出現(xiàn)掉幀的現(xiàn)象,即我們常說的卡頓,所以對重疊不可見元素的重復(fù)繪制會產(chǎn)生額外的開銷,我們需要盡量減少OverDraw的發(fā)生。
Android提供了測量OverDraw的選項(xiàng),在開發(fā)者選項(xiàng)->調(diào)試GPU過度繪制(Show GPU OverDraw),打開該選項(xiàng)就可以看到當(dāng)前頁面OverDraw的狀態(tài)。
- 沒有顏色 : 沒有OverDraw。像素只畫了一次。
- 藍(lán)色:OverDraw 1倍。像素繪制了兩次,大片的藍(lán)色是可以接受的(若整個窗口都是藍(lán)色,可以擺脫一層)。
- 綠色:OverDraw 2倍。像素繪制了三次,中等大小的綠色區(qū)域是可以接受的,但你應(yīng)該嘗試去優(yōu)化、減少它們。
- 淺紅:OverDraw 3倍。像素繪制了四次,小范圍可接受。
- 暗紅:OverDraw 4倍。像素繪制了五次或更多,這是錯誤的,要修復(fù)它們。

二、View的繪制流程
- measure :為整個View樹計(jì)算實(shí)際的大小,即設(shè)置實(shí)際的高(對應(yīng)屬性:mMeasuredHeight)和寬(對應(yīng)屬性:mMeasureWidth),每個View的控件的實(shí)際寬高都是由父視圖和本身的視圖決定的。
- layout:為將整個根據(jù)子視圖的大小以及布局參數(shù)將View樹放到合適的位置上。
- draw:利用前兩步得到的參數(shù),將視圖顯示在屏幕上。
三、三種常用布局的比較
RelativeLayout:
優(yōu)點(diǎn):
1. View樹扁平化,減少View層級
2. 使用場景廣
缺點(diǎn):
1. 測量效率稍差
2. 使用略復(fù)雜
LinearLayout:
優(yōu)點(diǎn):
1. 使用非常簡單
2. 測量效率高
缺點(diǎn):
1. 嵌套過多,易導(dǎo)致View層級復(fù)雜
2. 使用場景相對較窄
FrameLayout:
使用場景特殊,有些場景下可以替代RelativeLayout
選擇布局容器的基本準(zhǔn)則:
- 盡可能的使用RelativeLayout以減少View層級,使View樹趨于扁平化
- 在不影響層級深度的情況下,使用LinearLayout和FrameLayout,而不是RelativeLayout
四、RecyclerView VS ListView 之View層級關(guān)系
| --- RecyclerView
ViewGroup----| |--- ListView
| --- AdapterView --- AbsListView ---|
|--- GridView
從View層級關(guān)系,我們可以看出,在實(shí)際開發(fā)中更推薦使用RecyclerView。
五、高效布局標(biāo)簽
- Merge標(biāo)簽:減少視圖的層級結(jié)構(gòu)。
Merage標(biāo)簽一般配合FrameLayout和LinearLayout使用,當(dāng)然RelativeLayout也可以用Merge標(biāo)簽,只是RelativeLayout本身比較復(fù)雜,如果我們也通過Merge標(biāo)簽去添加一些子View的時候很容易弄巧成拙。
另外Merge標(biāo)簽只能作為XML布局的根標(biāo)簽使用,當(dāng)Inflate以<merge/>開頭的布局文件時,必須指定一個父ViewGroup,并且必須設(shè)定attachToRoot為true。
- ViewStub標(biāo)簽:高效占位符。
我們經(jīng)常會遇到這的情況,運(yùn)行時動態(tài)根據(jù)條件來決定顯示哪個View或者布局。常用的做法是把View都寫在上面,先把他們的可見性都設(shè)為View.GONE,然后在代碼中動態(tài)的更改它的可見性。這樣的做法的優(yōu)點(diǎn)是邏輯簡單而且控制起來比較靈活。但是它的缺點(diǎn)就是耗費(fèi)資源。雖然把View的初始化可見View.GONE,但是在Inflate布局的時候View仍然會被Inflate,也就是說仍然會創(chuàng)建對象,會被實(shí)例化,會被設(shè)置屬性,也就是說會耗費(fèi)內(nèi)存等資源。
ViewStub是一個輕量級的View,它是一個看不見的,不占布局位置,占用資源非常小的控件,可以為ViewStub指定一個布局,在Inflate布局的時候,只有ViewStub會被初始化,然后當(dāng)ViewStub被設(shè)置為可見的時候或是調(diào)用了ViewStub.inflate()的時候,ViewSub所指向的布局會被inflateh和實(shí)例化,然后ViewStub的布局屬性都會傳給它所指向的布局。
<ViewStub
android:id="@+id/stub_view"
android:inflatedId="@+id/panel_stub"
android:layout="@layout/progress_overlay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
當(dāng)想加載布局時,可以使用下面其中的一種方法:
((ViewStub) findViewById(R.id.stub_view)).setVisibility(View.VISIBLE);
或
View importPanel = ((ViewStub) findViewById(R.id.stub_view)).inflate();
- Space標(biāo)簽:空白控件。
六、去掉window的背景
在我們使用了Android自帶的一些主題時,window會被默認(rèn)添加一個純色的背景,這個背景是被DecorView持有的,當(dāng)我們的自定義布局時又添加一張背景圖或者設(shè)置背景色,那么DecorView的background此時對我們來說是無用的,但是它會產(chǎn)生一次OverDraw,帶來繪制性能損耗。
去掉window的背景可以在onCreate中的setContentView之后調(diào)用
getWindow().setBackgroundDrawable(null);
或者在theme中添加
android:windowbackground="null";
七、去掉其他不必要的背景
- 有時候?yàn)榱朔奖銜冉oLayout設(shè)置一個整體的背景,再給子View設(shè)置背景,會造成重疊,如果子View寬度match_parent,可以看到完全覆蓋了Layout的一部分,這里就可以通過分別設(shè)置背景來減少重繪
- 如果采用的是selector的背景,將normal狀態(tài)的color設(shè)置為"@android:color/transparent"也同樣可以解決問題。
這里只簡單舉兩個例子,我們在開發(fā)過程中的一些習(xí)慣性的思維定式會帶來不經(jīng)意的OverDraw,所以開發(fā)過程中我們?yōu)槟硞€View或者ViewGroup設(shè)置背景的時候,先思考下是否真的有必要,或者思考下這個背景能不能分段設(shè)置在子View上,而不是圖方便直接設(shè)置在根View上。
八、ClipRect & QuickReject
為了解決OverDraw的問題,Android系統(tǒng)會通過避免繪制那些完全不可見的組件來盡量減少消耗,但不幸的是,對于那些過于復(fù)雜的自定義View(通常重寫了OnDraw方法),Android系統(tǒng)無法檢測在OnDraw里面具體會執(zhí)行什么操作,系統(tǒng)無法監(jiān)控并自動優(yōu)化,也就無法避免OverDraw了。但是我們可以通過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)的組件,任然會得到繪制。除了clipRect方法之外,我們還可以使用canvas.quickReject()來判斷是否沒和某個矩形相交,從而跳過那些非矩形區(qū)域內(nèi)的繪制操作。
九、善用draw9patch
給ImageView加一個邊框,遇到這種需求,通常在ImageView后面設(shè)置一張背景圖,露出邊框便完美解決問題,此時這個ImageView,設(shè)置了兩次drawable,底下一層僅僅是為了作為圖片的邊框而已,但是兩層drawable的重疊區(qū)域卻繪制了兩次,導(dǎo)致OverDraw。
優(yōu)化方案:將背景drawable制作成draw9patch,并且將和前景重疊的部分設(shè)置為透明,由于Android的2D渲染器會優(yōu)化draw9patch中的透明區(qū)域,從而優(yōu)化了這次OverDraw。但是背景圖片必須制作成draw9patch才行,因?yàn)锳ndroid 2D渲染器只對draw9patch有這個優(yōu)化,否則,一張普通的png,就算你把中間的部分設(shè)置成透明,也不會減少這次OverDraw.
十、慎用Alpha
假如對一個View做Alpha轉(zhuǎn)化,需要先將View繪制出來,然后做Alpha轉(zhuǎn)化,最后將轉(zhuǎn)換的效果在界面上。通俗得說,做Alpha轉(zhuǎn)化就需要對當(dāng)前View繪制兩遍,可想而知,繪制效率會的大打折扣,耗時會翻倍,所以Alpha還是慎用。如果一定要做Alpha轉(zhuǎn)化的話,可以采用緩存的方式。
view.setLayerType(LAYER_TYPE_HARDWARE);
doSmoeThing();
view.setLayerType(LAYER_TYPE_NONE);
通過setLayerType方式可以將當(dāng)前界面緩存在GPU中,這樣不需要每次繪制原始界面,但是GPU內(nèi)存是相當(dāng)寶貴的,所以用完要馬上釋放掉。
十一、應(yīng)該早點(diǎn)知道的API
-
android:lineSpacingExtra
設(shè)置行間距,如“3dp”
-
android:lineSpacingMultiPlier
設(shè)置行間距的倍數(shù),如“1.2”
-
android:includeFontPadding="false"
TextView默認(rèn)上下是有一定的padding的,有時候我們可能不需要上下這部分留白,加上它即可
-
android:titleMode(BitmapDrawable)
可以指定圖片使用重復(fù)填充的模式
-
android:fillViewport
設(shè)置ScrollView撐滿父容器
-
ArgbEvaluator類
實(shí)現(xiàn)豐富的色彩效果,提高體驗(yàn)度。譬如:滑動Viewpager時,背景色漸變;隨著EditText輸入框的長度變化背景色等等
十二、其他
- 盡量避免過多的使用static變量
- Avtivity和Activity之間或Fragment和Fragment之間使用Intent、Bundle傳遞數(shù)據(jù)
- 常量提取到一個單獨(dú)的類中,并注意命名規(guī)范。如Intent傳值的key
- 善用 Gradle
有時候我們的App會把HOST_URL、DEBUG等常量寫在Constants中,這樣我們在打正式包或測試包除了要修改代碼外,studio還需要重新build一次,是比較耗時的,所以我們可以把一些常量的設(shè)置可以定義到build.gradle文件中
buildTypes {
debug {
buildConfigField("String", "HOST_URL", "\"http://api-test.ganxin.com\"")
buildConfigField("Boolean", "LOG_DEBUG", "true")
}
release {
buildConfigField("String", "HOST_URL", "\"http://api.ganxin.com\"")
buildConfigField("Boolean", "LOG_DEBUG", "false")
}
}
又或者在AndroidManifest文件中的元素在不同的場景下需要替換不同的值的,可以嘗試用productFlavors,如 :
productFlavors {
productive{
manifestPlaceholders =[
LAUCHER_LOGO:"@mipmap/ic_logo_normal" ,
CHANNEL_NAME : "normal"
]
}
customA{
manifestPlaceholders =[
LAUCHER_LOGO:"@mipmap/ic_logo_custom" ,
CHANNEL_NAME : "customA"
]
}
}
在AndroidManifest文件中的引用方式:
android:icon="${LAUCHER_LOGO}"
<meta-data
android:name="UMENG_CHANNEL"
android:value="${CHANNEL_NAME}" />
- 使用遞歸方法的時候避免造成OOM(內(nèi)存溢出)
- 注意使用Handler造成的內(nèi)存泄漏
一段簡單的使用Handler示例
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mImageView.setImageBitmap(mBitmap);
}
}
a. 當(dāng)使用內(nèi)部類(包括匿名類)來創(chuàng)建Handler的時候,Handler對象會隱式地持有一個外部類對象(通常是一個Activity)的引用(不然怎么可能通過Handler來操作Activity中的View ?)。而Handler通常會伴隨一個耗時的后臺線程(例如網(wǎng)絡(luò)拉取圖片)一起出現(xiàn),這個后臺線程在任務(wù)執(zhí)行完畢之后,通過消息機(jī)制通知Handler,然后Handler把圖片更新到界面,然后用戶在網(wǎng)絡(luò)請求過程中關(guān)閉了Activity,在正常情況下,Activity不再被使用,它就可能在GC檢查時被回收掉,但由于這時線程尚未執(zhí)行完,而該線程持有Handler的引用,這個Handler又持有Activity的引用,就導(dǎo)致該Activity無法被回收(即內(nèi)存泄漏),直到網(wǎng)絡(luò)請求結(jié)束。
b.另外,如果執(zhí)行了Handler的postDelayed()方法,該方法會將你的Handler裝入一個Message,并把這條Message推到MesageQueue中,那么在你設(shè)定的delay到達(dá)之前,會有一條MessageQueue->Message->Handler->Activity的鏈,導(dǎo)致你的Activity被引用而無法被回收。
解決方案:
a. 通過程序邏輯來進(jìn)行保護(hù)
- 在關(guān)閉Activity的時候停掉你的后臺線程。線程停掉了,就相當(dāng)于切斷了Handler和外部連接的線,Activity自然會在合適的時候被回收
- 如果你的Handler是被delay的Message持有了引用,那么使用相應(yīng)的Handler的removeCallbacks()方法,把消息對象從消息對象移除就行
關(guān)于Handler.remove方法:
removeCallbacks(Runnable r) —— 清除r匹配上的Message
removeCallbacks(Runnable r, Object token) —— 清除r匹配且匹配token(Message.obj)的Message,token為空時,只匹配r
removeCallbacksAndMessage(Object token) —— 清除token匹配的Message
removeMessages(int what) —— 按what來匹配
removeMessages(int what,Object object) —— 按what來匹配
如果需要清除以Handler為target的所有Message(包括Callback),調(diào)用如下方法即可:
handler.removeCallbacksAndMessages(null);
b. 將Handler聲明為靜態(tài)類
靜態(tài)類不持有外部類的對象,所以Activity可以隨意回收。
static class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
mImageView.setImageBitmap(mBitmap);
}
}
但使用了以上代碼后,會發(fā)現(xiàn),由于Handler不再持有外部類對象的引用,導(dǎo)致程序不允許你在Handler中操作Activity的對象了,所以你需要在Handler中增加一個對Activity的弱引用(WeakReference):
static class MyHandler extends Handler {
WeakReference<Activity > mActivityReference;
MyHandler(Activity activity) {
mActivityReference= new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
final Activity activity = mActivityReference.get();
if (activity != null) {
mImageView.setImageBitmap(mBitmap);
}
}
}