一、前言
性能優(yōu)化包含的部分很多鼻弧,包括布局设江、內(nèi)存、耗電攘轩、流量等等叉存,其中布局優(yōu)化是最容易掌握,也最容易被大家所忽視的一個(gè)方面度帮,今天歼捏,就來(lái)介紹一下有關(guān)布局優(yōu)化的一些技巧。
二笨篷、布局優(yōu)化技巧
(1) 使用 <include> 標(biāo)簽進(jìn)行布局復(fù)用
當(dāng)我們的布局中有多個(gè)相同的布局時(shí)瞳秽,可以使用include
標(biāo)簽來(lái)進(jìn)行布局的復(fù)用,這樣冕屯,當(dāng)視覺(jué)需要修改單個(gè)Item
的間距寂诱,文字大小時(shí),只需要修改一個(gè)布局就可以了安聘,例如像下面這種情況痰洒,我們就可以使用include
標(biāo)簽來(lái)實(shí)現(xiàn):
根布局為:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include android:id="@+id/include_1" layout="@layout/layout_is_merge" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<include android:id="@+id/include_2" layout="@layout/layout_is_merge" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<include android:id="@+id/include_3" layout="@layout/layout_is_merge" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</LinearLayout>
單個(gè)Item
的布局為:
<?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="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_content_1"
android:text="tv_content_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tv_content_2"
android:text="tv_content_2"
android:layout_marginLeft="40dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</merge>
<include> 要點(diǎn):
- 直接在根布局中瓢棒,如果希望找到
<include>
所指定的layout
中包含的控件,那么就需要給<include>
指定id
丘喻,再通過(guò)它來(lái)尋找子容器中的控件脯宿。 - 在
<include>
標(biāo)簽中,可以指定layout_xxx
屬性泉粉,它將會(huì)覆蓋子布局中的根標(biāo)簽中的屬性连霉。
(2) 使用 <merge> 標(biāo)簽減少布局層級(jí)
當(dāng)出現(xiàn)下面這種情況:一個(gè)xml
布局文件的根節(jié)點(diǎn)是一個(gè)FrameLayout
,并且它沒(méi)有一個(gè)有用的背景嗡靡,那么當(dāng)該xml
布局文件渲染出的ViewGroup
被添加到父布局中時(shí)跺撼,連接處就會(huì)出現(xiàn)一個(gè)多余的節(jié)點(diǎn),而采用<merge>
標(biāo)簽可以去掉這一無(wú)用節(jié)點(diǎn)讨彼,從而降低布局的層級(jí)歉井。
例如,在上面的例子當(dāng)中哈误,我們使用了<merge>
標(biāo)簽的情形為:
假如我們沒(méi)有使用
<merge>
標(biāo)簽哩至,那么情形為:<merge> 要點(diǎn):
- 當(dāng)需要通過(guò)
LayoutInflater
的inflate
方法渲染出以<merge>
作為根節(jié)點(diǎn)標(biāo)簽的xml
文件時(shí),必須傳入不為null
的root
參數(shù)蜜自,且attachToRoot
參數(shù)必須為true
菩貌。 -
<merge>
只可作為xml
的根節(jié)點(diǎn)。 -
<merge>
既不是View
也不是ViewGroup
重荠,它只是表示一組等待被添加的視圖箭阶,因此,對(duì)它設(shè)定的任何屬性都是無(wú)用的晚缩。
(3) 使用 ViewStub 標(biāo)簽動(dòng)態(tài)加載布局
當(dāng)我們的布局中尾膊,存在一些需要按序加載的控件媳危,那么就可以使用ViewStub
標(biāo)簽預(yù)先聲明荞彼,當(dāng)情況滿足時(shí)再去實(shí)例化ViewStub
中所聲明的布局,其用法如下:
- 首先待笑,在布局中預(yù)先聲明
ViewStub
鸣皂,并且通過(guò)layout
標(biāo)簽指定對(duì)應(yīng)的布局layout_stub
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewStub
android:id="@+id/view_stub"
android:inflatedId="@+id/view_inflated"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/layout_stub"/>
</LinearLayout>
- 當(dāng)需要加載以上指定的布局時(shí),那么首先通過(guò)獲得
ViewStub
暮蹂,再調(diào)用它的inflate
或者setVisibility(View.VISIBLE)
方法寞缝,其返回的布局就是layout=
所指定的布局的根節(jié)點(diǎn):
private void inflateIfNeed() {
//1.獲取到布局中的ViewStub。
mViewStub = (ViewStub) findViewById(R.id.view_stub);
//2.調(diào)用其inflate方法實(shí)例化它所指定的layout仰泻。
mStubView = mViewStub.inflate();
}
<ViewStub> 要點(diǎn):
- 任何
ViewStub
只能調(diào)用一次inflate
或者setVisibility(View.VISIBLE)
方法荆陆,并且調(diào)用完之后它將不再可用,ViewStub
原先所在位置將被替換成為layout
參數(shù)所指定的布局的根節(jié)點(diǎn)集侯,并且其根節(jié)點(diǎn)的id
值將變成android:inflatedId
所指定的值:
(4) 選擇合適的父容器以減少布局層級(jí)和測(cè)量次數(shù)
當(dāng)我們需要通過(guò)父容器來(lái)容納多個(gè)子控件時(shí)被啼,如何選擇父容器帜消,將會(huì)影響到布局的效率,而對(duì)于父容器的選擇浓体,有以下幾點(diǎn)原則:
- 首先應(yīng)當(dāng)考慮布局層級(jí)最小的方案泡挺。
- 布局層級(jí)相同時(shí),就應(yīng)當(dāng)選取合適的父容器命浴,一般來(lái)說(shuō)娄猫,有以下幾點(diǎn)經(jīng)驗(yàn):
- 選取的優(yōu)先級(jí)為:
FrameLayout
、不帶layout_weight
參數(shù)的LinearLayout
生闲、RelativeLayout
媳溺,這里選取的標(biāo)準(zhǔn)為帶有layout_weight
的LinearLayout
或者RelativeLayout
會(huì)測(cè)量?jī)纱巍?/li> - 當(dāng)使用
LinearLayout
時(shí),應(yīng)當(dāng)盡量避免使用layout_weight
參數(shù)碍讯。 - 避免使用
RelativeLayout
嵌套RelativeLayout
褂删。 - 如果允許,那么可以使用
Google
新推出的ConstraintLayout
布局冲茸。
(5) 使用 SpannableStringBuilder 替換多個(gè) TextView 的實(shí)現(xiàn)
當(dāng)我們存在多種不同大小屯阀、顏色或者圖文混排需要顯示時(shí),我們往往會(huì)利用多個(gè)TextView
來(lái)進(jìn)行組合轴术,但是某些效果通過(guò)一個(gè)TextView
就可以實(shí)現(xiàn)难衰,一般來(lái)說(shuō),利用SpannableStringBuilder
可以通過(guò)單個(gè)TextView
實(shí)現(xiàn)多種不同的布局逗栽,更多Span
的用法可以參考這篇文章:Android 中各種 Span 的用法盖袭,下面以不同大小的TextView
為例:
private void useSpan() {
TextView textView = (TextView) findViewById(R.id.tv_span);
SpannableStringBuilder ssb = new SpannableStringBuilder("300 RMB");
//設(shè)置文字大小。
ssb.setSpan(new RelativeSizeSpan(6.0f), 0, 3, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
//設(shè)置文字顏色彼宠。
ssb.setSpan(new ForegroundColorSpan(0xff303F9F), 0, 3, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
textView.setText(ssb);
}
最終可以實(shí)現(xiàn)如下的效果:
除此之外鳄虱,還可以實(shí)現(xiàn)圖文混排,例如下面這樣:
(6) 使用 LinearLayout 自帶的分割線凭峡,而不是在布局中手動(dòng)添加一個(gè) ImageView
例如下面的布局:
此時(shí)我們就可以使用
LinearLayout
自帶的divider
屬性來(lái)實(shí)現(xiàn)分割線:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:showDividers="beginning|end|middle"
android:divider="@android:drawable/divider_horizontal_bright"
android:dividerPadding="5dp"
android:paddingTop="20dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Line 1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Line 2"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Line 3"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Line 4"/>
</LinearLayout>
與分割線相關(guān)的屬性包括以下幾個(gè):
-
divider
:傳入分割線的drawable
拙已,可以是一個(gè)圖片,也可以是自己通過(guò)xml
實(shí)現(xiàn)的drawable
摧冀。 -
showDividers
:分割線顯示的位置倍踪,beginning/middle/end
,分割對(duì)應(yīng)頭部索昂、中間建车、尾部。 -
dividerPadding
:分割線距離兩邊的間距椒惨。
(7) 使用 Space 控件進(jìn)行合理的占位
Space
控件位于android.support.v4.widget
包中缤至,與一般控件不同,它的draw
方法是一個(gè)空實(shí)現(xiàn)康谆,因此它只占位置领斥,而不去渲染错洁,使用它來(lái)進(jìn)行占位填充比其它控件更加高效,例如下面戒突,我們需要將一行均等地分成五份屯碴,有顏色部分位于2,4
當(dāng)中:
這時(shí)候,就可以通過(guò)
Space
控件膊存,加上layout_weight
屬性來(lái)實(shí)現(xiàn):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.Space
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"/>
<View
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:background="@color/colorAccent"/>
<android.support.v4.widget.Space
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"/>
<View
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:background="@color/colorAccent"/>
<android.support.v4.widget.Space
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"/>
</LinearLayout>
(8) 使用 TextView 的 drawableLeft/drawableTop 屬性來(lái)替代 ImageView + TextView 的布局
當(dāng)出現(xiàn)圖片在文案的四周時(shí)导而,我們應(yīng)當(dāng)首先考慮能夠通過(guò)單個(gè)TextView
來(lái)實(shí)現(xiàn),而不是通過(guò)LinearLayout
包裹TextView+ImageView
的方式來(lái)實(shí)現(xiàn)隔崎,例如下面的效果:
其布局如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 方式一:使用 ImageView + TextView -->
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<ImageView
android:src="@android:drawable/ic_btn_speak_now"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="ImageView + TextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<!-- 方式二:使用單個(gè) TextView -->
<TextView
android:drawableLeft="@android:drawable/ic_btn_speak_now"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="單個(gè) TextView"/>
</LinearLayout>
可以看到今艺,雖然都是實(shí)現(xiàn)了圖片加上文字的顯示效果,但是第二種通過(guò)單個(gè)TextView
來(lái)實(shí)現(xiàn)其布局層級(jí)更少爵卒,并且控件的個(gè)數(shù)更少虚缎,因此效率更高,并且圖片不僅可以顯示在左邊钓株,還可以顯示在TextView
的四周实牡,圖片和TextView
之間的間隔可以通過(guò)drawablePadding
來(lái)實(shí)現(xiàn)。
(9) 去掉不必要的背景
- 在布局層級(jí)中避免重疊部分的背景
當(dāng)兩個(gè)控件在布局上有重疊的部分轴合,但是它們具有背景時(shí)创坞,就會(huì)出現(xiàn)過(guò)度繪制的情況,造成無(wú)用的性能損耗受葛。并且肉眼無(wú)法發(fā)現(xiàn)题涨,需要通過(guò)設(shè)置當(dāng)中的”調(diào)試GPU過(guò)度繪制"選項(xiàng)進(jìn)行檢查,詳細(xì)使用如下:性能優(yōu)化工具知識(shí)梳理(3) - 調(diào)試GPU過(guò)度繪制 & GPU呈現(xiàn)模式分析总滩。例如下面布局當(dāng)中纲堵,根布局和子控件有100dp
部分重疊,并且它們都有背景:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF">
<LinearLayout
android:background="#FFFFFF"
android:layout_width="match_parent"
android:layout_height="100dp"/>
</LinearLayout>
那么最終闰渔,打開(kāi)過(guò)度繪制檢測(cè)時(shí)席函,就會(huì)出現(xiàn)下面的效果:
- 去掉無(wú)用的
WindowBackgroud
當(dāng)我們使用某些主題時(shí),系統(tǒng)有可能在DecorView
中給我們加上一個(gè)背景澜建,但是有時(shí)候它是無(wú)用的向挖,例如上面的例子中,我們根布局為紫色炕舵,這其實(shí)就是由于默認(rèn)主題中的背景所導(dǎo)致的,我們可以通過(guò)下面的方式去除掉該背景跟畅。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_overdraw);
getWindow().setBackgroundDrawable(null);
}
此時(shí)的檢測(cè)結(jié)果如下咽筋,可以看到,根布局就不存在過(guò)度繪制的情況了:
(10) 優(yōu)化自定義控件中的 onDraw 方法
當(dāng)我們?cè)谧远x控件徊件,并重寫onDraw
方法來(lái)完成相應(yīng)的需求時(shí)奸攻,一些錯(cuò)誤的操作往往會(huì)導(dǎo)致布局效率的降低蒜危,一般來(lái)說(shuō),有兩點(diǎn)需要注意:
- 避免在其中進(jìn)行對(duì)象的分配
- 使用
Canvas
的ClipRect
方法避免過(guò)度繪制
這里用一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明一下第二點(diǎn)的實(shí)現(xiàn)睹耐,當(dāng)我們需要實(shí)現(xiàn)下面這個(gè)多張圖片重疊的自定義控件時(shí):
假如我們直接使用下面的方式辐赞,也可以實(shí)現(xiàn)上面的效果:
public class ClipRectView extends View {
private static final int[] ID = new int[]{R.drawable.pic_1, R.drawable.pic_2, R.drawable.pic_3};
private Bitmap[] mBitmaps;
public ClipRectView(Context context) {
super(context);
prepareBitmap();
}
public ClipRectView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
prepareBitmap();
}
public ClipRectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
prepareBitmap();
}
private void prepareBitmap() {
mBitmaps = new Bitmap[ID.length];
int i = 0;
for (int id : ID) {
mBitmaps[i++] = BitmapFactory.decodeResource(getResources(), id);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (Bitmap bitmap : mBitmaps) {
canvas.drawBitmap(bitmap, 0, 0, null);
canvas.translate(bitmap.getWidth() / 2, 0);
}
}
}
但是,如果我們打開(kāi)調(diào)試GPU
過(guò)度繪制的開(kāi)關(guān)硝训,那么可以得到下面的檢測(cè)結(jié)果响委,可以發(fā)現(xiàn)在兩張圖片重疊的地方,會(huì)出現(xiàn)明顯的過(guò)度繪制:
而如果窖梁,我們采用
ClipRect
對(duì)onDraw
方法進(jìn)行優(yōu)化:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
int bits = mBitmaps.length;
for (int i = 0; i < bits; i++) {
Bitmap bitmap = mBitmaps[i];
int bitW = bitmap.getWidth();
int bitH = bitmap.getHeight();
if (i != 0) {
canvas.translate(bitW / 2, 0);
}
canvas.save();
if (i != bits - 1) {
canvas.clipRect(0, 0, bitW / 2, bitH);
}
canvas.drawBitmap(bitmap, 0, 0, null);
canvas.restore();
}
canvas.restore();
}
此時(shí)赘风,檢測(cè)的結(jié)果如下,和上圖相比纵刘,我們很好地解決了過(guò)度繪制的問(wèn)題:
(11) 使用 AsyncLayoutInflater 異步加載布局
在Android Support Library 24
中邀窃,提供了一個(gè)AsyncLayoutInflater
工具類用于實(shí)現(xiàn)xml
布局的異步inflate
,它的用法和普通的LayoutInflater
類似假哎,只不過(guò)它inflate
的執(zhí)行是在子線程當(dāng)中瞬捕,當(dāng)這一過(guò)程完成之后,再通過(guò)OnInflateFinishedListener
接口舵抹,回調(diào)到主線程當(dāng)中山析。
首先是整個(gè)Activity
的根布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_root"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_async"
android:text="開(kāi)始異步 Inflate 布局"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="40dp"/>
</LinearLayout>
接下來(lái)是需要異步inflate
的子布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:text="異步 Inflate 的布局"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"/>
</LinearLayout>
使用方式如下:
private void asyncInflated() {
TextView textView = (TextView) findViewById(R.id.tv_async);
final ViewGroup root = (ViewGroup) findViewById(R.id.ll_root);
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AsyncLayoutInflater asyncLayoutInflater = new AsyncLayoutInflater(OptActivity.this);
asyncLayoutInflater.inflate(R.layout.layout_async, root, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(View view, int resId, ViewGroup parent) {
parent.addView(view);
}
});
}
});
}
其inflate
方法接收三個(gè)參數(shù):
- 需要異步
inflate
的布局id
。 - 所需要添加到的根布局的實(shí)例掏父。
- 異步
inflate
完成的回調(diào)笋轨,該回調(diào)是在主線程當(dāng)中執(zhí)行。需要注意赊淑,在該回調(diào)執(zhí)行時(shí)爵政,異步inflate
出來(lái)的布局并沒(méi)有添加到父布局當(dāng)中,因此陶缺,我們需要通過(guò)addView
的方法將其添加到View
樹(shù)當(dāng)中钾挟。
最終的運(yùn)行結(jié)果為:
(12) 使用性能檢測(cè)工具,找出布局中的性能瓶頸
在分析布局有可能導(dǎo)致的性能問(wèn)題時(shí)饱岸,我們一般會(huì)用到以下幾種工具掺出,這些工具我們?cè)谥皩W(xué)習(xí)性能優(yōu)化工具的時(shí)候都有接觸過(guò):
-
HierecyViewer
性能優(yōu)化工具知識(shí)梳理(4) - Hierarchy Viewer - 調(diào)試
GPU
過(guò)度繪制
性能優(yōu)化工具知識(shí)梳理(3) - 調(diào)試GPU過(guò)度繪制 & GPU呈現(xiàn)模式分析 -
Lint
檢查
性能優(yōu)化工具知識(shí)梳理(8) - Lint
更多文章,歡迎訪問(wèn)我的 Android 知識(shí)梳理系列:
- Android 知識(shí)梳理目錄:http://www.reibang.com/p/fd82d18994ce
- 個(gè)人主頁(yè):http://lizejun.cn
- 個(gè)人知識(shí)總結(jié)目錄:http://lizejun.cn/categories/