Android性能優(yōu)化摘錄

目錄
一、View的過度繪制(OverDraw)
二、View的繪制流程
三、三種常用布局的比較
四、RecyclerView VS ListView 之View層級關(guān)系
五、高效布局標(biāo)簽
六、去掉window的背景
七、去掉其他不必要的背景
八、ClipRect & QuickReject
九、善用draw9patch
十、慎用Alpha
十一、應(yīng)該早點(diǎn)知道的API
十二、其他

本文是有心課堂-性能優(yōu)化合輯 視頻的學(xué)習(xí)筆記,也翻閱過網(wǎng)上的相關(guān)資料,整理了個人認(rèn)為比較重要的知識點(diǎn)。

一、View的過度繪制(OverDraw)

OverDraw,是指在一幀的時間內(nèi)(16.67ms)像素被繪制了多次,理論上一個像素只繪制一次是最優(yōu)的,但由于重疊的布局導(dǎo)致一些像素被重復(fù)繪制多次,而每次繪制都會對應(yīng)到CPU的一組繪圖命令和GPU的一些操作,當(dāng)這個操作超過16.67ms時就會出現(xiàn)掉幀的現(xiàn)象,即我們常說的卡頓,所以對重疊不可見元素的重復(fù)繪制會產(chǎn)生額外的開銷,我們需要盡量減少OverDraw的發(fā)生。

Android提供了測量OverDraw的選項(xiàng),在開發(fā)者選項(xiàng)->調(diào)試GPU過度繪制(Show GPU OverDraw),打開該選項(xiàng)就可以看到當(dāng)前頁面OverDraw的狀態(tài)。

  • 沒有顏色 : 沒有OverDraw。像素只畫了一次。
  • 藍(lán)色:OverDraw 1倍。像素繪制了兩次,大片的藍(lán)色是可以接受的(若整個窗口都是藍(lán)色,可以擺脫一層)。
  • 綠色:OverDraw 2倍。像素繪制了三次,中等大小的綠色區(qū)域是可以接受的,但你應(yīng)該嘗試去優(yōu)化、減少它們。
  • 淺紅:OverDraw 3倍。像素繪制了四次,小范圍可接受。
  • 暗紅:OverDraw 4倍。像素繪制了五次或更多,這是錯誤的,要修復(fù)它們。
GPU-OverDraw

二、View的繪制流程

  • measure :為整個View樹計(jì)算實(shí)際的大小,即設(shè)置實(shí)際的高(對應(yīng)屬性:mMeasuredHeight)和寬(對應(yīng)屬性:mMeasureWidth),每個View的控件的實(shí)際寬高都是由父視圖和本身的視圖決定的。
  • layout:為將整個根據(jù)子視圖的大小以及布局參數(shù)將View樹放到合適的位置上。
  • draw:利用前兩步得到的參數(shù),將視圖顯示在屏幕上。

三、三種常用布局的比較

RelativeLayout:

優(yōu)點(diǎn):
1. View樹扁平化,減少View層級
2. 使用場景廣
缺點(diǎn):
1. 測量效率稍差
2. 使用略復(fù)雜

LinearLayout:

優(yōu)點(diǎn):
1. 使用非常簡單
2. 測量效率高
缺點(diǎn):
1. 嵌套過多,易導(dǎo)致View層級復(fù)雜
2. 使用場景相對較窄

FrameLayout:

使用場景特殊,有些場景下可以替代RelativeLayout

選擇布局容器的基本準(zhǔn)則:

  • 盡可能的使用RelativeLayout以減少View層級,使View樹趨于扁平化
  • 在不影響層級深度的情況下,使用LinearLayout和FrameLayout,而不是RelativeLayout

四、RecyclerView VS ListView 之View層級關(guān)系

                 | --- RecyclerView
    ViewGroup----|                                    |--- ListView
                 | --- AdapterView --- AbsListView ---|
                                                      |--- GridView

從View層級關(guān)系,我們可以看出,在實(shí)際開發(fā)中更推薦使用RecyclerView。

五、高效布局標(biāo)簽

  • Merge標(biāo)簽:減少視圖的層級結(jié)構(gòu)。

Merage標(biāo)簽一般配合FrameLayout和LinearLayout使用,當(dāng)然RelativeLayout也可以用Merge標(biāo)簽,只是RelativeLayout本身比較復(fù)雜,如果我們也通過Merge標(biāo)簽去添加一些子View的時候很容易弄巧成拙。

另外Merge標(biāo)簽只能作為XML布局的根標(biāo)簽使用,當(dāng)Inflate以<merge/>開頭的布局文件時,必須指定一個父ViewGroup,并且必須設(shè)定attachToRoot為true。

  • ViewStub標(biāo)簽:高效占位符。

我們經(jīng)常會遇到這的情況,運(yùn)行時動態(tài)根據(jù)條件來決定顯示哪個View或者布局。常用的做法是把View都寫在上面,先把他們的可見性都設(shè)為View.GONE,然后在代碼中動態(tài)的更改它的可見性。這樣的做法的優(yōu)點(diǎn)是邏輯簡單而且控制起來比較靈活。但是它的缺點(diǎn)就是耗費(fèi)資源。雖然把View的初始化可見View.GONE,但是在Inflate布局的時候View仍然會被Inflate,也就是說仍然會創(chuàng)建對象,會被實(shí)例化,會被設(shè)置屬性,也就是說會耗費(fèi)內(nèi)存等資源。

ViewStub是一個輕量級的View,它是一個看不見的,不占布局位置,占用資源非常小的控件,可以為ViewStub指定一個布局,在Inflate布局的時候,只有ViewStub會被初始化,然后當(dāng)ViewStub被設(shè)置為可見的時候或是調(diào)用了ViewStub.inflate()的時候,ViewSub所指向的布局會被inflateh和實(shí)例化,然后ViewStub的布局屬性都會傳給它所指向的布局。

<ViewStub
android:id="@+id/stub_view"
android:inflatedId="@+id/panel_stub"
android:layout="@layout/progress_overlay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />

當(dāng)想加載布局時,可以使用下面其中的一種方法:

((ViewStub) findViewById(R.id.stub_view)).setVisibility(View.VISIBLE);
或
View importPanel = ((ViewStub) findViewById(R.id.stub_view)).inflate();
  • Space標(biāo)簽:空白控件。

六、去掉window的背景

在我們使用了Android自帶的一些主題時,window會被默認(rèn)添加一個純色的背景,這個背景是被DecorView持有的,當(dāng)我們的自定義布局時又添加一張背景圖或者設(shè)置背景色,那么DecorView的background此時對我們來說是無用的,但是它會產(chǎn)生一次OverDraw,帶來繪制性能損耗。

去掉window的背景可以在onCreate中的setContentView之后調(diào)用

getWindow().setBackgroundDrawable(null);

或者在theme中添加

android:windowbackground="null";

七、去掉其他不必要的背景

  • 有時候?yàn)榱朔奖銜冉oLayout設(shè)置一個整體的背景,再給子View設(shè)置背景,會造成重疊,如果子View寬度match_parent,可以看到完全覆蓋了Layout的一部分,這里就可以通過分別設(shè)置背景來減少重繪
  • 如果采用的是selector的背景,將normal狀態(tài)的color設(shè)置為"@android:color/transparent"也同樣可以解決問題。

這里只簡單舉兩個例子,我們在開發(fā)過程中的一些習(xí)慣性的思維定式會帶來不經(jīng)意的OverDraw,所以開發(fā)過程中我們?yōu)槟硞€View或者ViewGroup設(shè)置背景的時候,先思考下是否真的有必要,或者思考下這個背景能不能分段設(shè)置在子View上,而不是圖方便直接設(shè)置在根View上。

八、ClipRect & QuickReject

為了解決OverDraw的問題,Android系統(tǒng)會通過避免繪制那些完全不可見的組件來盡量減少消耗,但不幸的是,對于那些過于復(fù)雜的自定義View(通常重寫了OnDraw方法),Android系統(tǒng)無法檢測在OnDraw里面具體會執(zhí)行什么操作,系統(tǒng)無法監(jiān)控并自動優(yōu)化,也就無法避免OverDraw了。但是我們可以通過canvas.clipRect()來幫助系統(tǒng)識別那些可見的區(qū)域,這個方法可以指定一塊矩形區(qū)域,只有在這個區(qū)域內(nèi)才會被繪制,其他的區(qū)域會被忽視,這個API可以很好的幫助那些有多組重疊組件的自定義View來控制顯示的區(qū)域,同時clipRect方法還可以幫助節(jié)約CPU與GPU資源,在clipRect區(qū)域之外的繪制指令都不會被執(zhí)行,那些部分內(nèi)容在矩形區(qū)域內(nèi)的組件,任然會得到繪制。除了clipRect方法之外,我們還可以使用canvas.quickReject()來判斷是否沒和某個矩形相交,從而跳過那些非矩形區(qū)域內(nèi)的繪制操作。

九、善用draw9patch

給ImageView加一個邊框,遇到這種需求,通常在ImageView后面設(shè)置一張背景圖,露出邊框便完美解決問題,此時這個ImageView,設(shè)置了兩次drawable,底下一層僅僅是為了作為圖片的邊框而已,但是兩層drawable的重疊區(qū)域卻繪制了兩次,導(dǎo)致OverDraw。

優(yōu)化方案:將背景drawable制作成draw9patch,并且將和前景重疊的部分設(shè)置為透明,由于Android的2D渲染器會優(yōu)化draw9patch中的透明區(qū)域,從而優(yōu)化了這次OverDraw。但是背景圖片必須制作成draw9patch才行,因?yàn)锳ndroid 2D渲染器只對draw9patch有這個優(yōu)化,否則,一張普通的png,就算你把中間的部分設(shè)置成透明,也不會減少這次OverDraw.

十、慎用Alpha

假如對一個View做Alpha轉(zhuǎn)化,需要先將View繪制出來,然后做Alpha轉(zhuǎn)化,最后將轉(zhuǎn)換的效果在界面上。通俗得說,做Alpha轉(zhuǎn)化就需要對當(dāng)前View繪制兩遍,可想而知,繪制效率會的大打折扣,耗時會翻倍,所以Alpha還是慎用。如果一定要做Alpha轉(zhuǎn)化的話,可以采用緩存的方式。

view.setLayerType(LAYER_TYPE_HARDWARE);
doSmoeThing();
view.setLayerType(LAYER_TYPE_NONE);

通過setLayerType方式可以將當(dāng)前界面緩存在GPU中,這樣不需要每次繪制原始界面,但是GPU內(nèi)存是相當(dāng)寶貴的,所以用完要馬上釋放掉。

十一、應(yīng)該早點(diǎn)知道的API

  1. android:lineSpacingExtra

    設(shè)置行間距,如“3dp”

  2. android:lineSpacingMultiPlier

    設(shè)置行間距的倍數(shù),如“1.2”

  3. android:includeFontPadding="false"

    TextView默認(rèn)上下是有一定的padding的,有時候我們可能不需要上下這部分留白,加上它即可

  4. android:titleMode(BitmapDrawable)

    可以指定圖片使用重復(fù)填充的模式

  5. android:fillViewport

    設(shè)置ScrollView撐滿父容器

  6. ArgbEvaluator類

    實(shí)現(xiàn)豐富的色彩效果,提高體驗(yàn)度。譬如:滑動Viewpager時,背景色漸變;隨著EditText輸入框的長度變化背景色等等

十二、其他

  • 盡量避免過多的使用static變量
  • Avtivity和Activity之間或Fragment和Fragment之間使用Intent、Bundle傳遞數(shù)據(jù)
  • 常量提取到一個單獨(dú)的類中,并注意命名規(guī)范。如Intent傳值的key
  • 善用 Gradle

有時候我們的App會把HOST_URL、DEBUG等常量寫在Constants中,這樣我們在打正式包或測試包除了要修改代碼外,studio還需要重新build一次,是比較耗時的,所以我們可以把一些常量的設(shè)置可以定義到build.gradle文件中


    buildTypes {
        debug {
            buildConfigField("String", "HOST_URL", "\"http://api-test.ganxin.com\"")
            buildConfigField("Boolean", "LOG_DEBUG", "true")
        }
        release {
            buildConfigField("String", "HOST_URL", "\"http://api.ganxin.com\"")
            buildConfigField("Boolean", "LOG_DEBUG", "false")
        }
    }

又或者在AndroidManifest文件中的元素在不同的場景下需要替換不同的值的,可以嘗試用productFlavors,如 :


    productFlavors {
        productive{
            manifestPlaceholders =[
                  LAUCHER_LOGO:"@mipmap/ic_logo_normal" ,
                  CHANNEL_NAME : "normal"
            ]
        }

        customA{
            manifestPlaceholders =[
                    LAUCHER_LOGO:"@mipmap/ic_logo_custom" ,
                    CHANNEL_NAME : "customA"
            ]
        }
    }

在AndroidManifest文件中的引用方式:

android:icon="${LAUCHER_LOGO}"

<meta-data
    android:name="UMENG_CHANNEL"
    android:value="${CHANNEL_NAME}" />
  • 使用遞歸方法的時候避免造成OOM(內(nèi)存溢出)
  • 注意使用Handler造成的內(nèi)存泄漏

一段簡單的使用Handler示例


Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        mImageView.setImageBitmap(mBitmap);
    }
}

a. 當(dāng)使用內(nèi)部類(包括匿名類)來創(chuàng)建Handler的時候,Handler對象會隱式地持有一個外部類對象(通常是一個Activity)的引用(不然怎么可能通過Handler來操作Activity中的View ?)。而Handler通常會伴隨一個耗時的后臺線程(例如網(wǎng)絡(luò)拉取圖片)一起出現(xiàn),這個后臺線程在任務(wù)執(zhí)行完畢之后,通過消息機(jī)制通知Handler,然后Handler把圖片更新到界面,然后用戶在網(wǎng)絡(luò)請求過程中關(guān)閉了Activity,在正常情況下,Activity不再被使用,它就可能在GC檢查時被回收掉,但由于這時線程尚未執(zhí)行完,而該線程持有Handler的引用,這個Handler又持有Activity的引用,就導(dǎo)致該Activity無法被回收(即內(nèi)存泄漏),直到網(wǎng)絡(luò)請求結(jié)束。

b.另外,如果執(zhí)行了Handler的postDelayed()方法,該方法會將你的Handler裝入一個Message,并把這條Message推到MesageQueue中,那么在你設(shè)定的delay到達(dá)之前,會有一條MessageQueue->Message->Handler->Activity的鏈,導(dǎo)致你的Activity被引用而無法被回收。


解決方案:

a. 通過程序邏輯來進(jìn)行保護(hù)

- 在關(guān)閉Activity的時候停掉你的后臺線程。線程停掉了,就相當(dāng)于切斷了Handler和外部連接的線,Activity自然會在合適的時候被回收
- 如果你的Handler是被delay的Message持有了引用,那么使用相應(yīng)的Handler的removeCallbacks()方法,把消息對象從消息對象移除就行

關(guān)于Handler.remove方法:
removeCallbacks(Runnable r) —— 清除r匹配上的Message
removeCallbacks(Runnable r, Object token) —— 清除r匹配且匹配token(Message.obj)的Message,token為空時,只匹配r 
removeCallbacksAndMessage(Object token) —— 清除token匹配的Message
removeMessages(int what) —— 按what來匹配
removeMessages(int what,Object object) —— 按what來匹配

如果需要清除以Handler為target的所有Message(包括Callback),調(diào)用如下方法即可:
handler.removeCallbacksAndMessages(null);

b. 將Handler聲明為靜態(tài)類

靜態(tài)類不持有外部類的對象,所以Activity可以隨意回收。


static class MyHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        mImageView.setImageBitmap(mBitmap);
    }
}

但使用了以上代碼后,會發(fā)現(xiàn),由于Handler不再持有外部類對象的引用,導(dǎo)致程序不允許你在Handler中操作Activity的對象了,所以你需要在Handler中增加一個對Activity的弱引用(WeakReference):


static class MyHandler extends Handler {
    WeakReference<Activity > mActivityReference;

    MyHandler(Activity activity) {
        mActivityReference= new WeakReference<Activity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        final Activity activity = mActivityReference.get();
        if (activity != null) {
            mImageView.setImageBitmap(mBitmap);
        }
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容