Android App性能優化之App啓動速度優化

一、App啓動分類

1.冷啓動 Cold start

在啓動應用前,系統還沒有App的任何進程。比如設備開機後應用的第一次啓動,系統殺掉應用進程 (如:系統內存吃緊引發的 kill 和 用戶主動產生的 kill) 後 的再次啓動等。那麼自然這種方式下,應用的啓動時間最長。

2.熱啓動 Warm start
當應用中的 Activities 被銷燬,但在內存中常駐時,應用的啓動方式就會變爲暖啓動。相比冷啓動,暖啓動過程減少了對象初始化、UI的佈局和渲染。啓動時間更短。但啓動時,系統依然會展示一個空白背景,直到第一個 Activity 的內容呈現爲止。

3.溫啓動 Lukewarm start
用戶退出您的應用,但隨後重新啓動。該過程可能已繼續運行,但應用程序必須通過調用onCreate()從頭開始重新創建活動。系統從內存中驅逐您的應用程序,然後用戶重新啓動它。進程和Activity需要重新啓動,但任務可以從保存的實例狀態包傳遞到onCreate()中。

啓動速度優化主要是針對冷啓動方式。下面看下冷啓動的時候會做哪些工作。

二、冷啓動

應用發生冷啓動時,系統有三件任務要做:

  • 加載啓動App;
  • App啓動之後立即展示出一個空白的Window;
  • 創建App的進程;

創建App進程後,會馬上執行以下任務:

  • 初始化應用中的對象 (比如 Application 中的工作);

  • 啓動主線程 (UI 線程) ;

  • 創建第一個 Activity;

  • 加載內容視圖 (Inflating) ;

  • 計算視圖在屏幕上的位置排版 (Laying out);

  • 進行第一次繪製 (draw)。

只有當應用完成第一次繪製,系統當前展示的空白背景纔會消失,纔會被 Activity 的內容視圖替換掉。也就是這個時候,用戶才能和我們的應用開始交互。下圖展示了冷啓動過程系統和應用的一個工作時間流:

三、優化思路

作爲普通應用,App進程的創建等環節我們是無法主動控制的。開發人員唯一能做的就是在Application 和 第一個 Activity 中,減少 onCreate() 方法的工作量,從而縮短冷啓動的時間。像應用中嵌入的一些第三方 SDK,都建議在 Application 中做一些初始化工作,開發人員不妨採取懶加載的形式移除這部分代碼,而在真正需要用到第三方 SDK 時再進行初始化。

Google也給出了啓動加速的方向:

1、利用提前展示出來的Window,快速展示出來一個界面,給用戶快速反饋的體驗;
2、 避免在啓動時做密集沉重的初始化(Heavy app initialization);
3、 定位問題:避免I/O操作、反序列化、網絡操作、佈局嵌套等

四、正確測量評估啓動性能的方法

1.display time

從Android KitKat版本開始,Logcat中會輸出從程序啓動到某個Activity顯示到畫面上所花費的時間。這個方法比較適合測量程序的啓動時間。

2.reportFullyDrawn

我們通常來說會使用異步懶加載的方式來提升程序畫面的顯示速度,這通常會導致的一個問題是,程序畫面已經顯示,可是內容卻還在加載中。爲了衡量這些異步加載資源所耗費的時間,我們可以在異步加載完畢之後調用activity.reportFullyDrawn()方法來告訴系統此時的狀態,以便獲取整個加載的耗時。

3.Traceview

告訴我們每一個方法執行了多長時間.這個工具可以通過 Android Device Monitor 或者從代碼中啓動。

3.1 Android Device Monitor啓動

啓動應用,點擊 Start Method Tracing,應用啓動後再次點擊,會自動打開剛纔操作所記錄下的.trace文件,建議使用DDMS來查看,功能更加方便全面。

3.2 代碼啓動

①在onCreate開始和結尾打上trace

    Debug.startMethodTracing("GithubApp");
    ...
    Debug.stopMethodTracing();

注意加讀寫權限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

運行程序, 會在sdcard上生成一個”GithubApp.trace”的文件.

②通過adb pull將文件導出到本地

adb pull /sdcard/GithubApp.trace ~/temp

③打開DDMS分析trace文件
④分析trace文件

  • 在下方的方法區點擊”Real Time/Call”, 按照方法每次調用耗時降序排.
  • 耗時超過500ms都是值得注意的.
  • 看左邊的方法名, 可以看到耗時大戶就是我們用的幾大平臺的初始化方法, 特別是Bugly, 還加載native的lib, 用ZipFile操作-等.
  • 點擊每個方法, 可以看到其父方法(調用它的)和它的所有子方法(它調用的).
  • 點擊方法時, 上方的該方法執行時間軸會閃動, 可以看該方法的執行線程及相對時長.

4.Systrace

在onCreate方法裏面添加trace.beginSection()與trace.endSection()方法來聲明需要跟蹤的起止位置,系統會幫忙統計中間經歷過的函數調用耗時,並輸出報表。

5.adb命令計算 App 的啓動時間

adb shell am start -W packageName/packageName.activity

例如:

adb shell am start -W com.media.painter/com.media.painter.PainterMainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.media.painter/.PainterMainActivity }
Status: ok
Activity: com.media.painter/.PainterMainActivity
ThisTime: 355
TotalTime: 355
WaitTime: 365
Complete

(注意 Android 5.0 之前的手機是沒有 WaitTime 這個值的)

  • WaitTime 就是總的耗時,包括前一個應用 Activity pause 的時間和新應用啓動的時間;
  • ThisTime 表示一連串啓動 Activity 的最後一個 Activity 的啓動耗時;
  • TotalTime 表示新應用啓動的耗時,包括新進程的啓動和 Activity 的啓動,但不包括前一個應用 Activity pause 的耗時。
  • 開發者一般只要關心 TotalTime 即可,這個時間纔是自己應用真正啓動的耗時。

詳細請見:Android 中如何計算 App 的啓動時間

五、優化方案

1.主題切換

通過主題設置,不顯示啓動時的白屏背景。有以下幾種方案:

1.1 直接不顯示白屏,直到程序初始化完畢直接顯示第一個Activity

<style name="LaunchStyle" parent="Theme.AppCompat.Light.DarkActionBar">
            ......
            <item name="android:windowIsTranslucent">true</item>
            <item name="android:windowNoTitle">true</item>
</style>

或者

    <style name="LaunchStyle" parent="Theme.AppCompat.Light.DarkActionBar">
        ......
        <item name="android:windowDisablePreview">true</item>
    </style>

然後設置給第一個activity

    <activity 
        android:name=".MainActivity"
        android:theme="@style/LaunchStyle">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

如果將主題設置到Application,那所有的Activity的主題都會改變

然後在MainActivity中在加載佈局之前,重新設置主題

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setTheme(R.style.AppTheme);
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
    }
效果如圖

這樣相當於把白屏變成透明的,隱藏起來了,但是會有一種點擊圖標後卡住了,過了好幾秒才進入App的感覺。這種方案用戶體驗很差。

1.2把白屏當成閃屏頁用

可以通過主題中的 windowBackground 屬性,自定義應用啓動時的窗口背景。窗口背景顯示的內容,Google推薦兩種方案,一種是顯示Logo,一種利用了 placeholder ,與主界面的 UI 框架保持一致,給用戶產生一種應用啓動非常快的視覺感受,如下圖所示:

①顯示Logo使用方式:

drawable/branded_launch_screens:

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android"
        android:opacity="opaque">
        <!--黑色背景顏色-->
        <item android:drawable="@android:color/black" />
        <!-- 產品logo-->
        <item>
            <bitmap
                android:gravity="center"
                android:src="@mipmap/empty_image01" />
        </item>
        <!-- 右上角的圖標元素 -->
        <item>
            <bitmap
                android:gravity="top|right"
                android:src="@mipmap/github" />
        </item>
        <!--最下面的文字-->
        <item android:bottom="50dp">
            <bitmap
                android:gravity="bottom"
                android:src="@mipmap/ic_launcher" />
        </item>
    </layer-list>
android:opacity=”opaque”參數是爲了防止在啓動的時候出現背景的閃爍。 定義style:
    <style name="AppTheme.Launcher">
       <item name="android:windowBackground">@drawable/branded_launch_screens</item>
    </style>
或者直接使用一張圖片
    <style name="AppTheme.Launcher">
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowBackground">@mipmap/app_welcome</item>
    </style>
然後將這個主題設置給啓動的 Activity。
②使用placeholder:

模擬了一個高度爲25dp的狀態欄和一個高度爲56dp的標題欄。
drawable/placeholder_ui

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android"
        android:opacity="opaque">
        <!--狀態欄顏色-->
        <item android:drawable="@color/colorPrimaryDark" />
        <!--假裝這裏是個toolbar-->
        <item
            android:drawable="@color/colorPrimary"
            android:top="25dp" />
        <!--狀態欄25+toolbar56=距離top81-->
        <item
            android:drawable="@android:color/white"
            android:top="81dp" />
    </layer-list>
定義style:
    <style name="AppTheme.Launcher">
        <item name="android:windowBackground">@drawable/placeholder_ui</item>
    </style>

然後將這個主題設置給啓動的 Activity。

##### ③還可以適度結合 Activity 內容視圖使用動畫過渡效果。 如下圖:

2.避免Application的onCreate進行太多的工作

在Application初始化的地方做太多繁重的事情是可能導致嚴重啓動性能問題的元兇之一。Application裏面的初始化操作不結束,其他任意的程序操作都無法進行。Application的onCreate中會做大量第三方組件的初始化工作,其實很多組件是需要做區別對待的,有些可以做延遲加載,有些可以放到其他的地方做初始化操作,特別需要留意包含Disk IO操作,網絡訪問等嚴重耗時的任務,他們會嚴重阻塞程序的啓動。

注意點:

  • 項目是多進程架構,只在主進程執行Application的onCreate();
  • 流程梳理,延後執行;
  • 異步加載、延時加載、懶加載

示例如下,Application以及首屏Activity中我們主要做了:

如何判斷第三方的庫是不是能放在子線程裏面:

需要初始化的第三方一般分爲兩種, 一種是第三方平臺的SDK(推送, 分享, 反饋, 統計等) 這個可以通過看其SDK文檔, 結合業務需求考慮. 例如分享, 反饋一般不是必須要應用一開啓就能用的, 這類業務一般層級比較深, 有足夠的理由讓它們在後臺異步初始化. 另外一種第三方是第三方的庫, 一般來說, 建議閱讀其源碼, 瞭解其實現原理, 再決定是否放在後臺初始化.

項目修改:

將友盟、Bugly、聽雲、GrowingIO、BlockCanary等組件放在WorkThread中初始化;
延遲地圖定位、ImageLoader、自有統計等組件的初始化:地圖及自有統計延遲4秒,此時應用已經打開;而ImageLoader
因爲調用關係不能異步以及過久延遲,初始化從Application延遲到SplashActivity;而EventBus因爲再Activity中使用所以必須在Application中初始化。

參考:Android性能優化(一)之啓動加速35%

3.避免首個Activity的onCreate進行太多的工作

使用延遲加載。確保在Activity的頁面顯示出來之後再進行加載數據,避免過早或過晚的加載導致頁面空白時間過長。可採用以下 代碼實現延遲加載。在Activity的onCreate方法中:

getWindow().getDecorView().post(new Runnable() {
  @Override
  public void run() {
    myHandler.post(mLoadingRunnable);
  }
});

詳細原理可見Android應用啓動優化:一種DelayLoad的實現和原理

4.MultiDex初次啓動優化

4.1問題

隨着代碼數量的膨脹,工程本身的代碼加上引用的第三方庫的代碼中方法數量會超過65536的限制。是由於DEX文件格式限制,一個DEX文件中method個數採用使用原生類型short來索引文件中的方法,也就是4個字節共計最多表達65536個method,field/class的個數也均有此限制。 Google爲構建超過65K方法數的應用提供官方支持的方案:MultiDex。

但是在Dalvik下MultiDex有個問題:5.0以下某些低端機會出現ANR或者長時間卡頓不進入引導頁,而罪魁禍首是MultiDex.install(Context context)的dexopt過程耗時過長。因此需要在初次啓動時做特別處理。
而5.0以上會使用ART,在ART下MultiDex是不存在這個問題的,這主要是因爲ART下采用Ahead-of-time (AOT) compilation技術,系統在APK的安裝過程中會使用自帶的dex2oat工具對APK中可用的DEX文件進行編譯並生成一個可在本地機器上運行的文件,這樣能提高應用的啓動速度,只是在安裝過程中進行了處理這樣會影響應用的安裝速度。

4.2解決思路

1、在Application.attachBaseContext(Context base)中,判斷是否初次啓動,以及系統版本是否小於5.0,如果是,跳到2;否則,直接執行MultiDex.install(Context context)。
2、開啓一個新進程,在這個進程中執行MultiDex.install(Context context)。執行完畢,喚醒主進程,自身結束。主進程在開啓新進程後,自身是掛起的,直到被喚醒。
3、喚醒的主進程繼續執行初始化操作。

詳細示例可見:Android MultiDex初次啓動APP優化

參考:
- google:Launch-Time Performance
- Android 你應該知道的的應用冷啓動過程分析和優化方案
- Android APP啓動優化
- Android性能優化(一)之啓動加速35%
- Android性能優化典範 - 第6季
- Android 中如何計算 App 的啓動時間
- Android App優化之提升你的App啓動速度之實例挑戰
- Android應用啓動優化:一種DelayLoad的實現和原理
- Android MultiDex初次啓動APP優化
- 美團Android DEX自動拆包及動態加載簡介

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章