多數手機的屏幕刷新頻率是60hz,在1000/60=16.67ms內沒有辦法把這一幀的繪製任務執行完畢,就會發生丟幀的現象。丟幀越多,用戶感受到的卡頓情況就越嚴重。這裏的繪製包含了所有View的meature、layout、draw等,CPU的計算,以及GPU的柵格化渲染等一系列操作,也就是說,一般我們需要在16ms以內完成單次繪製的所有工作,才能保證app的流暢。
拋去CPU在meature、layout、draw過程中的各種不恰當的耗時操作(如處理大圖片等), Android UI問題一般就是過度繪製,即屏幕上某一像素點在一幀中被重複繪製多次。其解決方法簡單概括就是:
- 通過Hierarchy Viewer去檢測渲染效率,去除不必要的嵌套
- 通過Show GPU Overdraw去檢測Overdraw,最終可以通過移除不必要的背景,以及使用canvas.clipRect解決大多數覆蓋繪製問題。
過度繪製調試工具
GPU呈現模式分析
此工具可以記錄每一幀繪製所使用的總時長,其中綠橫線代表16ms,每一幀豎線代表的含義:
其他具體可見此處。若發現超過16ms綠線的幀較多或較密集,可以使用下面的工具進一步確定。
ADM中的Dump
使用此工具,獲取佈局層次結構,根據具體情況,手動去除不必要的層級。之後,用以下工具做進一步處理。
GPU過度繪製調試工具
以顏色塊表示出像素點被繪製過多少次的工具,過度繪製的主要調試工具。
假設頁面佈局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<!--1x OverDraw-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff"
android:gravity="bottom">
<!--2x OverDraw-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="#fff"
android:gravity="bottom">
<!--3x OverDraw-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#fff"
android:gravity="bottom">
<!--4x+ OverDraw-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#fff"
android:gravity="bottom">
<!--4x+ OverDraw-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#fff"
android:gravity="bottom">
<!--4x+ OverDraw-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#fff"
android:gravity="bottom">
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
開啓過度繪製之後可以看到:
如果顏色爲原色,則表示像素只被繪製過一次,是最理想的情況。
解決過度繪製的幾個方法
引發過度繪製的原因,不外乎以下幾點:
1. 頁面UI嵌套層級深;
2. 同一區域裏,出現一個元素被另一個元素覆蓋的情況;
3. 背景顏色覆蓋(其實跟1是同樣的的問題)。
解決背景顏色覆蓋問題
一般情況下,如果不設置Activity中theme的android:windowBackground屬性的話,DecorView會默認的使用黑色背景:
/**
* Returns the color used to fill areas the app has not rendered content to yet when the
* user is resizing the window of an activity in multi-window mode.
*/
public static Drawable getResizingBackgroundDrawable(Context context, int backgroundRes,
int backgroundFallbackRes, boolean windowTranslucent) {
if (backgroundRes != 0) {
final Drawable drawable = context.getDrawable(backgroundRes);
if (drawable != null) {
return enforceNonTranslucentBackground(drawable, windowTranslucent);
}
}
if (backgroundFallbackRes != 0) {
final Drawable fallbackDrawable = context.getDrawable(backgroundFallbackRes);
if (fallbackDrawable != null) {
return enforceNonTranslucentBackground(fallbackDrawable, windowTranslucent);
}
}
return new ColorDrawable(Color.BLACK);
}
所以,不使用android:windowBackground的話,將其設置成透明色,可以全局的減少一層繪製,及其划算。上例中設置windowBackground爲透明後:
同理,在佈局時,應去掉佈局文件中不必要的background設置,以此減少過度繪製的次數。
解決頁面UI嵌套層級深問題
使用Hierarchy Viewer可以直觀地看到佈局的層次結構,但我們大多數時候,都可以直接修改佈局文件來減少嵌套過深的問題。基本原則是是我們的佈局儘量“扁平化”,佈局中的嵌套層次以及冗餘層次儘量減少,如下例中:
佈局1:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#e5e5e5"
android:orientation="horizontal"
android:padding="10dp">
<ImageView
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@mipmap/ic_launcher"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="姓名"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="年齡"/>
</FrameLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="社會主義好青年"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="個人簡介:富強、民主、文明、和諧、自由、平等、公正、法治、愛國、敬業、誠信、友善"/>
</LinearLayout>
</LinearLayout>
佈局二:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:background="#e5e5e5"
android:orientation="horizontal"
android:padding="10dp">
<ImageView
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="70dp"
android:text="姓名"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="年齡"/>
<TextView
android:id="@+id/tv_tag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tv_name"
android:layout_below="@+id/tv_name"
android:layout_marginTop="10dp"
android:text="社會主義好青年"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tv_tag"
android:layout_below="@+id/tv_tag"
android:layout_marginTop="10dp"
android:text="個人簡介:富強、民主、文明、和諧、自由、平等、公正、法治、愛國、敬業、誠信、友善"/>
</RelativeLayout>
兩者的顯示效果是一樣的:
但是佈局二中,少繪製了很多佈局一中的冗餘控件和層次,使用Hierachy Viewe的話,可以明顯的看出兩者的差距(Hierachy Viewer一般無法在真機上使用,此處就不做比較結果了…)。
其次,在需要有條件的動態顯示View的情況下,使用ViewStub先佔位,可以避免直接先將所有的View實例化(即使Gone的情況也會被實例化),而在ViewStub使用inflate方法後,具體的View纔會被實例化,這樣,就可以使用ViewStub,來方便的在運行時決定,要不要顯示某個佈局或者具體要顯示哪個佈局。
另外,使用merge作爲xml的根標籤,也可以在子視圖不需要指定任何針對父視圖的佈局屬性(layout_gravity等),或者父視圖與當前根節點是同一種ViewGroup時,幹掉一個view層級。
解決元素覆蓋問題
先來看一個例子,顯示一個疊加的橫向圖片列表:
public class Card extends View {
private int picWidth;
private int space;
private Bitmap bm;
private Paint paint;
public Card(Context context) {
super(context);
init();
}
public Card(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public Card(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
picWidth = getResources().getDimensionPixelOffset(R.dimen.dp150);
space = getResources().getDimensionPixelOffset(R.dimen.dp50);
bm = BitmapFactory.decodeResource(getResources(), R.mipmap.ysl);
paint = new Paint();
paint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int n = (getWidth() - picWidth) / space;
for (int i = 0; i < n; i++) {
drawPic(canvas, space * i, picWidth);
}
}
private void drawPic(Canvas canvas, int marginLeft, int size) {
Rect recDes = new Rect(marginLeft, 0, marginLeft + size, size);
canvas.drawBitmap(bm, new Rect(0, 0, getWidth(), getHeight()),
recDes, paint);
}
}
此時的表現爲:
可以看到,中間紅、綠色部分本應是被蓋住的部分,依然多繪製了一次以上。我們的期望是,只繪製每張圖片沒有“相交”的部分,此時就用到了canvas.clipRece:
/**
* Intersect the current clip with the specified rectangle, which is
* expressed in local coordinates.
*
* @param left The left side of the rectangle to intersect with the
* current clip
* @param top The top of the rectangle to intersect with the current clip
* @param right The right side of the rectangle to intersect with the
* current clip
* @param bottom The bottom of the rectangle to intersect with the current
* clip
* @return true if the resulting clip is non-empty
*/
public boolean clipRect(int left, int top, int right, int bottom) {
return nClipRect(mNativeCanvasWrapper, left, top, right, bottom,
Region.Op.INTERSECT.nativeInt);
}
簡單來說,這個clipRect就是對canvas畫布進行裁剪,即只在定義的矩形範圍內繪製,運用後修改原代碼爲:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int n = (getWidth() - picWidth) / space;
for (int i = 0; i < n; i++) {
int left = space * i;
canvas.save();
int right = left + (i < n - 1 ? space : picWidth);
// 只裁剪出需要繪製的區域
canvas.clipRect(left, 0, right, picWidth);
drawPic(canvas, left, picWidth);
canvas.restore();
}
}
private void drawPic(Canvas canvas, int marginLeft, int size) {
Rect recDes = new Rect(marginLeft, 0, marginLeft + size, size);
canvas.drawBitmap(bm, new Rect(0, 0, getWidth(), getHeight()),
recDes, paint);
}
現在效果如下:
實際上,熟悉一下canvas的具體用法,以及clip的具體用法等,能發現更多值得探索和優化的東西,此處就不一一列舉了。
至於View之間的疊加引發的過度繪製(很多時候都是這種情況),目前鄙人還沒找到很好的解決方法,只能在開發中儘量避免了。
避免過度優化
減少過度繪製確實可以提升app性能,但一味的追求所謂的優化,有時候也是很浪費時間的。比如明明簡簡單單的就能用疊加控件的方式,實現一些交叉比較小的動態畫廊控件,爲了追求完美,花了大量時間去計算運動過程中的疊加部分,結果反而會讓cpu計算時間增長,與初衷相背馳了。因此,在實際設計和開發時,需要在複雜的邏輯,與簡單易用的界面中做一個平衡。
Hierarchy Viewer
上面的工具是可以比較直觀的去發現優化方案的,對於要精細把握app整體頁面的渲染情況是,可以使用Hierarchy Viewer。此工具提供了可視化的頁面佈局層次結構,並可得到每個view的具體meature、layout、draw的耗時,非常直觀的幫助我們優化佈局。
Hierarchy Viewer只能在開發版手機(幾乎沒有)以及模擬器上使用,使用ViewServer庫也可以在普通android機上使用,但是在部分國產機上無效,且在需要調試的Activity中需添加部分代碼,麻煩,建議還是以模擬器的方式來使用。
食用方式: