Android性能編程中的幾個方面

Android性能優(yōu)化

談Android性能優(yōu)化,總結(jié)起來分為四大問題:流暢、穩(wěn)定、省電、省流量。

1、流暢

我們試著分析下APP操作起來感覺不流暢的原因:1、因?yàn)榫W(wǎng)絡(luò)請求而等待感覺卡頓;2、繪制頁面時無法及時顯示而感覺卡頓。第一個原因受當(dāng)時用戶所在的環(huán)境影響較大,可能網(wǎng)絡(luò)較差或者是服務(wù)器處理時間過長等,這個時候優(yōu)化就得從網(wǎng)絡(luò)數(shù)據(jù)緩存、壓縮傳輸數(shù)據(jù)、使用DNS服務(wù)、后臺服務(wù)器優(yōu)化等方面著手。但是這方面APP端能做的有限,我們的重點(diǎn)還是關(guān)注頁面繪制優(yōu)化。

1.1、Android繪制原理

APP通過對View樹的遞歸完成測量、布局和繪制之后會通過IPC將這一幀的數(shù)據(jù)發(fā)送給SurfaceFlinger服務(wù)進(jìn)程,SurfaceFlinger拿到數(shù)據(jù)之后開始進(jìn)行渲染,然后再刷新屏幕。這里需要注意的是屏幕的刷新機(jī)制:
Android每16ms發(fā)送VSNCY同步信號來發(fā)起一次屏幕的刷新。在顯示內(nèi)容的數(shù)據(jù)內(nèi)存上采用了雙緩沖機(jī)制,一個為前臺緩沖區(qū),一個為后臺緩沖區(qū)。只有當(dāng)另一個緩沖區(qū)準(zhǔn)備好數(shù)據(jù)之后才會通知顯示設(shè)備切換緩沖區(qū)中的數(shù)據(jù),這樣能有效的解決屏幕閃爍的問題。在收到VSNCY信號時,假如當(dāng)前開始渲染幀A的數(shù)據(jù),同時CPU開始處理下一幀B的數(shù)據(jù),當(dāng)下一次VSNCY信號到來時,顯示設(shè)備應(yīng)該切換至幀B,這時如果CPU還沒處理完幀B的數(shù)據(jù),那么顯示設(shè)備依然只能顯示A,也就是發(fā)生了丟幀的情況(從直觀角度上來看就是卡頓)。當(dāng)然如果渲染幀A用時時超過了16ms,同樣的也會發(fā)生丟幀的情況。
那么通過簡單的了解了繪制原理,我們就可以知道了導(dǎo)致卡頓的原因是:

  • 繪制一幀需要耗費(fèi)很長時間
  • 主線程在16ms內(nèi)無法完成數(shù)據(jù)的準(zhǔn)備
1.2、優(yōu)化繪制內(nèi)容
1.2.1、布局優(yōu)化
  • 減少層級
    因?yàn)閷蛹壴缴伲琕iewTree在遞歸測量和繪制的時間就會越短。所以我們可以通過合理的使用RelativeLayout和LineaLayout,合理使用Merge標(biāo)簽(merge標(biāo)簽只能作為復(fù)用布局的root元素來使用)。關(guān)于查看layout層級關(guān)系時,我們可以通過hierarchy View工具來查看。

    • linearLayout
      如果設(shè)置了weight(權(quán)重的話)會測量兩次 第一次是測量子View的寬高;第二次的測量,父視圖會把剩余的寬度按照weight值的大小平均分配給相應(yīng)的子視圖。
    • relativeLayout
      會進(jìn)行兩次測量 分別是垂直方向和水平方向 來確定View的位置
  • 加快顯示
    使用ViewStub標(biāo)簽,只有顯示調(diào)用顯示方法時才會加載。但只能加載一次并且加載顯示之后不能在通過ViewStub進(jìn)行操作了。

  • 布局復(fù)用
    使用include標(biāo)簽來引入一個統(tǒng)一的布局,方便維護(hù)修改。

1.2.2、避免過度繪制

過度繪制是指在屏幕上的某個像素在同一幀的時間內(nèi)被繪制了多次。簡單點(diǎn)就是看不見的部分就不用繪制了。例如兩個View有重疊的部分,被覆蓋的部分UI就不用繪制了。一般產(chǎn)生過度繪制的主要原因有:

  • xml布局時控件有重疊并且都要設(shè)置自己的背景
  • 自定義控件時同一個區(qū)域被繪制了多次
    在自定義View時,如果發(fā)現(xiàn)有重疊部分,可以使用clipRect()來控制繪制的區(qū)域。

檢測是否過度繪制,可以使用開發(fā)者模式開發(fā)過度繪制選項(xiàng),就可以看到過度繪制的區(qū)域。

1.3、檢測卡頓

我們?nèi)绾尾拍軝z測到UI是否發(fā)生了卡頓現(xiàn)象呢?如果你了解handler機(jī)制的話,那自然也就知道Looper對象。檢測和監(jiān)控卡頓我們可以利用Looper中的Printer對象來實(shí)現(xiàn)。

 public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            return;
        }

        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

        final long traceTag = me.mTraceTag;
        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        final long end;
        try {
            msg.target.dispatchMessage(msg);
            end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        if (slowDispatchThresholdMs > 0) {
            final long time = end - start;
            if (time > slowDispatchThresholdMs) {
                Slog.w(TAG, "Dispatch took " + time + "ms on "
                        + Thread.currentThread().getName() + ", h=" +
                        msg.target + " cb=" + msg.callback + " msg=" + msg.what);
            }
        }

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        final long newIdent = Binder.clearCallingIdentity();
        msg.recycleUnchecked();
    }
}

在每個message處理的前后都會調(diào)用Printer.println()方法,如果主線程卡住了說明dispatchMessage內(nèi)部出現(xiàn)了問題。根據(jù)這個我們就能檢測出UI是否發(fā)生了卡頓。

2、內(nèi)存優(yōu)化

內(nèi)存優(yōu)化也是性能優(yōu)化中的重要一項(xiàng),當(dāng)內(nèi)存發(fā)生泄漏時就有可能導(dǎo)致OOM,而且當(dāng)內(nèi)存發(fā)生抖動也就是gc頻繁時也會導(dǎo)致卡頓。因?yàn)間c時所有的線程都會停止工作,因此我們需要減少gc的頻率。我們都知道gc的作用的回收無任何引用的對象占據(jù)的內(nèi)存空間,那怎么來判斷對象是否被引用了呢?gc會選擇一些還存活的對象作為內(nèi)存遍歷的根節(jié)點(diǎn)GC Roots,通過對GC Roots的可達(dá)性來判斷是否需要回收。那么什么時候會引起gc呢?
Android系統(tǒng)中為了整個系統(tǒng)的內(nèi)存控制需要,為每一個應(yīng)用都設(shè)置了一個硬性的Dalvik heap size最大限制閥值。

  • 當(dāng)分配內(nèi)存時發(fā)現(xiàn)內(nèi)存不夠的情況下引起的gc;
  • 當(dāng)內(nèi)存達(dá)到一定的閥值時出發(fā)gc;
  • 顯示的調(diào)用gc。

所以我們不應(yīng)該頻繁的顯示的調(diào)用gc。

2.1、避免內(nèi)存泄露

分配出去的內(nèi)存,當(dāng)不需要時對這部分內(nèi)存沒有收回,這時就發(fā)送了內(nèi)存泄露。常見的內(nèi)存泄露場景:

  • 資源性對象未關(guān)閉
    資源性對象(coursor、file等),在不使用的時候應(yīng)該及時關(guān)閉他們。
  • 注冊對象未注銷
    如果事件注冊后未注銷,會導(dǎo)致觀察者列表中維持著對象的引用,阻止垃圾回收,一般發(fā)生在注冊廣播接收器、注冊觀察者等。
  • 類的靜態(tài)變量持有大數(shù)據(jù)對象
    靜態(tài)變量長期維持對象的引用,阻止垃圾回收,會造成內(nèi)存不足等問題。
  • 非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例
    非靜態(tài)內(nèi)部類會維持一個到外部類實(shí)例的引用,如果非靜態(tài)內(nèi)部類的實(shí)例是靜態(tài)的,就會間接長期維持著外部類的引用,阻止被系統(tǒng)回收。
  • Handler臨時性內(nèi)存泄露
    Message發(fā)出之后會存放在messageQueue中,如果這個message還沒來的及處理,Activity退出了,但是message對象的target是指向handle,這樣就會導(dǎo)致Handler無法回收。如果這個handler是非靜態(tài)的,還會導(dǎo)致Activity不會別回收。
  • 容器中的對象沒清理造成的內(nèi)存泄露
    通常把一些對象的引用加入集合中,在不需要該對象時,如果沒有把它的引用從集合中清理掉,這個集合就會越來越大。
  • webView內(nèi)存泄露
    我們都知道WebView都存在內(nèi)存泄露的問題,在應(yīng)用中只要使用一次webview,那這部分的內(nèi)存就不會釋放掉。通常解決方案就是為它單獨(dú)開啟一個進(jìn)程。

2.2、內(nèi)存優(yōu)化

當(dāng)然是不是沒有內(nèi)存泄露就可以了呢?不是的,前面也提到了每個應(yīng)用的內(nèi)存使用都是有限制的,所以在內(nèi)存的使用上我們就得注意了。

  • 減少不必要的內(nèi)存開銷;
    盡量使用自動裝箱對象,這樣能避免創(chuàng)建相對應(yīng)對象;在內(nèi)存上根據(jù)相應(yīng)場景對內(nèi)存進(jìn)行復(fù)用,比如視圖的復(fù)用(listView)、Bitmap對象的復(fù)用。

  • 使用最優(yōu)的數(shù)據(jù)類型
    ArrayMap相對于HashMap而言避免了過多的內(nèi)存開銷,因?yàn)锳rrayMap內(nèi)部使用兩個小數(shù)組實(shí)現(xiàn)。還有比如SpareArray等。

  • 避免使用枚舉
    我們知道枚舉在轉(zhuǎn)換成class時都是靜態(tài)常量,相比于普通常量占用內(nèi)存要高很多??梢允褂肁ndroid提供的注解IntDef、StringDef等來實(shí)現(xiàn)類型安全。

  • 圖片內(nèi)存優(yōu)化
    圖片在移動開發(fā)中占用的內(nèi)存是非常突出的,那么關(guān)于圖片的內(nèi)存優(yōu)化我們能做些什么呢?一般從壓縮圖片來減少內(nèi)存的使用:降低位圖的規(guī)格(RGB_8888/RGB_565等規(guī)格);根據(jù)需要縮放圖片和壓縮圖片質(zhì)量等。

2.3、內(nèi)存泄露檢測

  • 借助第三方庫LeakCanary檢測內(nèi)存泄露
  • 借助android studio提供的Android profile工具分析內(nèi)存

3、數(shù)據(jù)存儲優(yōu)化

數(shù)據(jù)的存儲方式主要分為ContentProvider、文件、sharedPreference及sqlite數(shù)據(jù)庫四種數(shù)據(jù)存儲方式。這里主要看SQLite數(shù)據(jù)庫的一些關(guān)于性能的注意點(diǎn)。

  • 使用SQLiteStatement插入數(shù)據(jù)
    與使用普通的執(zhí)行executeSql()而言,它的插入數(shù)據(jù)花費(fèi)的時間更短,而且能在一定的程度上防止SQL語句的注入。
  • 使用事務(wù)
    頻繁的插入數(shù)據(jù),在沒有顯示的創(chuàng)建事務(wù)時,插入時會頻繁的創(chuàng)建事務(wù)會影響插入的效率。顯式創(chuàng)建事務(wù)時能更快的插入數(shù)據(jù),另外開啟事務(wù)能夠保證原子性提交。

4、代碼優(yōu)化

盡管我們平時在寫代碼的時候需要時刻注意了代碼編寫規(guī)范,但是還是會有些小問題存在。那么我們就需要借助一些代碼靜態(tài)掃描工具來檢測我們的代碼,比使用Android studio自帶的Android Lint工具、findbugs等。當(dāng)然我們可能也需要收集一些我們代碼沒有捕獲到的異常,我們可以利用Java虛擬機(jī)為每個進(jìn)程都設(shè)置了一個UNcaughtExceptionHandler,實(shí)現(xiàn)這個接口就能收集到?jīng)]有捕獲的異常,然后返回到我們的服務(wù)器,根據(jù)這些異常再進(jìn)行定位問題。

5、耗電優(yōu)化

我們可以借助google提供的Android系統(tǒng)電量分析工具Battery Historian,通過這個可以直觀的展示出手機(jī)的電量消耗過程。用法:

1、初始化Battery Historian;

adb shell dumpsys batterystats --enable full-wake-history  
adb dumpsys batterystats --reset  

2、 初始化完成之后,操作需要測量電量的一些場景;
3、保存數(shù)據(jù)

adb bugreport > bugreport.txt

保存完數(shù)據(jù)之后,可以借助Battery Historian工具來打開文件查看具體的耗電情況。

最后編輯于
?著作權(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)容