Android的布局管理器本身就是個(gè)UI組件,所有的布局管理器都是ViewGroup的子類,而ViewGroup是View的子類,所以布局管理器可以當(dāng)成普通的UI組件使用,也可以作為容器類使用,可以調(diào)用多個(gè)重載addView()向布局管理器中添加組件,并且布局管理器可以互相嵌套,當(dāng)然不推薦過多的嵌套 (兼容低端機(jī)型,最好不要超過5層)。
布局層級(jí)管理
讓咱們一起了解一下每當(dāng)系統(tǒng)繪制一個(gè)布局時(shí),都會(huì)發(fā)生一些什么。這一過程由兩個(gè)步驟完成:
繪制(Measurement)
1:根布局測(cè)量自身。
2:根布局要求它內(nèi)部所有子組件測(cè)量自身。
3:所有自布局都需要讓它們內(nèi)部的子組件完成這樣的操作,直到遍歷完視圖層級(jí)中所有的View。
擺放(Positioning)
1:當(dāng)布局中所有的View都完成了測(cè)量,根布局則開始將它們擺放到合適的位置。
2:所有子布局都需要做相同的事情,直到遍歷完視圖層級(jí)中所有的View。
背景設(shè)置產(chǎn)生的過度繪制
組件背景:每個(gè)組件每設(shè)置一次背景, 該組件的區(qū)域就會(huì)增加一層繪制 , 如 LinearLayout 設(shè)置背景顏色 , 里面的 TextView 設(shè)置背景顏色 , 都會(huì)增加該組件區(qū)域內(nèi)的過渡繪制 ;
主題背景:Activity 界面的主題背景,會(huì)增加一次 GPU 繪制 ;
不要隨意給布局中的 UI 組件設(shè)置背景 。如 ImageView 設(shè)置一張圖片,會(huì)增加一次繪制 ,再給該 ImageView 組件設(shè)置背景顏色, 那么又會(huì)增加一次繪制, 那么該 ImageView 組件肯定過渡繪制了。
小結(jié)
當(dāng)某個(gè)View的屬性發(fā)生變化(如:TextView內(nèi)容變化或ImageView圖像發(fā)生變化),View自身會(huì)調(diào)用View.invalidate()方法(必須從 UI 線程調(diào)用),自底向上傳播該請(qǐng)求,直到根布局(根布局會(huì)計(jì)算出需要重繪的區(qū)域,進(jìn)而對(duì)整個(gè)布局層級(jí)中需要重繪的部分進(jìn)行重繪)。布局層級(jí)越復(fù)雜,UI加載的速度就越慢。因此,在編寫布局的時(shí)候,盡可能地扁平化是非常重要的。
FrameLayout和TableLayout有各自的特殊用途,LinearLayout 和 RelativeLayout 是可以互換的,ConstraintLayout和RelativeLayout類似。也就是說,在編寫布局時(shí),可以選擇其中一種,咱們可以以不同的方式來(lái)編寫下面這個(gè)簡(jiǎn)單的布局。
小實(shí)驗(yàn)
LinearLayout
第一種方式是使用LinearLayout,雖然可讀性比較強(qiáng),但是性能比較差。由于嵌套LinearLayout會(huì)加深視圖層級(jí),每次擺放子組件時(shí),相對(duì)需要消耗更多的計(jì)算。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:id="@+id/view_top_1"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/color_666666"/>
<View
android:id="@+id/view_top_2"
android:layout_width="200dp"
android:layout_height="100dp"
android:background="@color/teal_200"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<View
android:id="@+id/view_top_3"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/color_FF773D"/>
<View
android:id="@+id/view_top_4"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/purple_500"/>
</LinearLayout>
</LinearLayout>
復(fù)制代碼
LinearLayout視圖層級(jí)如下所示:

使用RelativeLayout
第二種方法使用RelativeLayout,在這種情況下,你不需要嵌套其他ViewGroup,因?yàn)槊總€(gè)子View可以相當(dāng)于其他View,或相對(duì)與父控件進(jìn)行擺放。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/view_top_1"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/color_666666"/>
<View
android:id="@+id/view_top_2"
android:layout_width="200dp"
android:layout_below="@id/view_top_1"
android:layout_height="100dp"
android:background="@color/teal_200"/>
<View
android:id="@+id/view_top_3"
android:layout_width="100dp"
android:layout_below="@id/view_top_2"
android:layout_height="100dp"
android:background="@color/color_FF773D"/>
<View
android:id="@+id/view_top_4"
android:layout_width="100dp"
android:layout_below="@id/view_top_2"
android:layout_toRightOf="@id/view_top_3"
android:layout_height="100dp"
android:background="@color/purple_500"/>
</RelativeLayout>
復(fù)制代碼
RelativeLayout視圖層級(jí)如下所示:

通過兩種方式,可以很容易看出,第一種方式LinearLayout需要3個(gè)視圖層級(jí)和6個(gè)View,第二種方式RelativeLayout僅需要2個(gè)視圖層級(jí)和5個(gè)View。
當(dāng)然,雖然RelativeLayout效率更高,但不是所有情況都能通過相對(duì)布局的方式來(lái)完成控件擺放。所以通常情況下,這兩種方式需要配合使用。
注意:為了保證應(yīng)用程序的性能,在創(chuàng)建布局時(shí),需要盡量避免重繪,布局層級(jí)應(yīng)盡可能地扁平化,這樣當(dāng)View被重繪時(shí),可以減少系統(tǒng)花費(fèi)的時(shí)間。在條件允許的情況下,盡量的使用RelativeLayout和ConstraintLayout,而非LinearLayout,或者用GridLayoutl來(lái)替換LinearLayout。
咱們最常使用的是ViewGroup是LinearLayout,只是因?yàn)樗苋菀卓炊?,編寫起?lái)簡(jiǎn)單,所以它就成了Android開發(fā)的首選。出于這個(gè)原因,Google推出了一個(gè)全新的ViewGroup。在適當(dāng)?shù)臅r(shí)候時(shí)候使用它,可以減少冗余,它就是網(wǎng)格布局GridLayout下。
布局復(fù)用(< include/>和 < merge/> )
Android 提供了一個(gè)非常有用的標(biāo)簽。在某些情況下,當(dāng)你希望在其他布局中用一些已存在的布局時(shí),< include/> 標(biāo)簽可通過制定相關(guān)引用ID,將一個(gè)布局添加到另一個(gè)布局。
比如自定義一個(gè)標(biāo)題欄,那么可以按照下面的方式,創(chuàng)建一個(gè)可重復(fù)用的布局文件。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:id="@+id/view_top_1"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/color_666666"/>
</RelativeLayout>
復(fù)制代碼
接著,將< include/>標(biāo)簽放入相應(yīng)的布局文件中,替換掉對(duì)應(yīng)的 View:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/include_layout"/>
...
</RelativeLayout>
復(fù)制代碼
這么一來(lái),當(dāng)你希望重用某些View時(shí),就不用復(fù)制/粘貼的方式來(lái)實(shí)現(xiàn),只需要定義一個(gè)layout文件,然后通過 < include/> 引用即可。
但是這樣做,可能會(huì)引入一個(gè)冗余的ViewGroup(重用的布局文件的根視圖)。為此,Android 提供了另一個(gè)標(biāo)簽,用來(lái)幫我們減少布局冗余,讓層級(jí)變得更加扁平化。我們只需要將可重用的根視圖,替換為 < merge/> 標(biāo)簽即可。如下所示:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:id="@+id/view_top_1"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/color_666666"/>
</merge>
復(fù)制代碼
如此一來(lái),就沒有了冗余的視圖控件,因?yàn)橄到y(tǒng)會(huì)忽略< merge/>標(biāo)簽,并將< merge/>標(biāo)簽中的視圖直接放置在相應(yīng)的布局文件中,替換< include/>標(biāo)簽。
注意:使用此標(biāo)簽時(shí),需要記住它的兩個(gè)主要限制:
1、它只能作為布局文件的跟來(lái)使用。 2、每次調(diào)用LayoutInflater.inflate()時(shí),必須為布局文件提供一個(gè)View,作為它的父容器:LayoutInflater.from(this).inflate(R.layout.merge_layout,parent,true);
ViewStub
ViewStub是一個(gè)不可見的零大小View,可以作為一個(gè)節(jié)點(diǎn)被加入布局文件,但他關(guān)聯(lián)的布局,知道運(yùn)行時(shí)通過調(diào)用 ViewStub.inflate() 或 View.setVisibility(View.VISIBLE) 方法,才會(huì)被繪制。
先看效果圖:

ViewStub 設(shè)置
<ViewStub android:id="@+id/viewStub"
android:inflatedId="@+id/subTree"
android:layout="@layout/activity_imageview"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
復(fù)制代碼
顯示
上方 ViewStub 所關(guān)聯(lián)的布局 activity_imageview 并不會(huì)被實(shí)例化(不要調(diào)用布局內(nèi)的控件,因?yàn)檫€沒加載會(huì)報(bào)空指針異常),只有程序在運(yùn)行期間調(diào)用了以下方法:
findViewById(R.id.viewStub).setVisibility(View.VISIBLE);
((ViewStub)findViewById(R.id.viewStub)).inflate();
復(fù)制代碼
在這期間不要調(diào)用關(guān)聯(lián)布局內(nèi)的控件,因?yàn)檫€沒唄加載沒有
一旦 ViewStub 變成 visible 或者被 inflate,它便不再可用(Id:viewStub沒了),因?yàn)樗诓季謱蛹?jí)中的位置已經(jīng)實(shí)例化出來(lái)的布局所替代,因?yàn)椴荒鼙辉L問,而應(yīng)該使用 android:inflatedId 屬性中的id。如下:
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_view:
break;
case R.id.btn_scheme:
//加載,選擇一種即可。
findViewById(R.id.v_stud).setVisibility(View.VISIBLE);
//((ViewStub)findViewById(R.id.v_stud)).inflate();
//加載后layout所用ID
subTree = findViewById(R.id.subTree);
findViewById(R.id.btn_iv_basis).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,"我是 ViewStud 加載的控件",Toast.LENGTH_SHORT).show();
}
});
break;
case R.id.btn_invisible:
subTree.setVisibility(View.INVISIBLE);
break;
case R.id.btn_visible:
subTree.setVisibility(View.VISIBLE);
break;
case R.id.btn_init:
//ViewStub變?yōu)榭梢姾笤俅握{(diào)用會(huì)報(bào)空指針,因?yàn)閕d:viewStub 已經(jīng)不存在了。
View viewStub = findViewById(R.id.viewStub);
viewStub.setVisibility(View.GONE);
break;
}
}
復(fù)制代碼
小結(jié)
ViewStub 非常有用,我們可以通過 ViewStub 來(lái)延遲部分 View 的加載,縮短首次加載時(shí)間,以及減少一些不必要的內(nèi)存分配。
自定義組件優(yōu)化
在自定義View時(shí)需要注意,避免犯以下的性能錯(cuò)誤:
在非必要時(shí),對(duì)View進(jìn)行重繪。
繪制一些不被用戶所看到的的位置,也就是過度繪制(被覆蓋的地方)。
在繪制期間做了一些非必要的操作,導(dǎo)致內(nèi)存資源的消耗。
優(yōu)化
-
View.invalite()是最最廣泛的使用操作,因?yàn)樵谌魏螘r(shí)候都是刷新和更新視圖最快的方式。
在自定義View時(shí)要小心避免調(diào)用非必要的方法,因?yàn)檫@樣會(huì)導(dǎo)致重復(fù)強(qiáng)行繪制整個(gè)視圖層級(jí),消耗寶貴的幀繪制周期。檢查清楚View.invalite()和View.requestLayout()方法調(diào)用時(shí)間位置,因?yàn)檫@會(huì)影響整個(gè)UI,導(dǎo)致GPU和它的幀速率變慢。
避免過渡重繪。為了避免過渡重繪,我們可以利用Canvas方法,只繪制控件中所需要的部分。整個(gè)一般在重疊部分或控件時(shí)特別有用。相應(yīng)的方法是Canvas.clipRect()(指定要被繪制的區(qū)域);
-
在實(shí)現(xiàn)View.onDraw()方法中,不應(yīng)該在方法內(nèi)及調(diào)用的方法中進(jìn)行任何的對(duì)象分配。在該方法中進(jìn)行對(duì)象分配,對(duì)象會(huì)被創(chuàng)建和初始化。而當(dāng)View.onDraw()方法執(zhí)行完畢時(shí)。垃圾回收器會(huì)釋放內(nèi)存。如果View帶動(dòng)畫,那么View在一秒內(nèi)會(huì)被重繪60次。所以要避免在View.onDraw()方法中分配內(nèi)存。
永遠(yuǎn)不要在View.onDraw()方法中及調(diào)用的方法中進(jìn)行內(nèi)存分配,避免帶來(lái)負(fù)擔(dān)。垃圾回收器多次釋放內(nèi)存,會(huì)導(dǎo)致卡頓。最好的方式就是在View被首次創(chuàng)建出來(lái)時(shí),實(shí)例化這些對(duì)象。
布局優(yōu)化到這里就結(jié)束了,還是那句話后面如果有更好的方案會(huì)及時(shí)的添加進(jìn)去,如果有老大有其他方案也可以留言哈,感謝!
Android布局優(yōu)化教學(xué)——→視頻地址
作者:Android帥次
鏈接:https://juejin.cn/post/7020246896111255560
來(lái)源:稀土掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。