一、前言
效能優化包含的部分很多,包括佈局、記憶體、耗電、流量等等,其中佈局優化是最容易掌握,也最容易被大家所忽視的一個方面,今天,就來介紹一下有關佈局優化的一些技巧。
二、佈局優化技巧
(1) 使用 標籤進行佈局複用
當我們的佈局中有多個相同的佈局時,可以使用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>
所指定的layout
中包含的控制元件,那麼就需要給<include>
指定id
,再通過它來尋找子容器中的控制元件。 - 在
<include>
標籤中,可以指定layout_xxx
屬性,它將會覆蓋子佈局中的根標籤中的屬性。
(2) 使用 標籤減少佈局層級
當出現下面這種情況:一個xml
佈局檔案的根節點是一個FrameLayout
,並且它沒有一個有用的背景,那麼當該xml
佈局檔案渲染出的ViewGroup
被新增到父佈局中時,連線處就會出現一個多餘的節點,而採用<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
只能呼叫一次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 知識梳理目錄:www.jianshu.com/p/fd82d1899…
- 個人主頁:lizejun.cn
- 個人知識總結目錄:lizejun.cn/categories/