【Android 性能優化】—— UI篇

圖片來自別樣網

本文同時發布在CSDN上,歡迎查看

1. 前言

隨著最近幾年移動市場蓬勃發展,引來大批人員投入到Android、IOS的開發前線,與此同時全國各大培訓機構每月都培養出成千上萬名號稱擁有2到3年工作經驗的開發者。當然,這都已經不是什么秘密了,從目前來看,中國IT行業的主力軍基本上都走過培訓的道路。

但問題是,這號稱23年工作經驗者,使招聘單位錯誤的認為:23年開發經驗和剛剛結束的培訓經歷,基本上劃等號。這就導致了企業大幅度提高用人標準,造成了為何如今移動開發市場依舊火熱,但是工作卻不好找的現狀。

最悲慘的例子恐怕就是前幾年IOS如日中天,可時間就過了一年開發人員就出現了井噴的情況,大量IOS開發者找不到工作。

總的來說:工作機會的確是很多,但是企業把用人要求都大大提高了。如何在萬千人群中脫穎而出,走上人生巔峰,迎娶白富美,沒有亮點,是萬萬不行滴。。。

接下來我就一起學習Android UI優化吧

2. Android渲染機制分析

大家在開發應用的時候或多或少都遇到過可感知的界面卡頓現象,尤其是在布局層次嵌套太多,存在不必要的繪制,或者onDraw方法中執行了過多耗時操作、動畫執行的次數過多等情況下,很容易造成此類情況。如今APP設計都要求界面美觀、擁有更多的動畫、圖片等時尚元素從而打造良好的用戶體驗。但是大量復雜的渲染工作很可能造成Android系統壓力過大,無法及時完成渲染工作。那么多久執行一次渲染,才能讓界面流暢運行呢?

一圖勝千言

如上圖所示,Android系統每隔16ms就會發送一個VSYNC信號(VSYNC:vertical synchronization 垂直同步,幀同步),觸發對UI進行渲染,如果每次渲染都成功,這樣就能夠達到流暢的畫面所需要的正常幀率:60fps。一旦這時候系統正在做大于16ms的耗時操作,系統就會無法響應VSYNC信號,執行渲染工作,導致發生丟幀現象。

大家在察覺到APP卡頓的時候,可以看看logcat控制臺,會有drop frames類似的警告
本引用來自: Android UI性能優化實戰 識別繪制中的性能問題

丟幀啦。。。。

例如上圖所示:如果你的某個操作花費時間是24ms,系統在得到VSYNC信號的時候就無法進行正常渲染,只能等待下一個VSYNC信號(第二個16ms)才能執行渲染工作。那么用戶在32ms內看到的會是同一幀畫面。(我就是感覺google給的圖給錯了,明明是 32ms,怎么給標了一個34ms,難道是有其他寓意我沒有理解上去???)

用戶容易在UI執行動畫、ListView、RecyclerView滑動的時候感知到界面的卡頓與不流暢現象。所以開發者一定要注意在設計布局時不要嵌套太多層,多使用 include方法引入布局。同時不要讓動畫執行次數太多,導致CPU或者GPU負載過重

看到這里同學可能會疑問:為什么是16ms渲染一次,和60fps有什么關系呢?下面讓我們看一下原理:

16ms意味著著1000/60hz,相當于60fps。

那么只要解釋為什么是60fps,這個問題就迎刃而解:

這是因為人眼和大腦之間的寫作無法感知超過60fps的畫面更新,12fps大概類似手動快速翻動書籍的幀率,這是明顯可以感知到不夠順滑的。
24fps使得人眼感知的是連續的線性運動,這其實是歸功于運動模糊效果,24fps是電影膠圈通常使用的幀率,因為這個幀率已經足夠支撐大部分電影畫面需要表達的內容,同時能夠最大的減少費用支出。
但是低于30fps是
無法順暢表現絢麗的畫面內容的,此時就需要用到60fps來達到想要的效果,當然超過60fps是沒有必要的
本引用來源:Google 發布 Android 性能優化典范 - 開源中國社區

3.1 界面卡頓的主要元兇—— 過度繪制(Overdraw)

3.1 什么是過度繪制?

過渡繪制是指屏幕上某個像素在同一幀的時間內繪制了多次。在多層次的UI結構里面,如果不可見的UI也在做繪制操作,這就會導致某些像素區域被繪制了多次,這就是很大程度上浪費了CPU和GPU資源。最最常見的過度繪制,就是設置了無用的背景顏色!!!

3.2 如何發現過度繪制?

對于Overdraw這個問題還是很容易發現的,我們可以通過以下步驟打開顯示GPU過度繪制(Show GPU Overrdraw)選項

設置 -> 開發者選項 -> 調試GPU過度繪制 -> 顯示GPU過度繪制

打開以后之后,你會發現屏幕上有各種顏色,此時你可以切換到需要檢測的程序與界面,對于各個色塊的含義,請看下圖:

Overdraw的參考圖

藍色,淡綠,淡紅,深紅代表了4種不同程度的Overdraw情況,
藍色: 意味著overdraw 1倍。像素繪制了兩次。大片的藍色還是可以接受的(若整個窗口是藍色的,可以擺脫一層)。
綠色: 意味著overdraw 2倍。像素繪制了三次。中等大小的綠色區域是可以接受的但你應該嘗試優化、減少它們。
淡紅: 意味著overdraw 3倍。像素繪制了四次,小范圍可以接受。
深紅: 意味著overdraw 4倍。像素繪制了五次或者更多。這是錯誤的,要修復它們。
我們的目標就是盡量減少紅色Overdraw,看到更多的藍色區域。

3.3 解決問題的工具和方法

通過Hierarchy Viewer去檢測渲染效率,去除不必要的嵌套
通過Show GPU Overdraw去檢測Overdraw,最終可以通過移除不必要的背景。

4. UI優化實踐

4.1 移除不必要的background

(由于公司項目還處于保密階段,所以摘取了Android UI性能優化實戰 識別繪制中的性能問題的部分示例)
下面看一個簡單的展示ListView的例子:

  • activity_main
?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:paddingLeft="@dimen/activity_horizontal_margin"
              android:paddingRight="@dimen/activity_horizontal_margin"
              android:background="@android:color/white"
              android:paddingTop="@dimen/activity_vertical_margin"
              android:paddingBottom="@dimen/activity_vertical_margin"
              android:orientation="vertical"
    >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="@dimen/narrow_space"
        android:textSize="@dimen/large_text_size"
        android:layout_marginBottom="@dimen/wide_space"
        android:text="@string/header_text"/>

    <ListView
        android:id="@+id/id_listview_chats"
        android:layout_width="match_parent"
        android:background="@android:color/white"
        android:layout_height="wrap_content"
        android:divider="@android:color/transparent"
        android:dividerHeight="@dimen/divider_height"/>
</LinearLayout>
  • 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="match_parent"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/chat_padding_bottom">

    <ImageView
        android:id="@+id/id_chat_icon"
        android:layout_width="@dimen/avatar_dimen"
        android:layout_height="@dimen/avatar_dimen"
        android:src="@drawable/joanna"
        android:layout_margin="@dimen/avatar_layout_margin" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/darker_gray"
        android:orientation="vertical">

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@android:color/white"
            android:textColor="#78A"
            android:orientation="horizontal">

            <TextView xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:padding="@dimen/narrow_space"
                android:text="@string/hello_world"
                android:gravity="bottom"
                android:id="@+id/id_chat_name" />

            <TextView xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:textStyle="italic"
                android:text="@string/hello_world"
                android:padding="@dimen/narrow_space"
                android:id="@+id/id_chat_date" />
        </RelativeLayout>

        <TextView xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="@dimen/narrow_space"
            android:background="@android:color/white"
            android:text="@string/hello_world"
            android:id="@+id/id_chat_msg" />
    </LinearLayout>
</LinearLayout>
  • Activity的代碼
package com.zhy.performance_01_render;

import android.os.Bundle;
import android.os.PersistableBundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

/**
 * Created by zhy on 15/4/29.
 */
public class OverDrawActivity01 extends AppCompatActivity
{
    private ListView mListView;
    private LayoutInflater mInflater ;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_overdraw_01);

        mInflater = LayoutInflater.from(this);
        mListView = (ListView) findViewById(R.id.id_listview_chats);

        mListView.setAdapter(new ArrayAdapter<Droid>(this, -1, Droid.generateDatas())
        {
            @Override
            public View getView(int position, View convertView, ViewGroup parent)
            {

                ViewHolder holder = null ;
                if(convertView == null)
                {
                    convertView = mInflater.inflate(R.layout.chat_item,parent,false);
                    holder = new ViewHolder();
                    holder.icon = (ImageView) convertView.findViewById(R.id.id_chat_icon);
                    holder.name = (TextView) convertView.findViewById(R.id.id_chat_name);
                    holder.date = (TextView) convertView.findViewById(R.id.id_chat_date);
                    holder.msg = (TextView) convertView.findViewById(R.id.id_chat_msg);
                    convertView.setTag(holder);
                }else
                {
                    holder = (ViewHolder) convertView.getTag();
                }

                Droid droid = getItem(position);
                holder.icon.setBackgroundColor(0x44ff0000);
                holder.icon.setImageResource(droid.imageId);
                holder.date.setText(droid.date);
                holder.msg.setText(droid.msg);
                holder.name.setText(droid.name);

                return convertView;
            }

            class ViewHolder
            {
                ImageView icon;
                TextView name;
                TextView date;
                TextView msg;
            }

        });
    }


}

實體的代碼

package com.zhy.performance_01_render;

import java.util.ArrayList;
import java.util.List;

public class Droid
{
    public String name;
    public int imageId;
    public String date;
    public String msg;


    public Droid(String msg, String date, int imageId, String name)
    {
        this.msg = msg;
        this.date = date;
        this.imageId = imageId;
        this.name = name;
    }

    public static List<Droid> generateDatas()
    {
        List<Droid> datas = new ArrayList<Droid>();

        datas.add(new Droid("Lorem ipsum dolor sit amet, orci nullam cra", "3分鐘前", -1, "alex"));
        datas.add(new Droid("Omnis aptent magnis suspendisse ipsum, semper egestas", "12分鐘前", R.drawable.joanna, "john"));
        datas.add(new Droid("eu nibh, rhoncus wisi posuere lacus, ad erat egestas", "17分鐘前", -1, "7heaven"));
        datas.add(new Droid("eu nibh, rhoncus wisi posuere lacus, ad erat egestas", "33分鐘前", R.drawable.shailen, "Lseven"));

        return datas;
    }


}

現在的效果是:



注意,我們的需求是整體是Activity是個白色的背景。
開啟Show GPU Overdraw以后:



對比上面的參照圖,可以發現一個簡單的ListView展示Item,竟然很多地方被過度繪制了4X 。 那么,其實主要原因是由于該布局文件中存在很多不必要的背景,仔細看上述的布局文件,那么開始移除吧。
  • 不必要的Background 1

我們主布局的文件已經是background為white了,那么可以移除ListView的白色背景

  • 不必要的Background 2

Item布局中的LinearLayout的android:background="@android:color/darker_gray"

  • 不必要的Background 3

Item布局中的RelativeLayout的android:background="@android:color/white"

  • 不必要的Background 4

Item布局中id為id_msg的TextView的android:background="@android:color/white"

這四個不必要的背景也比較好找,那么移除后的效果是:



對比之前的是不是好多了~~~接下來還存在一些不必要的背景,你還能找到嗎?

  • 不必要的Background 5

這個背景比較難發現,主要需要看Adapter的getView的代碼,上述代碼你會發現,首先為每個icon設置了背景色(主要是當沒有icon圖的時候去顯示),然后又設置了一個頭像。那么就造成了overdraw,有頭像的完全沒必要去繪制背景,所有修改代碼:

Droid droid = getItem(position);
                if(droid.imageId ==-1)
                {
                    holder.icon.setBackgroundColor(0x4400ff00);
                    holder.icon.setImageResource(android.R.color.transparent);
                }else
                {
                    holder.icon.setImageResource(droid.imageId);
                    holder.icon.setBackgroundResource(android.R.color.transparent);
                }

ok,還有最后一個,這個也是非常容易被忽略的。

  • 不必要的Background 6

記得我們之前說,我們的這個Activity要求背景色是白色,我們的確在layout中去設置了背景色白色,那么這里注意下,我們的Activity的布局最終會添加在DecorView中,這個View會中的背景是不是就沒有必要了,所以我們希望調用mDecor.setWindowBackground(drawable);,那么可以在Activity調用getWindow().setBackgroundDrawable(null);

setContentView(R.layout.activity_overdraw_01); 
getWindow().setBackgroundDrawable(null);

ok,一個簡單的listview顯示item,我們一共找出了6個不必要的背景,現在再看最后的Show GPU Overdraw 與最初的比較。


ok,對比參照圖,基本已經達到了最優的狀態。

4.2 使用布局標簽優化布局

4.2.1 <include>標簽

相信大家使用的最多的布局標簽就是 <include>了。 <include>的用途就是將布局中的公共部分提取出來以供其他Layout使用,從而實現布局的優化。這種布局的編寫方式大大便利了開發,個人感覺這種思想和React Native中的面向組件編程思想有著異曲同工之妙,都是將特定功能抽取成為一個獨立的組件,只要控制其中傳入的參數就可以滿局不同的需求。例如:我們在編輯Android界面的時候常常需要添加標題欄,如果在不使用<include>的情況下,只能在每一個需要顯示標題欄的xml文件中編寫重復的代碼,費時費力。但是只要我們將這個需要多次被使用的標題欄布局抽取成一個獨立的xml文件,然后在需要的地方使用<include>標簽引入即可。
下面以在一個布局main.xml中用include引入另一個布局foot.xml為例。main.mxl代碼如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/simple_list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="80dp" />

    <include
        android:id="@+id/my_foot_ly"
        layout="@layout/foot" />

</RelativeLayout>

其中include引入的foot.xml為公用的頁面底部,代碼如下:

?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" 
    android:id="@+id/my_foot_parent_id">

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_40"
        android:layout_above="@+id/title_tv"/>

    <TextView
        android:id="@+id/title_tv"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_40"
        android:layout_alignParentBottom="true"
        android:text="@string/app_name" />
</RelativeLayout>

<include>使用起來很簡單,只需要指定一個layout屬性為需要包含的布局文件即可。當然還可以根據需求指定 android:idandroid:heightandroid:width屬性來覆蓋被引入根節點屬性。

注意
在使用<include>標簽最常見的問題就是 findViewById查找不到<include>進來地控件的跟布局,而這個問題出現的前提就是在include的時候設置了id當設置id后,原有的foot.xml跟布局Id已經被替換為在 <include>中指定的id,所以在 findViewById查找原有id的時候就會報空指針異常。

View titleView = findViewById(R.id.my_foot_parent_id) ; // 此時id已經被覆蓋 titleView 為空,找不到。此時空指針 
View titleView = findViewById(R.id.my_foot_ly) ; //重寫指定id即可

<include>標簽簡單的說就是相當與將layout指定的布局整體引入到main.xml中。所以我們就和操作直接在main.xml中的布局是一樣的只不過有一個上面提到的更布局id被覆蓋的問題。

4.2.2 <ViewStub>標簽

ViewStub標簽同include一樣可以用來引入一個外部布局。不同的是,ViewStub引入的布局默認是不會顯示也不會占用位置的,從而在解析的layout的時候可以節省cpu、內存等硬件資源。

ViewStub常常用來引入那些默認不顯示,只在特定情況下才出現的布局,例如:進度條,網絡連接失敗顯示的提示布局等。
下面以在一個布局main.xml中加入網絡錯誤時的提示頁面network_error.xml為例。main.mxl代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

……
    <ViewStub
        android:id="@+id/network_error_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/network_error" />

</RelativeLayout>

其中network_error.xml為只有在網絡錯誤時才需要顯示的布局,默認不會被解析,示例代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/network_setting"
        android:layout_width="@dimen/dp_160"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="@string/network_setting" />

    <Button
        android:id="@+id/network_refresh"
        android:layout_width="@dimen/dp_160"
        android:layout_height="wrap_content"
        android:layout_below="@+id/network_setting"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/dp_10"
        android:text="@string/network_refresh" />

</RelativeLayout>

在代碼中通過(ViewStub)findViewById(id)找到ViewStub,通過stub.inflate()展開ViewStub,然后得到子View,如下:

private View networkErrorView;

private void showNetError() {
  if (networkErrorView != null) {
    networkErrorView.setVisibility(View.VISIBLE);
  }else{
    ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
    if(stub !=null){
      networkErrorView = stub.inflate();
    
      //  效果和上面是一樣的
      //  stub.setVisibility(View.VISIBLE);   // ViewStub被展開后的布局所替換
      //  networkErrorView =  findViewById(R.id.network_error_layout); // 獲取展開后的布局
    }
 }
}

private void showNormal() {
  if (networkErrorView != null) {
    networkErrorView.setVisibility(View.GONE);
  }
}

在上面showNetError()中展開了ViewStub,同時我們對networkErrorView進行了保存,這樣下次不用繼續inflate。

注意這里我對ViewStub的實例進行了一個非空判斷,這是因為ViewStub在XML中定義的id只在一開始有效,一旦ViewStub中指定的布局加載之后,這個id也就失敗了,那么此時findViewById()得到的值也會是空
viewstub標簽大部分屬性同include標簽類似。

注意:
根據需求我們有時候需要將View的可講性設置為GONE,在inflate時,這個View以及他的字View還是會被解析的。所以使用<ViewStub>就能避免解析其中的指定的布局文件。從而加快布局的解析時間,節省cpu內存等硬件資源。同時ViewStub所加載的布局是不可以使用<merge>標簽的

4.2.3 <merge>標簽

在使用了include后可能會導致布局嵌套太多,導致視圖節點太多,減慢了解析速度。

merge標簽可用于兩種典型情況:

  1. 布局頂接點是FrameLayout并且不需要設置background或者padding等屬性,可使用merge代替,因為Activity內容視圖的parent view就是一個FrameLayout,所以可以用merge消除只能一個。
  2. 某布局作為子布局被其他布局include時,使用merge當作該布局的頂節點,這樣在被引入時,頂結點會自動被忽略,而其自己點全部合并到主布局中。

以【4.2.1 <include>標簽 】中的代碼示例為例,使用用hierarchy viewer查看main.xml布局如下圖:

[引自:Android性能優化系列之布局優化](http://blog.csdn.net/u012124438/article/details/54564659)

可以發現多了一層沒必要的RelativeLayout,將foot.xml中RelativeLayout改為merge,如下:

<?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" >

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_40"
        android:layout_above="@+id/text"/>

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_40"
        android:layout_alignParentBottom="true"
        android:text="@string/app_name" />

</merge>

運行后再次用hierarchy viewer查看main.xml布局如下圖:
[引自:Android性能優化系列之布局優化](http://blog.csdn.net/u012124438/article/details/54564659)

這樣就不會有多余的RelativeLayout節點了。
參考:
Android UI性能優化實戰 識別繪制中的性能問題
Google 發布 Android 性能優化典范
Android性能優化系列之布局優化

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,462評論 25 708
  • 注意事項: 布局優化;盡量使用include、merge、ViewStub標簽,盡量不存在冗余嵌套及過于復雜布局(...
    HarryXR閱讀 5,260評論 1 19
  • 在說性能優化之前,我們必須了解為什么要做性能優化,首先第一點肯定是為了用戶體驗,你想啊要是你的App用起來很卡...
    PengJunJun閱讀 428評論 0 0
  • 版權聲明:本文為LooperJing原創文章,轉載請注明出處! 優化性能一般從渲染,運算與內存,電量三個方面進行,...
    LooperJing閱讀 23,521評論 20 88
  • 很衰的片子只適合心情愉悅的時候看,當你本身很衰的時候,再看看那些生活的“饋贈”,你懂的。 其實也還好還好,只是每次...
    狐十八閱讀 461評論 0 0