1. 程式人生 > >Android UI效能優化—過度繪製篇

Android UI效能優化—過度繪製篇

Android UI效能優化——過度繪製篇

過度繪製(overdraw)

過度繪製介紹

每過幾年,就會有傳聞說某個博物館在用x光掃描一副無價的名畫之後,發現畫作的作者其實重用了老的畫布,在名畫的底下還藏著另一副沒有被發現的畫作。有時候,博物館還能用高階的影象技術來還原畫布上的原作。android裡面的view的繪製就是類似的情況。當android系統繪製螢幕的時候,先畫父view,然後子view,再是更深的子view等等。這會導致所有的view都被繪製到了螢幕上,就像畫家的畫布一樣,這些view都被他們的子view覆蓋住了。

簡單來說,過度繪製就是螢幕上的某個畫素點在同一幀的時間內繪製了多次。

當設計上追求更華麗的視覺效果的時候,我們就容易陷入採用越來越多的層疊元件來實現這種視覺效果的怪圈。這很容易導致大量的效能問題,為了獲得最佳的效能,我們必須儘量減少 Overdraw 的情況發生。

當我們來繪製一個介面時,會有一個 windows,然後是建立 Activity,在 Activity 裡可以建立多個 view,或 view group,view 也可以巢狀 view。這些元件從上到下分佈,上面的元件是可以被使用者看見的,而在下面的元件是不可見的,但是我們依然要花很多時間去繪製那些不可見的元件,因為在某些時候,它也可能會顯示出來。

過度繪製檢測

按照以下步驟開啟 Show GPU Overrdraw 的選項:設定 -> 開發者選項 -> 除錯GPU過度繪製 -> 顯示GPU過度繪製


藍色,淡綠,淡紅,深紅代表了4種不同程度的Overdraw情況,

藍色: 意味著overdraw 1倍。畫素繪製了兩次。大片的藍色還是可以接受的(若整個視窗是藍色的,可以擺脫一層)。
綠色: 意味著overdraw 2倍。畫素繪製了三次。中等大小的綠色區域是可以接受的但你應該嘗試優化、減少它們。
淡紅: 意味著overdraw 3倍。畫素繪製了四次,小範圍可以接受。
深紅: 意味著overdraw 4倍。畫素繪製了五次或者更多。這是錯誤的,要修復它們。

我們的目標就是儘量減少紅色 Overdraw,最理想的是藍色,一個畫素只繪製一次,合格的頁面繪製是白色、藍色為主,綠色以上區域不能超過整個的三分之一,顏色越淺越好。

可能有些人覺得不以為然,覺得沒什麼影響。話又說回來,GPU 繪製過渡對應用造成什麼影響。
實際上,GPU 繪製影響的是介面的流暢度和使用者體驗,對於好的手機可能體驗不到差距,對於差的手機,流暢度卻起著關鍵的作用。

背景過度繪製優化

下面我們來通過一個Demo來檢測過度繪製:

  • activity_overdraw :

    <?xml version="1.0" encoding="utf-8"?>  
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="@android:color/white">
    
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="@dimen/mar_pad_len_30px"
            android:textSize="@dimen/text_size_100px"
            android:text="@string/String_overdraw_list"/>
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/overdraw_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/white"
            android:layout_margin="@dimen/mar_pad_len_30px"/>
    
    </LinearLayout>
    
  • Recyclerview item 的佈局檔案 :

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingTop="@dimen/mar_pad_len_30px"
        android:paddingBottom="@dimen/mar_pad_len_30px">
    
        <ImageView
            android:id="@+id/item_img"
            android:layout_width="@dimen/mar_pad_len_100px"
            android:layout_height="@dimen/mar_pad_len_100px"
            android:layout_gravity="center_vertical"
            android:layout_margin="@dimen/mar_pad_len_20px"
            android:src="@mipmap/ic_launcher" />
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/white"
            android:orientation="vertical"
            android:layout_marginTop="@dimen/mar_pad_len_24px">
    
            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_gravity="center_vertical"
                android:background="@android:color/white">
    
                <TextView
                    android:id="@+id/item_title"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentLeft="true"
                    android:padding="@dimen/mar_pad_len_4px" />
    
                <TextView
                    android:id="@+id/item_time"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentRight="true"
                    android:padding="@dimen/mar_pad_len_4px" />
    
            </RelativeLayout>
    
            <TextView
                android:id="@+id/item_msg"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@android:color/white"
                android:padding="@dimen/mar_pad_len_4px" />
        </LinearLayout>
    
    </LinearLayout>
    
  • Activity 程式碼:

    public class OverdrawActivity extends AppCompatActivity {
    
        private static final String TAG = "OverdrawActivity";
    
        @BindView(R.id.overdraw_list)
        RecyclerView mRecyclerView;
    
        private OverdrawAdapter mAdapter;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_overdraw);
            ButterKnife.bind(this);
            initOverdrawView();
            initOverdrawData();
        }
    
        private void initOverdrawView() {
            mAdapter = new OverdrawAdapter(this);
            mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
            mRecyclerView.setAdapter(mAdapter);
        }
    
        private void initOverdrawData() {
            List<TestItemBean> list = new ArrayList<>();
            for (int i = 0; i < 15; i++) {
                TestItemBean bean = new TestItemBean();
                bean.setTitle("Title " + i);
                bean.setTime("Time " + i);
                bean.setMsg("Msg is ba la ba la ba la ba la ba la!" + i);
                bean.setImgRes(R.mipmap.ic_launcher);
                list.add(bean);
            }
            mAdapter.addItems(list);
        }
    }
    
  • Recyclerview Adapter 程式碼:

    public class OverdrawAdapter extends CommonRecyclerAdapter<TestItemBean> {
    
        public OverdrawAdapter(Context context) {
            super(context);
        }
    
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = mLayoutInflater.inflate(R.layout.layout_recycler_item, parent, false);
            return new OverdrawViewHolder(view, mItemClickListener);
        }
    
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            bindView((OverdrawViewHolder) holder, getContentList().get(position));
        }
    
        private void bindView(OverdrawViewHolder holder, TestItemBean bean) {
            if (holder != null && bean != null) {
                holder.setTitleTxt(bean.getTitle());
                holder.setTimeTxt(bean.getTime());
                holder.setMsgTxt(bean.getMsg());
                holder.setImageView(bean.getImgRes());
            }
        }
    
        class OverdrawViewHolder extends RecyclerViewHolder {
    
            @BindView(R.id.item_title)
            TextView mTitleTxt;
    
            @BindView(R.id.item_time)
            TextView mTimeTxt;
    
            @BindView(R.id.item_msg)
            TextView mMsgTxt;
    
            @BindView(R.id.item_img)
            ImageView mImageView;
    
            public OverdrawViewHolder(View itemView, OnItemClickListener listener) {
                super(itemView, listener);
                ButterKnife.bind(this, itemView);
            }
    
            public void setTitleTxt(String titleTxt) {
                mTitleTxt.setText(titleTxt);
            }
    
            public void setTimeTxt(String timeTxt) {
                mTimeTxt.setText(timeTxt);
            }
    
            public void setMsgTxt(String msgTxt) {
                mMsgTxt.setText(msgTxt);
            }
    
            public void setImageView(int res) {
                mImageView.setImageResource(res);
                mImageView.setBackgroundColor(Color.WHITE);
            }
        }
    }
    
  • 整體效果以及開啟過度繪製效果圖:


對比上面的參照圖,可以發現一個簡單的 RecyclerView 展示 Item,竟然很多地方被過度繪製了4X 。 那麼,其實主要原因是由於該佈局檔案中存在很多不必要的背景,仔細看上述的佈局檔案,那麼開始移除吧。

  • 不必要的Background 1
    主佈局的檔案已經是 background 為 white 了,那麼可以移除ListView的白色背景;

  • 不必要的Background 2
    主佈局的檔案已經是 background 為 white 了,Item主佈局中的 LinearLayout 的白色背景可以移除;

  • 不必要的Background 3
    主佈局的檔案已經是 background 為 white 了,Item 文字佈局中的 LinearLayout 的白色背景可以移除;

  • 不必要的Background 4
    主佈局的檔案已經是 background 為 white 了,Item 文字佈局中的 RelativeLayout 的白色背景可以移除;

  • 不必要的Background 5
    主佈局的檔案已經是 background 為 white 了,Item 文字佈局中下方的 TextView 的白色背景可以移除;

  • 不必要的Background 6
    Adapter的Bingview時,ImageView 的白色背景可以移除;

  • 不必要的Background 7
    最後一個,這個也是非常容易被忽略的,記得我們之前說,我們的這個 Activity 要求背景色是白色,我們的確在 layout 中去設定了背景色白色,那麼這裡注意下,我們的 Activity 的佈局最終會新增在 DecorView 中,這個 View 會中的背景是不是就沒有必要了,所以我們希望呼叫 mDecor.setWindowBackground(drawable);,那麼可以在 Activity 呼叫 getWindow().setBackgroundDrawable(null); 或者 getWindow().setBackgroundDrawableResource(android.R.color.transparent);

    setContentView(R.layout.activity_overdraw);
    getWindow().setBackgroundDrawable(null);
    // getWindow().setBackgroundDrawableResource(android.R.color.transparent);
    

    下面有效果提,ActionBar上的藍色已經消失了。
    這裡有個比較重要注意點,使用了這個後一定要在自己的layout裡面加上背景,有的機型會出現問題。

好了,優化了不必要的背景後,我們來看看優化的效果:

繪圖過度繪製優化

下面我們繼續Demo走起:

  • CardView 程式碼 :

    public class CardView extends View {
    
        private Bitmap[] mCards = new Bitmap[3];
    
        private int[] mImgId = new int[]{R.mipmap.a, R.mipmap.b, R.mipmap.c};
    
        public CardView(Context context) {
            super(context);
            for (int i = 0; i < mCards.length; i++) {
                Bitmap bm = BitmapFactory.decodeResource(getResources(), mImgId[i]);
                mCards[i] = Bitmap.createScaledBitmap(bm, 510, 510, false);
            }
        setBackgroundColor(Color.WHITE);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            canvas.save();
            canvas.translate(50, 200);
            for (Bitmap bitmap : mCards) {
                canvas.translate(150, 0);
                canvas.drawBitmap(bitmap, 0, 0, null);
            }
            canvas.restore();
        }
    }
    
  • Activity 程式碼 :

    public class ClipOverdrawActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(new CardView(this));
        }
    }
    
  • 整體效果以及開啟過度繪製效果圖:


對比上面的參照圖,可以發現一個簡單的三個圖片展示,竟然也過度繪製了4X 。 那麼,其實主要原因是由於該三個圖片draw的時候繪製了全部區域導致互相重疊,仔細看上述的程式碼,那麼開始優化吧。

  • 修改onDraw方法 :

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(50, 200);
        for (int i = 0; i < mCards.length; i++) {
            canvas.translate(150, 0);
            canvas.save();
            if (i < mCards.length - 1) {
                canvas.clipRect(0, 0, 150, mCards[i].getHeight());
            }
            canvas.drawBitmap(mCards[i], 0, 0, null);
            canvas.restore();
        }
        canvas.restore();
    }
    

    分析得出,除了最後一張需要完整的繪製,其他的都只需要繪製部分;所以我們在迴圈的時候,給i到n-1都添加了clipRect的程式碼。

  • 去除windowbackground :

    getWindow().setBackgroundDrawable(null);
    // getWindow().setBackgroundDrawableResource(android.R.color.transparent);
    

優化了以上兩點,可以看一下效果:

所以遇到類似的需要繪製的自定義view時,大家要多注意這方面的問題,這裡只是舉例clipRect方法,還有比如畫兩個圓重疊時,可以畫一個圓以及一個圓環,以此類推,這樣可以大大的減少過度繪製,優化UI效能。

過度繪製(overdraw)優化總結

  • 在佈局中,如果存在多個線性佈局重疊時,可以考慮只針對最上層的佈局設定背景色,而不需要每一個佈局(例如LinearLayout)都設定背景色,過多的相同的背景色會導致過度繪製;
  • 在設計到activity類中,如果牽涉到的佈局存在背景色,可以考慮消除視窗的背景色,減少1X的繪製。
  • 在自定義view的onDraw中,如果涉及到重疊的繪製view時,可以考慮利用區域性繪製避免過度繪製。
  • 考慮到效率和效能問題,介面是有一定重新整理頻率的,每一次重新整理都會呼叫View的onDraw方法,而View提前繪製就是在onDraw中進行,避免在onDraw建立物件,避免在onDraw進行繪製,應在建構函式中畫好,交給onDraw。