天天看點

Android性能優化之啟動速度優化

App啟動卡慢會影響一個App的解除安裝率和使用率。啟動速度快會給人一種輕快的感覺,減少使用者等待時間。如果一個App從點選桌面圖示到看到主界面花了10秒,請問你能接受麼?忍耐不好的估計直接就解除安裝了,或者沒等打開就直接Home鍵按出去,然後殺程序了。這樣一來App解除安裝率提升了,使用率下降了。是以對于有大量使用者的App來說,這些性能細節是很重要的,畢竟使用者就是錢啊

Android性能優化之啟動速度優化

  Android app 啟動速度優化,首先談談為什麼會走到優化這一步,如果一開始建立 app 項目的時候就把這個啟動速度考慮進去,那麼肯定就不需要重新再來優化一遍了。這是因為在移動網際網路時代,大家都追求快,什麼功能都是先做出來再說,其他的可以先不考慮,先占據先機,或者驗證是否值得做。那為什麼要這麼做呢?我個人的觀點有以下幾點

  • 如果 app 不能快速開發出來,先放出去驗證一下可行性,可能連是否值得做都不知道,如果花很長時間做了一個對使用者無價值的功能,那麼還不如不做
  • 如果 app 不能快速做出來,可能被競争對手捕獲先機,那麼可能錯失最佳商業時機
  • 如果一開始就規定不能影響啟動速度的這個目标,那麼做功能的時候就會有束縛,快不起來
  • app 初期大家都忙着開發新功能,疊代新版本,沒有時間停下來做優化
  • 同類型 app 變多,競争對手變多,大家才開始關注啟動性能,才開始做啟動速度優化(有主動出擊也有被動優化)

一、引起性能問題的原因

  随着項目不斷的快速疊代,往往會造成App啟動卡慢現象,因為可能在App主程序啟動階段或者在主界面啟動階段放了很多初始化其他業務的邏輯,而這些業務落地可能一開始并不需要用到。本文從作者的親身經曆給大家闡述啟動速度優化相關的點點滴滴,為啟動速度優化提供一種思路給大家參考。

二、為什麼要做啟動速度優化

  App啟動卡慢會影響一個App的解除安裝率和使用率。啟動速度快會給人一種輕快的感覺,減少使用者等待時間。如果一個App從點選桌面圖示到看到主界面花了10秒,請問你能接受麼?忍耐不好的估計直接就解除安裝了,或者沒等打開就直接Home鍵按出去,然後殺程序了。這樣一來App解除安裝率提升了,使用率下降了。是以對于有大量使用者的App來說,這些性能細節是很重要的,畢竟使用者就是錢啊。

三、分析制定優化技術路線

3.1 分析啟動性能瓶頸

  在具體的優化之前,首先我們得找到需要優化的地方,怎麼找?這就要求了解Android App的啟動原理,我們要知道一個App從點選桌面圖示到我們看到App的主界面整個過程中經過了哪些步驟,哪些地方是我們可以優化的地方。下圖是App啟動過程的一個大概描述。

Android性能優化之啟動速度優化

具體的代碼流程,分析關鍵的函數耗時

Android性能優化之啟動速度優化

  圖中onFirstDrawFinish和onWindowFocusChanged的前後順序可能會颠倒,但是時間差不大。

3.2 制定優化方向

  從上面的分析可以看出,App啟動過程中我們優化的地方包括主程序啟動流程和主界面啟動流程,主程序啟動就是Application的建立過程,主界面啟動就是MainActivity的建立過程。隻需要分别對這兩個部分進行優化即可。

  1. Application中attachBaseContext最早被調用,随後是onCreate方法,盡量在這兩個方法中不要有耗時操作。
  2. MainActivity中重點關注onCreate,onResume,onWindowFocusChange,Activity啟動完成結束标志這裡采用沒有使用生命周期函數,而是以主界面View的第一次繪制作為啟動完成的标志,View被第一次繪制證明View即将展示出來被我們看到。是以我們在Activity根布局中加入一個自定義View,以它的onDraw方法第一次回調作為Activity啟動完成的标志。
    public class FirstDrawListenView extends View {
         private boolean isFirstDrawFinish = false;
     
         private IFirstDrawListener mIFirstDrawListener;
     
         public FirstDrawListenView(Context context, AttributeSet attrs) {
             super(context, attrs);
         }
     
         @Override
         protected void onDraw(Canvas canvas) {
             super.onDraw(canvas);
             if (!isFirstDrawFinish) {
                 isFirstDrawFinish = true;
                 if (mIFirstDrawListener != null) {
                     mIFirstDrawListener.onFirstDrawFinish();
                 }
             }
         }
     
         public void setFirstDrawListener(IFirstDrawListener firstDrawListener) {
             mIFirstDrawListener = firstDrawListener;
         }
     
     	public interface IFirstDrawListener {
     	    void onFirstDrawFinish();
     	}
     }
               

四、怎麼統計資料檢視優化前後的資料對比

  通過上面的分析,我們可以統計程序啟動各個階段的耗時點,以及Activity啟動各個階段的耗時點(這個步驟需要額外在主布局中加入一個自定義的空View,監聽它的onDraw方法的第一次回調),可以通過埋點資料收集這些資料,在優化之前可以先加入埋點資料,統計上報各個時間段的埋點,是以需要先發個版本驗證一下優化之前的情況。統計資料的機制加入之後,就可以着手優化了,一邊優化一邊對比,可以很清楚看到優化前後的對比。

五、制定優化的目标

  由于App啟動速度在不同是裝置上差别很大,是以目标不太好定,但是做事情總得要有個目标吧。首先我們使用大家都熟悉的一個概念“秒開”,其次是冷啟動熱啟動分開算,再次是分出不同的機型(高端機,中端機型,低端機型),最後是需要先看看沒優化之前的啟動資料。這樣就可以定義出類似下面的目标:

  1. 高端機型1秒内打開(比如小米5,Android6.0以上)
  2. 中端機型1.5秒内打開
  3. 低端機型2.5秒内打開

  上面是終極目标,真正優化的時候,要結合App實際資料以及團隊實際情況來定自己的優化目标。

六、優化具體步驟

  一般來說,快速優化最好的方式就是把不必要提前做的操作放到異步線程中去做,也就是我們經常做的異步加載。除了異步加載,一些真正有性能影響的代碼需要做具體優化。下面依次介紹一些具體的優化實施步驟。

6.1 封裝一個列印耗時點日志的輔助類

  優化的時候為了快速定位耗時的代碼塊,我們需要在耗時代碼塊的前後加上日志,統計耗時具體的時間。這個能在Debug模式下幫助我們快速分析定位到耗時的代碼塊,然後我們在針對具體的耗時代碼塊去下手,看看怎麼優化。

6.2 異步加載一:Application中加入異步線程

  在Application中封裝兩個方法:onSyncLoad(同步加載)和 onAsyncLoad(異步加載,在Thread中執行),把不需要同步加載的部分全部放到onAsyncLoad方法,需要同步的方法放到onSyncLoad中去做,就這種簡單的分類就可以帶來一個很好的優化效果。

public class StartUpApplication extends Application {

    @Override
    public void onCreate() {
        // 程式建立時調用,次方法應該執行應該盡量快,否則會拖慢整個app的啟動速度
        super.onCreate();
        onSyncLoadForCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        onSyncLoad();
        onAsyncLoad();
    }

    private void onSyncLoadForCreate() {
        AppStartUpTimeLog.isColdStart = true;   // 設定為冷啟動标志
        AppLog.log("StartUpApplication onCreate");
        AppStartUpTimeLog.logTimeDiff("App onCreate start", false, true);
        BlockingUtil.simulateBlocking(500); // 模拟阻塞100毫秒
        AppStartUpTimeLog.logTimeDiff("App onCreate end");
    }

    private void onSyncLoad() {
        AppLog.log("StartUpApplication attachBaseContext");
        AppStartUpTimeLog.markStartTime("App attachBaseContext", true);
        BlockingUtil.simulateBlocking(200); // 模拟阻塞100毫秒
        AppStartUpTimeLog.logTimeDiff("App attachBaseContext end", true);
    }

    public void onAsyncLoad() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 異步加載邏輯
            }
        }, "ApplicationAsyncLoad").start();
    }
}
           

6.3 異步加載二:MainActivity中加入異步線程

  這一步驟與Application的優化思路一樣,也是封裝onSyncLoad和onAsyncLoad方法對現有代碼進行一個分類,但是這兩個方法的調用時機要晚一點,是在主界面首屏繪制完成的時候調用。這個步驟也需要new一個Thead,屬于額外的開銷,不過這不影響我們整體性能。

6.4 延遲加載功能:首屏繪制完成之後加載

  還有些操作必須要在UI線程做,但是不需要那麼快速就做,這裡放到首屏繪制完成之後,我們之前在主布局中加入一個空的View來監聽它的第一次onDraw回調,我們通過接口的方式把這個事件接到我們的MainActivity中去(Activity中實作接口的onFirstDrawFinish方法)。為了讓使用者盡快看到主界面,我們就可以把一些需要在UI線程執行,但是又不需要那麼快的執行的操作放到onFirstDrawFinish中去。

6.5 動态加載布局:主布局檔案優化

  把主界面中不需要第一次就用到的布局全部使用動态加載的方式來處理,使用ViewStub或者直接在使用時動态addView的方式。

6.6 主布局檔案深度優化

  如果做了上面這些優化還是會發現進入主界面還是有些慢,那麼需要重點關注主布局檔案了。主布局檔案的複雜度直接影響到了Activity的加載速度,這個時候需要對主布局檔案進行深度優化了。Activity在加載布局的時候,會對整個布局檔案進行解析,測量(measure),布局(layout)和繪制(draw),是以設計簡單合理的布局尤為重要。布局的優化不做詳細介紹,網上很多文章的。幾個重要的優化如下:

  1. 減少布局層級
  2. 減少首次加載View的數量
  3. 減少過度繪制

  如果需要看看主布局加載具體用了多少時間,需要用自定ViewGroup作為根布局根元素,然後監控它的onInflateFinished,onMeasure,onLayout,onDraw方法,通過我們之前寫好的列印時間日志的輔助類,列印一些關鍵日志,可以分析出具體的耗時的步驟,還可以定位哪個View加載耗時最長。

6.7 功能代碼深度優化

  前面的優化步驟中,我們有部分耗時操作放到了首屏繪制onFirstDrawFinish之後來做了,這裡會帶來一個體驗上的問題,雖然進入主界面變快了,但是可能進入之後短暫的時間類UI線程是阻塞的,如果有其他的UI操作可能會卡主,因為onFirstDrawFinish中挂了很多耗時的操作,需要等這些做完之後UI線程才能空閑。是以我們還需要對一些功能代碼進行優化,確定其真正用時少。另外我們異步加載線程中的操作是有一定的安全風險的,如果有些操作很耗時,可能導緻我們進入主界面需要用到資料時還沒有準備好,是以異步加載我們要注意代碼塊的順序,如果有些非常耗時的操作考慮用單獨的線程去處理。

七、總結

  優化是一條持續之路,通過優化我們可以了解到影響啟動性能的因素有哪些,這樣我們平時在編碼的過程中就會多注意自己的代碼性能。本文從全局的角度去看待整個啟動性能優化,看起來好像還挺容易,但是可能實際過程中優化并不會很順利,不同的裝置上可能表現不一樣,有時候可能啟動一個服務都會耗時。是以要想真正的不耗時,那就是大招:删除它吧。

八、項目位址

模拟耗時點,列印日志觀察生命周期函數回調情況

https://github.com/PopFisher/AppStartUpSpeedOpt

繼續閱讀