如需轉載請評論或簡信,并注明出處,未經允許不得轉載
- Android布局優化(一)LayoutInflate — 從布局加載原理說起
- Android布局優化(二)優雅獲取界面布局耗時
- Android布局優化(三)使用AsyncLayoutInflater異步加載布局
- Android布局優化(四)X2C — 提升布局加載速度200%
- Android布局優化(五)繪制優化—避免過度繪制
目錄
前言
本系列的前面幾篇文章我們介紹了布局加載的原理及優化,布局加載完成后(生成VIew對象)就要進行視圖繪制,我們知道,android要求每幀的繪制時間不超過16ms,不然就會導致丟幀及應用卡頓。所以本文將會介紹一些布局繪制優化技巧
如何監控應用渲染速度
點擊設置—>開發人員選項—>監控—>GPU呈現模式分析,然后選擇 在屏幕上顯示為條形圖 即可以看到一個圖表,如下圖所示
1.沿水平軸的每個豎條都代表一個幀,每個豎條的高度表示渲染該幀所花的時間(單位:毫秒)
2.水平綠線表示 16 毫秒。 要實現每秒 60 幀,代表每個幀的豎條需要保持在此線以下。 當豎條超出此線時,可能會使動畫出現暫停
再來看下每個豎條的顏色代表什么意思:
分析從哪些方向進行繪制優化
從GPU呈現模式分析可以看出來,我們能夠進行優化的點主要就是測量、布局、繪制、動畫和輸入處理
- 測量、布局、繪制過程都會存在自頂而下遍歷過程,所以如果布局的層級過多,這會占用額外的CPU資源
- 當屏幕上的某個像素在同一幀的時間內被繪制了多次(Overdraw),這會浪費大量的CPU以及GPU資源
- 在繪制過程,也就是
onDraw()
方法內,我們應該盡量避免局部對象的創建,因為onDraw()
方法在繪制過程中會多次調用,大量的局部變量可能會造成內存抖動 - 合理使用動畫,這個本章不做討論,有興趣的可以自己了解動畫的相關知識
- 不應該在Event響應的回調中做耗時操作
總結下來視圖繪制優化主要要解決的問題就是:
減少view樹層級,要寬而淺,避免窄而深
如何檢測過度繪制
點擊設置—>開發人員選項—>硬件—>調試GPU過度繪制,然后選擇 顯示過度繪制區域 即可以看到一個圖表,如下圖所示
再來看下每種顏色代表什么意思:
有些過度繪制是無法避免的。但是在優化界面時,應該盡量讓大部分的界面顯示為原色(即無過度繪制)或者為藍色(僅有 1 次過度繪制)。如果出現粉色或者紅色,應該查看代碼看看能否盡量避免
如何避免過度繪制
移除window的背景
一般情況下我們的AppTheme
都默認帶會有windowBackground
<style name="AppTheme" parent="Theme.AppCompat.Light">
...
<item name="android:windowBackground">@color/background_material_light</item>
...
</style>
但是這個windowBackground
大部分清潔下都是沒有什么意義的,因為我們往往都會在布局文件中設置我們當前view的背景顏色。如果我們同時設置了windowBackground
和布局文件中的background
,那就會出現兩次繪制,這顯然是沒有什么意義的,因為最終用戶看到的顏色還是以background
為準
我們可以通過下面兩個方法來解決這個問題
- 在xml中設置
<item name="android:windowBackground">@null</item>
通過代碼設置
getWindow().setBackgroundDrawable(null);
移除控件中不需要的背景
例子:
- 列表頁(
RecyclerView
) 與 其內子控件(Item
)的背景相同,故可移除子控件(Item
)布局中的背景 - 對于1個
ViewPager
+多個Fragment
組成的首頁界面,若每個Fragment
都設有背景色,即 ViewPager 則無必要設置,可移除
所以對于控件背景顏色的設置基本可以歸納為以下兩個原則:
- 對于子控件,如果其背景顏色跟父布局一致,那么就不用再給子控件添加背景了
- 如果子控件背景五顏六色,且能夠完全覆蓋父布局,那么父布局就可以不用添加背景了
減少透明度的使用
對于不透明的view
,只需要渲染一次即可把它顯示出來。但是如果這個view
設置了alpha
值,則至少需要渲染兩次。這是因為使用了alpha
的view
需要先知道混合view
的下一層元素是什么,然后再結合上層的view
進行Blend混色處理。透明動畫、淡入淡出和陰影等效果都涉及到某種透明度,這就會造成了過度繪制。可以通過減少渲染這些透明對象來改善過度繪制。比如:在TextView
上設置帶透明度alpha
值的黑色文本可以實現灰色的效果。但是,直接通過設置灰色的話能夠獲得更好的性能
使用ConstraintLayout減少布局層級
ConstraintLayout
,可以翻譯為約束布局,在2016年Google I/O 大會上發布。ConstraintLayout
相比RelativeLayout
,其性能更好,也更容易使用。連官方的hello world都用ConstraintLayout
來寫了。所以極力推薦使用ConstraintLayout
來編寫布局
關于ConstraintLayout
如何使用,推薦一篇文章講的非常詳細:http://www.lxweimin.com/p/17ec9bd6ca8a,所以這里就不過多介紹了。當你熟練使用它之后,相信我,你再也不想用其他布局了!
使用merge標簽減少布局層級
我們通過兩個例子來認識merge
標簽
- 自定義view時使用
merge
標簽
比如我們現在要寫一個自定義viewGroup繼承自RelativeLayout
public class MyViewGroup extends RelativeLayout {
public MyView(Context context) {
this(context, null, 0);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
public void initView() {
LayoutInflater.from(getContext()).inflate(R.layout.layout_my_view, this, true);
}
}
我們通過LayoutInflater
將XML加載出view并添加到這個自定義view的根布局中,這時候我們的XML文件就可以這么寫。我們在根布局中使用了merge標簽,就代表這個xml文件的根布局就是其parent,也就是我們上面的MyViewGroup
,這樣相比在根布局中使用RelativeLayout
就減少了一個布局層級
這里有一個細節需要注意:當我們使用merge
標簽時,如果我們希望在Design窗口中實時預覽布局效果,我們需要使用 tools:parentTag="android.widget.RelativeLayout"
來告訴AndroidStudio你的父布局是什么
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:parentTag="android.widget.RelativeLayout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="hello world" />
</merge>
- 有時候我們會通 過
include
標簽來提高布局的復用性,如果layout_include_xx.xml
的布局和其父布局使用的是同一個布局類型,如線性布局等。這時候就可以在layout_include_xx.xml
中使用merge
標簽來減少布局層級
使用ViewStub標簽延遲加載
ViewStub
是一個不可見的View
類,用于在運行時按需懶加載資源,只有在代碼中調用了viewStub.inflate()
或者viewStub.setVisible(View.visible)
方法時才內容才變得可見。這里需要注意的一點是,當ViewStub
被inflate
到parent時,ViewStub
就被remove掉了,即當前view hierarchy中不再存在ViewStub
,而是使用對應的layout
視圖代替
<ViewStub
android:id="@+id/view_stub"
android:layout="@layout/layout_error_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
通常用于不常使用的控件,如
- 網絡請求失敗的提示
- 列表為空的提示
- 新內容、新功能的引導,因為引導基本上只顯示一次
- 又或者我們寫了一個通用的自定義 View,但其中部分子 View 只在部分情況下才顯示
ViewStub
標簽使用注意點:
ViewStub
標簽不支持merge
標簽。因此這有可能導致加載出來的布局存在著多余的嵌套結構,具體如何去取舍就要根據各自的實際情況來決定了ViewStub
的inflate
只能被調用一次,第二次調用會拋出異常雖然
ViewStub
是不占用任何空間的,但是每個布局都必須要指定layout_width
和layout_height
屬性,否則運行就會報錯
減少自定義View的過度繪制,使用clipRect()
下面我們自定義一個View用來顯示多張重疊的圖片,效果圖如下:
其onDraw()
方法也很簡單,就是遍歷所有圖片,然后繪制出來:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < imgs.length; i++) {
canvas.drawBitmap(imgs[i], i * 100, 0, mPaint);
}
}
顯示過度繪制區域:
過度繪制比較嚴重,那么如何解決?
我們先來分析一下為什么會出現過度繪制:以第一張圖為例,上面的代碼會把整張圖都繪制出來了,第二張在第一張上面繼續繪制,這就造成了過度繪制
那么,解決辦法也很簡單,對于前面的n-1張圖,我們只需要繪制一部分即可,對于最后一張才繪制完整的。
Canvas
中的clipRect()
方法能夠設置一個裁剪矩形,只在這個矩形區域內的內容才能夠繪制出來
優化后的代碼如下:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < imgs.size(); i++) {
canvas.save();
if (i < imgs.size() - 1) {
//前面的n-1張圖,只裁剪一部分
canvas.clipRect(i * 100, 0, (i + 1) * 100, imgs.get(i).getHeight());
} else if (i == imgs.size() - 1) {
//最后一張,完整的
canvas.clipRect(i * 100, 0, i * 100 + imgs.get(i).getWidth(), imgs.get(i).getHeight());
}
canvas.drawBitmap(imgs.get(i), i * 100, 0, mPaint);
canvas.restore();
}
}
優化后的效果圖如下:
所有區域都是藍色的,即只有1次過度繪制。
Canvas
除了clipRect()
方法外,還有clipPath()
等方法,優化時選擇合理的方法去裁剪即可
總結
布局加載優化主要從IO和反射為突破口,也可以通過異步加載從側面環境這個問題。而布局繪制優化致力于解決過度繪制問題。本系列文章(布局優化)到此就結束了,希望對你有所幫助