目錄
一啥纸、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)資料,整理了個(gè)人認(rèn)為比較重要的知識(shí)點(diǎn)量淌。
一骗村、View的過度繪制(OverDraw)
OverDraw,是指在一幀的時(shí)間內(nèi)(16.67ms)像素被繪制了多次呀枢,理論上一個(gè)像素只繪制一次是最優(yōu)的胚股,但由于重疊的布局導(dǎo)致一些像素被重復(fù)繪制多次,而每次繪制都會(huì)對應(yīng)到CPU的一組繪圖命令和GPU的一些操作裙秋,當(dāng)這個(gè)操作超過16.67ms時(shí)就會(huì)出現(xiàn)掉幀的現(xiàn)象琅拌,即我們常說的卡頓缨伊,所以對重疊不可見元素的重復(fù)繪制會(huì)產(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)色是可以接受的(若整個(gè)窗口都是藍(lán)色灾而,可以擺脫一層)。
- 綠色:OverDraw 2倍扳剿。像素繪制了三次旁趟,中等大小的綠色區(qū)域是可以接受的,但你應(yīng)該嘗試去優(yōu)化庇绽、減少它們锡搜。
- 淺紅:OverDraw 3倍。像素繪制了四次敛劝,小范圍可接受余爆。
- 暗紅:OverDraw 4倍。像素繪制了五次或更多夸盟,這是錯(cuò)誤的蛾方,要修復(fù)它們。
二上陕、View的繪制流程
- measure :為整個(gè)View樹計(jì)算實(shí)際的大小桩砰,即設(shè)置實(shí)際的高(對應(yīng)屬性:mMeasuredHeight)和寬(對應(yīng)屬性:mMeasureWidth),每個(gè)View的控件的實(shí)際寬高都是由父視圖和本身的視圖決定的释簿。
- layout:為將整個(gè)根據(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的時(shí)候很容易弄巧成拙磨取。
另外Merge標(biāo)簽只能作為XML布局的根標(biāo)簽使用,當(dāng)Inflate以<merge/>開頭的布局文件時(shí)柴墩,必須指定一個(gè)父ViewGroup忙厌,并且必須設(shè)定attachToRoot為true。
- ViewStub標(biāo)簽:高效占位符江咳。
我們經(jīng)常會(huì)遇到這的情況逢净,運(yùn)行時(shí)動(dòng)態(tài)根據(jù)條件來決定顯示哪個(gè)View或者布局。常用的做法是把View都寫在上面歼指,先把他們的可見性都設(shè)為View.GONE爹土,然后在代碼中動(dòng)態(tài)的更改它的可見性。這樣的做法的優(yōu)點(diǎn)是邏輯簡單而且控制起來比較靈活踩身。但是它的缺點(diǎn)就是耗費(fèi)資源胀茵。雖然把View的初始化可見View.GONE,但是在Inflate布局的時(shí)候View仍然會(huì)被Inflate挟阻,也就是說仍然會(huì)創(chuàng)建對象琼娘,會(huì)被實(shí)例化,會(huì)被設(shè)置屬性附鸽,也就是說會(huì)耗費(fèi)內(nèi)存等資源脱拼。
ViewStub是一個(gè)輕量級的View,它是一個(gè)看不見的坷备,不占布局位置熄浓,占用資源非常小的控件,可以為ViewStub指定一個(gè)布局省撑,在Inflate布局的時(shí)候赌蔑,只有ViewStub會(huì)被初始化,然后當(dāng)ViewStub被設(shè)置為可見的時(shí)候或是調(diào)用了ViewStub.inflate()的時(shí)候竟秫,ViewSub所指向的布局會(huì)被inflateh和實(shí)例化娃惯,然后ViewStub的布局屬性都會(huì)傳給它所指向的布局。
<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)想加載布局時(shí)鸿摇,可以使用下面其中的一種方法:
((ViewStub) findViewById(R.id.stub_view)).setVisibility(View.VISIBLE);
或
View importPanel = ((ViewStub) findViewById(R.id.stub_view)).inflate();
- Space標(biāo)簽:空白控件。
六劈猿、去掉window的背景
在我們使用了Android自帶的一些主題時(shí)拙吉,window會(huì)被默認(rèn)添加一個(gè)純色的背景潮孽,這個(gè)背景是被DecorView持有的,當(dāng)我們的自定義布局時(shí)又添加一張背景圖或者設(shè)置背景色筷黔,那么DecorView的background此時(shí)對我們來說是無用的往史,但是它會(huì)產(chǎn)生一次OverDraw,帶來繪制性能損耗佛舱。
去掉window的背景可以在onCreate中的setContentView之后調(diào)用
getWindow().setBackgroundDrawable(null);
或者在theme中添加
android:windowbackground="null";
七椎例、去掉其他不必要的背景
- 有時(shí)候?yàn)榱朔奖銜?huì)先給Layout設(shè)置一個(gè)整體的背景,再給子View設(shè)置背景请祖,會(huì)造成重疊订歪,如果子View寬度match_parent,可以看到完全覆蓋了Layout的一部分肆捕,這里就可以通過分別設(shè)置背景來減少重繪
- 如果采用的是selector的背景刷晋,將normal狀態(tài)的color設(shè)置為"@android:color/transparent"也同樣可以解決問題。
這里只簡單舉兩個(gè)例子慎陵,我們在開發(fā)過程中的一些習(xí)慣性的思維定式會(huì)帶來不經(jīng)意的OverDraw眼虱,所以開發(fā)過程中我們?yōu)槟硞€(gè)View或者ViewGroup設(shè)置背景的時(shí)候,先思考下是否真的有必要席纽,或者思考下這個(gè)背景能不能分段設(shè)置在子View上捏悬,而不是圖方便直接設(shè)置在根View上。
八润梯、ClipRect & QuickReject
為了解決OverDraw的問題过牙,Android系統(tǒng)會(huì)通過避免繪制那些完全不可見的組件來盡量減少消耗,但不幸的是仆救,對于那些過于復(fù)雜的自定義View(通常重寫了OnDraw方法)抒和,Android系統(tǒng)無法檢測在OnDraw里面具體會(huì)執(zhí)行什么操作,系統(tǒng)無法監(jiān)控并自動(dòng)優(yōu)化彤蔽,也就無法避免OverDraw了摧莽。但是我們可以通過canvas.clipRect()來幫助系統(tǒng)識(shí)別那些可見的區(qū)域,這個(gè)方法可以指定一塊矩形區(qū)域顿痪,只有在這個(gè)區(qū)域內(nèi)才會(huì)被繪制镊辕,其他的區(qū)域會(huì)被忽視,這個(gè)API可以很好的幫助那些有多組重疊組件的自定義View來控制顯示的區(qū)域蚁袭,同時(shí)clipRect方法還可以幫助節(jié)約CPU與GPU資源征懈,在clipRect區(qū)域之外的繪制指令都不會(huì)被執(zhí)行,那些部分內(nèi)容在矩形區(qū)域內(nèi)的組件揩悄,任然會(huì)得到繪制卖哎。除了clipRect方法之外,我們還可以使用canvas.quickReject()來判斷是否沒和某個(gè)矩形相交,從而跳過那些非矩形區(qū)域內(nèi)的繪制操作亏娜。
九焕窝、善用draw9patch
給ImageView加一個(gè)邊框,遇到這種需求维贺,通常在ImageView后面設(shè)置一張背景圖它掂,露出邊框便完美解決問題,此時(shí)這個(gè)ImageView溯泣,設(shè)置了兩次drawable虐秋,底下一層僅僅是為了作為圖片的邊框而已,但是兩層drawable的重疊區(qū)域卻繪制了兩次垃沦,導(dǎo)致OverDraw客给。
優(yōu)化方案:將背景drawable制作成draw9patch,并且將和前景重疊的部分設(shè)置為透明栏尚,由于Android的2D渲染器會(huì)優(yōu)化draw9patch中的透明區(qū)域起愈,從而優(yōu)化了這次OverDraw。但是背景圖片必須制作成draw9patch才行译仗,因?yàn)锳ndroid 2D渲染器只對draw9patch有這個(gè)優(yōu)化抬虽,否則,一張普通的png纵菌,就算你把中間的部分設(shè)置成透明阐污,也不會(huì)減少這次OverDraw.
十、慎用Alpha
假如對一個(gè)View做Alpha轉(zhuǎn)化咱圆,需要先將View繪制出來笛辟,然后做Alpha轉(zhuǎn)化,最后將轉(zhuǎn)換的效果在界面上序苏。通俗得說手幢,做Alpha轉(zhuǎn)化就需要對當(dāng)前View繪制兩遍,可想而知忱详,繪制效率會(huì)的大打折扣围来,耗時(shí)會(huì)翻倍,所以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的再榄,有時(shí)候我們可能不需要上下這部分留白阅虫,加上它即可
-
android:titleMode(BitmapDrawable)
可以指定圖片使用重復(fù)填充的模式
-
android:fillViewport
設(shè)置ScrollView撐滿父容器
-
ArgbEvaluator類
實(shí)現(xiàn)豐富的色彩效果,提高體驗(yàn)度不跟。譬如:滑動(dòng)Viewpager時(shí),背景色漸變米碰;隨著EditText輸入框的長度變化背景色等等
十二窝革、其他
- 盡量避免過多的使用static變量
- Avtivity和Activity之間或Fragment和Fragment之間使用Intent、Bundle傳遞數(shù)據(jù)
- 常量提取到一個(gè)單獨(dú)的類中吕座,并注意命名規(guī)范虐译。如Intent傳值的key
- 善用 Gradle
有時(shí)候我們的App會(huì)把HOST_URL、DEBUG等常量寫在Constants中吴趴,這樣我們在打正式包或測試包除了要修改代碼外漆诽,studio還需要重新build一次,是比較耗時(shí)的锣枝,所以我們可以把一些常量的設(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}" />
- 使用遞歸方法的時(shí)候避免造成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的時(shí)候撇叁,Handler對象會(huì)隱式地持有一個(gè)外部類對象(通常是一個(gè)Activity)的引用(不然怎么可能通過Handler來操作Activity中的View 供鸠?)。而Handler通常會(huì)伴隨一個(gè)耗時(shí)的后臺(tái)線程(例如網(wǎng)絡(luò)拉取圖片)一起出現(xiàn)陨闹,這個(gè)后臺(tái)線程在任務(wù)執(zhí)行完畢之后楞捂,通過消息機(jī)制通知Handler,然后Handler把圖片更新到界面趋厉,然后用戶在網(wǎng)絡(luò)請求過程中關(guān)閉了Activity寨闹,在正常情況下,Activity不再被使用君账,它就可能在GC檢查時(shí)被回收掉繁堡,但由于這時(shí)線程尚未執(zhí)行完,而該線程持有Handler的引用杈绸,這個(gè)Handler又持有Activity的引用帖蔓,就導(dǎo)致該Activity無法被回收(即內(nèi)存泄漏),直到網(wǎng)絡(luò)請求結(jié)束瞳脓。
b.另外塑娇,如果執(zhí)行了Handler的postDelayed()方法,該方法會(huì)將你的Handler裝入一個(gè)Message劫侧,并把這條Message推到MesageQueue中埋酬,那么在你設(shè)定的delay到達(dá)之前哨啃,會(huì)有一條MessageQueue->Message->Handler->Activity的鏈,導(dǎo)致你的Activity被引用而無法被回收写妥。
解決方案:
a. 通過程序邏輯來進(jìn)行保護(hù)
- 在關(guān)閉Activity的時(shí)候停掉你的后臺(tái)線程拳球。線程停掉了,就相當(dāng)于切斷了Handler和外部連接的線珍特,Activity自然會(huì)在合適的時(shí)候被回收
- 如果你的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為空時(shí)莱找,只匹配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);
}
}
但使用了以上代碼后,會(huì)發(fā)現(xiàn)骨宠,由于Handler不再持有外部類對象的引用浮定,導(dǎo)致程序不允許你在Handler中操作Activity的對象了,所以你需要在Handler中增加一個(gè)對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);
}
}
}