Android布局優化(五)繪制優化—避免過度繪制

如需轉載請評論或簡信,并注明出處,未經允許不得轉載

目錄

前言

本系列的前面幾篇文章我們介紹了布局加載的原理及優化,布局加載完成后(生成VIew對象)就要進行視圖繪制,我們知道,android要求每幀的繪制時間不超過16ms,不然就會導致丟幀及應用卡頓。所以本文將會介紹一些布局繪制優化技巧

如何監控應用渲染速度

點擊設置—>開發人員選項—>監控—>GPU呈現模式分析,然后選擇 在屏幕上顯示為條形圖 即可以看到一個圖表,如下圖所示

1.沿水平軸的每個豎條都代表一個幀,每個豎條的高度表示渲染該幀所花的時間(單位:毫秒)
2.水平綠線表示 16 毫秒。 要實現每秒 60 幀,代表每個幀的豎條需要保持在此線以下。 當豎條超出此線時,可能會使動畫出現暫停

再來看下每個豎條的顏色代表什么意思:

分析從哪些方向進行繪制優化

從GPU呈現模式分析可以看出來,我們能夠進行優化的點主要就是測量、布局、繪制、動畫和輸入處理

  1. 測量、布局、繪制過程都會存在自頂而下遍歷過程,所以如果布局的層級過多,這會占用額外的CPU資源
  2. 當屏幕上的某個像素在同一幀的時間內被繪制了多次(Overdraw),這會浪費大量的CPU以及GPU資源
  3. 在繪制過程,也就是onDraw()方法內,我們應該盡量避免局部對象的創建,因為onDraw()方法在繪制過程中會多次調用,大量的局部變量可能會造成內存抖動
  4. 合理使用動畫,這個本章不做討論,有興趣的可以自己了解動畫的相關知識
  5. 不應該在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為準

我們可以通過下面兩個方法來解決這個問題

  1. 在xml中設置
 <item name="android:windowBackground">@null</item>

通過代碼設置

 getWindow().setBackgroundDrawable(null);

移除控件中不需要的背景

例子:

  1. 列表頁(RecyclerView) 與 其內子控件(Item)的背景相同,故可移除子控件(Item)布局中的背景
  2. 對于1個ViewPager+多個 Fragment 組成的首頁界面,若每個Fragment 都設有背景色,即 ViewPager 則無必要設置,可移除

所以對于控件背景顏色的設置基本可以歸納為以下兩個原則:

  1. 對于子控件,如果其背景顏色跟父布局一致,那么就不用再給子控件添加背景了
  2. 如果子控件背景五顏六色,且能夠完全覆蓋父布局,那么父布局就可以不用添加背景了

減少透明度的使用

對于不透明的view,只需要渲染一次即可把它顯示出來。但是如果這個view設置了alpha值,則至少需要渲染兩次。這是因為使用了alphaview需要先知道混合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標簽

  1. 自定義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>
  1. 有時候我們會通 過include標簽來提高布局的復用性,如果layout_include_xx.xml的布局和其父布局使用的是同一個布局類型,如線性布局等。這時候就可以在layout_include_xx.xml中使用merge標簽來減少布局層級

使用ViewStub標簽延遲加載

ViewStub是一個不可見的View類,用于在運行時按需懶加載資源,只有在代碼中調用了viewStub.inflate()或者viewStub.setVisible(View.visible)方法時才內容才變得可見。這里需要注意的一點是,當ViewStubinflate到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標簽使用注意點:

  1. ViewStub標簽不支持merge標簽。因此這有可能導致加載出來的布局存在著多余的嵌套結構,具體如何去取舍就要根據各自的實際情況來決定了

  2. ViewStubinflate只能被調用一次,第二次調用會拋出異常

  3. 雖然ViewStub是不占用任何空間的,但是每個布局都必須要指定layout_widthlayout_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反射為突破口,也可以通過異步加載從側面環境這個問題。而布局繪制優化致力于解決過度繪制問題。本系列文章(布局優化)到此就結束了,希望對你有所幫助

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容