一、前言
性能優化包含的部分很多,包括布局、內存、耗電、流量等等,其中布局優化是最容易掌握,也最容易被大家所忽視的一個方面,今天,就來介紹一下有關布局優化的一些技巧。
二、布局優化技巧
(1) 使用 <include> 標簽進行布局復用
當我們的布局中有多個相同的布局時,可以使用include
標簽來進行布局的復用,這樣,當視覺需要修改單個Item
的間距,文字大小時,只需要修改一個布局就可以了,例如像下面這種情況,我們就可以使用include
標簽來實現:
根布局為:
<?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>
單個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> 要點:
- 直接在根布局中,如果希望找到
<include>
所指定的layout
中包含的控件,那么就需要給<include>
指定id
,再通過它來尋找子容器中的控件。 - 在
<include>
標簽中,可以指定layout_xxx
屬性,它將會覆蓋子布局中的根標簽中的屬性。
(2) 使用 <merge> 標簽減少布局層級
當出現下面這種情況:一個xml
布局文件的根節點是一個FrameLayout
,并且它沒有一個有用的背景,那么當該xml
布局文件渲染出的ViewGroup
被添加到父布局中時,連接處就會出現一個多余的節點,而采用<merge>
標簽可以去掉這一無用節點,從而降低布局的層級。
例如,在上面的例子當中,我們使用了<merge>
標簽的情形為:
假如我們沒有使用
<merge>
標簽,那么情形為:<merge> 要點:
- 當需要通過
LayoutInflater
的inflate
方法渲染出以<merge>
作為根節點標簽的xml
文件時,必須傳入不為null
的root
參數,且attachToRoot
參數必須為true
。 -
<merge>
只可作為xml
的根節點。 -
<merge>
既不是View
也不是ViewGroup
,它只是表示一組等待被添加的視圖,因此,對它設定的任何屬性都是無用的。
(3) 使用 ViewStub 標簽動態加載布局
當我們的布局中,存在一些需要按序加載的控件,那么就可以使用ViewStub
標簽預先聲明,當情況滿足時再去實例化ViewStub
中所聲明的布局,其用法如下:
- 首先,在布局中預先聲明
ViewStub
,并且通過layout
標簽指定對應的布局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>
- 當需要加載以上指定的布局時,那么首先通過獲得
ViewStub
,再調用它的inflate
或者setVisibility(View.VISIBLE)
方法,其返回的布局就是layout=
所指定的布局的根節點:
private void inflateIfNeed() {
//1.獲取到布局中的ViewStub。
mViewStub = (ViewStub) findViewById(R.id.view_stub);
//2.調用其inflate方法實例化它所指定的layout。
mStubView = mViewStub.inflate();
}
<ViewStub> 要點:
- 任何
ViewStub
只能調用一次inflate
或者setVisibility(View.VISIBLE)
方法,并且調用完之后它將不再可用,ViewStub
原先所在位置將被替換成為layout
參數所指定的布局的根節點,并且其根節點的id
值將變成android:inflatedId
所指定的值:
(4) 選擇合適的父容器以減少布局層級和測量次數
當我們需要通過父容器來容納多個子控件時,如何選擇父容器,將會影響到布局的效率,而對于父容器的選擇,有以下幾點原則:
- 首先應當考慮布局層級最小的方案。
- 布局層級相同時,就應當選取合適的父容器,一般來說,有以下幾點經驗:
- 選取的優先級為:
FrameLayout
、不帶layout_weight
參數的LinearLayout
、RelativeLayout
,這里選取的標準為帶有layout_weight
的LinearLayout
或者RelativeLayout
會測量兩次。 - 當使用
LinearLayout
時,應當盡量避免使用layout_weight
參數。 - 避免使用
RelativeLayout
嵌套RelativeLayout
。 - 如果允許,那么可以使用
Google
新推出的ConstraintLayout
布局。
(5) 使用 SpannableStringBuilder 替換多個 TextView 的實現
當我們存在多種不同大小、顏色或者圖文混排需要顯示時,我們往往會利用多個TextView
來進行組合,但是某些效果通過一個TextView
就可以實現,一般來說,利用SpannableStringBuilder
可以通過單個TextView
實現多種不同的布局,更多Span
的用法可以參考這篇文章:Android 中各種 Span 的用法,下面以不同大小的TextView
為例:
private void useSpan() {
TextView textView = (TextView) findViewById(R.id.tv_span);
SpannableStringBuilder ssb = new SpannableStringBuilder("300 RMB");
//設置文字大小。
ssb.setSpan(new RelativeSizeSpan(6.0f), 0, 3, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
//設置文字顏色。
ssb.setSpan(new ForegroundColorSpan(0xff303F9F), 0, 3, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
textView.setText(ssb);
}
最終可以實現如下的效果:
除此之外,還可以實現圖文混排,例如下面這樣:
(6) 使用 LinearLayout 自帶的分割線,而不是在布局中手動添加一個 ImageView
例如下面的布局:
此時我們就可以使用
LinearLayout
自帶的divider
屬性來實現分割線:
<?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>
與分割線相關的屬性包括以下幾個:
-
divider
:傳入分割線的drawable
,可以是一個圖片,也可以是自己通過xml
實現的drawable
。 -
showDividers
:分割線顯示的位置,beginning/middle/end
,分割對應頭部、中間、尾部。 -
dividerPadding
:分割線距離兩邊的間距。
(7) 使用 Space 控件進行合理的占位
Space
控件位于android.support.v4.widget
包中,與一般控件不同,它的draw
方法是一個空實現,因此它只占位置,而不去渲染,使用它來進行占位填充比其它控件更加高效,例如下面,我們需要將一行均等地分成五份,有顏色部分位于2,4
當中:
這時候,就可以通過
Space
控件,加上layout_weight
屬性來實現:
<?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 屬性來替代 ImageView + TextView 的布局
當出現圖片在文案的四周時,我們應當首先考慮能夠通過單個TextView
來實現,而不是通過LinearLayout
包裹TextView+ImageView
的方式來實現,例如下面的效果:
其布局如下所示:
<?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>
<!-- 方式二:使用單個 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="單個 TextView"/>
</LinearLayout>
可以看到,雖然都是實現了圖片加上文字的顯示效果,但是第二種通過單個TextView
來實現其布局層級更少,并且控件的個數更少,因此效率更高,并且圖片不僅可以顯示在左邊,還可以顯示在TextView
的四周,圖片和TextView
之間的間隔可以通過drawablePadding
來實現。
(9) 去掉不必要的背景
- 在布局層級中避免重疊部分的背景
當兩個控件在布局上有重疊的部分,但是它們具有背景時,就會出現過度繪制的情況,造成無用的性能損耗。并且肉眼無法發現,需要通過設置當中的”調試GPU過度繪制"選項進行檢查,詳細使用如下:性能優化工具知識梳理(3) - 調試GPU過度繪制 & GPU呈現模式分析。例如下面布局當中,根布局和子控件有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>
那么最終,打開過度繪制檢測時,就會出現下面的效果:
- 去掉無用的
WindowBackgroud
當我們使用某些主題時,系統有可能在DecorView
中給我們加上一個背景,但是有時候它是無用的,例如上面的例子中,我們根布局為紫色,這其實就是由于默認主題中的背景所導致的,我們可以通過下面的方式去除掉該背景。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_overdraw);
getWindow().setBackgroundDrawable(null);
}
此時的檢測結果如下,可以看到,根布局就不存在過度繪制的情況了:
(10) 優化自定義控件中的 onDraw 方法
當我們在自定義控件,并重寫onDraw
方法來完成相應的需求時,一些錯誤的操作往往會導致布局效率的降低,一般來說,有兩點需要注意:
- 避免在其中進行對象的分配
- 使用
Canvas
的ClipRect
方法避免過度繪制
這里用一個簡單的例子來說明一下第二點的實現,當我們需要實現下面這個多張圖片重疊的自定義控件時:
假如我們直接使用下面的方式,也可以實現上面的效果:
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);
}
}
}
但是,如果我們打開調試GPU
過度繪制的開關,那么可以得到下面的檢測結果,可以發現在兩張圖片重疊的地方,會出現明顯的過度繪制:
而如果,我們采用
ClipRect
對onDraw
方法進行優化:
@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();
}
此時,檢測的結果如下,和上圖相比,我們很好地解決了過度繪制的問題:
(11) 使用 AsyncLayoutInflater 異步加載布局
在Android Support Library 24
中,提供了一個AsyncLayoutInflater
工具類用于實現xml
布局的異步inflate
,它的用法和普通的LayoutInflater
類似,只不過它inflate
的執行是在子線程當中,當這一過程完成之后,再通過OnInflateFinishedListener
接口,回調到主線程當中。
首先是整個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="開始異步 Inflate 布局"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="40dp"/>
</LinearLayout>
接下來是需要異步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
方法接收三個參數:
- 需要異步
inflate
的布局id
。 - 所需要添加到的根布局的實例。
- 異步
inflate
完成的回調,該回調是在主線程當中執行。需要注意,在該回調執行時,異步inflate
出來的布局并沒有添加到父布局當中,因此,我們需要通過addView
的方法將其添加到View
樹當中。
最終的運行結果為:
(12) 使用性能檢測工具,找出布局中的性能瓶頸
在分析布局有可能導致的性能問題時,我們一般會用到以下幾種工具,這些工具我們在之前學習性能優化工具的時候都有接觸過:
-
HierecyViewer
性能優化工具知識梳理(4) - Hierarchy Viewer - 調試
GPU
過度繪制
性能優化工具知識梳理(3) - 調試GPU過度繪制 & GPU呈現模式分析 -
Lint
檢查
性能優化工具知識梳理(8) - Lint
更多文章,歡迎訪問我的 Android 知識梳理系列:
- Android 知識梳理目錄:http://www.lxweimin.com/p/fd82d18994ce
- 個人主頁:http://lizejun.cn
- 個人知識總結目錄:http://lizejun.cn/categories/