如何統計Android App啓動時間

真愛,請置頂或星標

作者:申國駿 https://www.jianshu.com/p/59a2ca7df681

隨着App的邏輯不斷龐大,一不注意就會將耗時的操作放置在應用啓動過程之中,導致應用啓動速度越來越慢,用戶體驗也越來越差。優化啓動速度是幾乎所有大型App應用開發者需要考慮的問題。優化啓動速度之前首先需要準確測量App啓動時間,這樣有利於我們更準確可量化地看出優化效果,也可以指導我們進行持續優化。

1.使用命令行方式

使用命令行方式統計多次啓動某個Activity的平均用時可以在shell中執行如下指令:

adb shell am start -S -R 10 -W com.example.app/.MainActivity

其中-S表示每次啓動前先強行停止,-R表示重複測試次數。每一次的輸出如下所示信息。

Stopping: com.example.app
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.app/.MainActivity }
Status: ok
Activity: com.example.app/.MainActivity
ThisTime: 1059
TotalTime: 1059
WaitTime: 1073
Complete

其中TotalTime代表當前Activity啓動時間,將多次TotalTime加起來求平均即可得到啓動這個Activity的時間。

缺點 應用的啓動過程往往不只一個Activity,有可能是先進入一個啓動頁,然後再從啓動頁打開真正的首頁。某些情況下還有可能中間經過更多的Activity,這個時候需要將多個Activity的時間加起來。 將多個Activity啓動時間加起來並不完全等於用戶感知的啓動時間。例如在啓動頁可能是先等待某些初始化完成或者某些動畫播放完畢後再進入首頁。使用命令行統計的方式只是計算了Activity的啓動以及初始化時間,並不能體現這種等待任務的時間。 沒有在AndroidManifest.xml對應的Activity聲明中指定或者屬性沒有android:exported="true"的Activity不能使用這種命令行的形式計算啓動時間。

2.思考更準確的方式

以上基於命令行的方式存在諸多問題,迫使我們思考怎樣才能得到從用戶角度上觀察更準確的啓動時間。在嘗試其他方法之前,我們先定義一下怎樣纔是從用戶角度上觀察的啓動時間。

冷啓動、熱啓動(注意不是官方的定義,是我們從用戶角度考慮的定義) 冷啓動時間:冷啓動表示用戶首次打開應用,這時進程還沒創建,包含了Application創建的過程。冷啓動時間指從第一次用戶點擊Launcher中的應用圖標開始,到首頁內容全部展示出來的時間。 熱啓動時間:熱啓動表示用戶在首頁按了返回,首頁Activity已經Destroy,不過Application仍在內存中存在,對應的進程並沒有被殺掉,不包含Application創建過程。熱啓動時間指在Application仍然存在的情況下,從用戶點擊桌面圖標,到首頁內容全部展示出來的時間。 App啓動流程 要優化以及分析啓動時間,需要先了解App的啓動流程。以冷啓動爲例子,Application以及Activity的啓動流程如下.

更爲直觀和簡單的流程圖參考Colt McAnlis在Android Performance Patterns Season 6中的表述。有興趣的同學可以點擊鏈接看看(https://www.youtube.com/watch?v=Vw1G1s73DsY&index=2&list=PLWz5rJ2EKKc-9gqRx5anfX0Ozp-qEI2CF)。

從流程圖以及參考Colt McAnlis的Android Performance Patterns[6]得知,在冷啓動的過程中,首先會通過AMS在System進程展示一個Starting Window(通常情況下是個白屏,可以通過設置Application的theme修改),接着AMS會通過Zygote創建應用程序的進程,並通過一系列的步驟後調用Application的attachBaseContext()、onCreate()然後最終調用Activity的onCreate()以及進行View相關的初始化工作。在Activity展示出來後會替換掉之前的Starting Window,這樣啓動過程結束。

如何加log 在Activity中onWindowFocusChanged()方法是最好的Activity對用戶可見的標誌,因此綜合上一節的分析,我們可以考慮在Application的attachBaseContext()方法中開始計算冷啓動計時,然後在真正首頁Activity的onWindowFocusChanged()中停止冷啓動計時,這樣就可以初步得到應用的冷啓動時間。 爲了方便統計,設置一個Util類專門做計時,添加的代碼如下:

/**
 * 計時統計工具類
 */
public class TimeUtils {
    private static HashMap<String, Long> sCalTimeMap = new HashMap<>();
    public static final String COLD_START = "cold_start";
    public static final String HOT_START = "hot_start";
    public static long sColdStartTime = 0;

    /**
     * 記錄某個事件的開始時間
     * @param key 事件名稱
     */
    public static void beginTimeCalculate(String key) {
        long currentTime = System.currentTimeMillis();
        sCalTimeMap.put(key, currentTime);
    }

    /**
     * 獲取某個事件的運行時間
     *
     * @param key 事件名稱
     * @return 返回某個事件的運行時間,調用這個方法之前沒有調用 {@link #beginTimeCalculate(String)} 則返回-1
     */
    public static long getTimeCalculate(String key) {
        long currentTime = System.currentTimeMillis();
        Long beginTime = sCalTimeMap.get(key);
        if (beginTime == null) {
            return -1;
        } else {
            sCalTimeMap.remove(key);
            return currentTime - beginTime;
        }
    }

    /**
     * 清除某個時間運行時間計時
     *
     * @param key 事件名稱
     */
    public static void clearTimeCalculate(String key) {
        sCalTimeMap.remove(key);
    }

    /**
     * 清除啓動時間計時
     */
    public static void clearStartTimeCalculate() {
        clearTimeCalculate(HOT_START);
        clearTimeCalculate(COLD_START);
        sColdStartTime = 0;
    }
}

然後在Application的attachBaseContext()方法中添加如下代碼:

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    if (/**如果是主進程**/) {
        TimeUtils.beginTimeCalculate(TimeUtils.COLD_START);
    }
}

在第一個Activity的onCreate()方法中添加如下代碼:

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

private void calculateStartTime() {
    long coldStartTime = TimeUtils.getTimeCalculate(TimeUtils.COLD_START);
    // 這裏記錄的TimeUtils.coldStartTime是指Application啓動的時間,最終的冷啓動時間等於Application啓動時間+熱啓動時間
    TimeUtils.sColdStartTime = coldStartTime > 0 ? coldStartTime : 0;
    TimeUtils.beginTimeCalculate(DictTimeUtil.HOT_START);
}

在真正的首頁Activity的 onWindowFocusChanged()方法中添加如下代碼:

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    if (hasFocus && /**沒有經過廣告或者引導頁**/) {
        long hotStartTime = TimeUtils.getTimeCalculate(TimeUtils.HOT_START);
        if (TimeUtils.sColdStartTime > 0 && hotStartTime > 0) {
            // 真正的冷啓動時間 = Application啓動時間 + 熱啓動時間
            long coldStartTime = TimeUtils.sColdStartTime + hotStartTime;
            // 過濾掉異常啓動時間
            if (coldStartTime < 50000) {
                // 上傳冷啓動時間coldStartTime 
            }
        } else if (hotStartTime > 0) {
            // 過濾掉異常啓動時間
            if (hotStartTime < 30000) {
                // 上傳熱啓動時間hotStartTime 
            }
        }
    }
}

需要避免的點 上面的分析給了我們初步的加log的起始和結束點,然而在實際的統計中會發現得到的數據有20%左右是不準確的,體現在計時數據非常大,有些甚至會顯示冷啓動時間超過一天。經過分析,在計算啓動計時的時候需要注意一些問題。以下列舉一下添加log時候需要注意的點。

應用在啓動過程可能會有廣告(我們的業務是有道詞典),第一次啓動會有引導頁,需要根據業務情況標記在沒有廣告、沒有引導頁的時候才計算。這種情況要注意在非正常啓動的時候忽略啓動時間統計。

由於詞典首頁之前還有幾個Activity,在沒到首頁Activity之前如果過早的返回,會出現冷啓動時間過長的問題。這是因爲詞典返回的時候並沒有殺掉進程,而時間統計信息是保存在內存中的,而等下次再進入的時候因爲是熱啓動不會重新開始冷啓動計時。這導致了這次熱啓動實際上打log的時候發現有上次冷啓動的開始時間,算成了冷啓動,而且因爲啓動時間是上一次的,所以這次冷啓動log的時間比實際時間長。這種情況要注意在首頁Activity之前的其他ActivityonPause()方法中調用TimeUtils.clearStartTimeCalculate();清除計時。

除了正常的啓動流程,應用還有很多可能會導致Application的創建的入口,例如點擊桌面小插件、系統賬號同步、Deep Link跳轉、直接進入設置了的Activity、push達到等。我們需要檢查所有有可能引起Application創建,但是不是正常啓動流程的地方,調用TimeUtils.clearStartTimeCalculate();清除計時,避免引起冷啓動時間計算過長錯誤的問題。

3.使用第三方工具

爲了測試啓動的過程中哪些方法比較耗時,我們可以使用Android Studio中集成的Android Monitor提供的Method Tracering或者Systrace。不過在實踐中發現,有另外一個nimbledroid工具使用更加簡便且能更明確指出耗時的地方。上傳了應用之後會自動分析情景如下圖所示。其中會自動檢測出首頁的Activity並且給出冷啓動的啓動情況。

點擊進入Cold Startup的情景可以看到主要耗時的方法如下圖。

至於爲什麼nimbledroid會知道那個是我們首頁的Activity,官網上解析如下:

We use a heuristic to tell when an app finishes startup by detecting when (1) the main Activity has been displayed and (2) things like animated progress bars in the main Activity have stopped. Based on our experiments, this heuristic works in most cases.

點擊進入某個方法,可以看到這個方法具體是由於調用了哪個子方法導致了耗時的問題。

通過nimbledroid這個工具,我們可以比較輕鬆地發現一些比較明顯的問題,並可以指導我們進行啓動優化。同時nimbledroid還支持Memory Leaks、網絡監測以及結果分享等一些功能,更多的功能有待讀者繼續發現。

後記

統計和分析啓動時間有利於指導我們優化啓動時間。以上介紹了有道詞典在進行啓動優化中的分析過程。通過詳細瞭解Android應用啓動的流程,進行準確的log記錄,並且結合第三方工具,我們最終得到準確的啓動時間統計數據以及啓動優化的一些頭緒。具體優化的方法可以看下一篇文章《如何優化Androd App啓動速度》。

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