應用程式啟動時間

使用者希望應用程式可以快速載入並回應。應用程式如果啟動時間緩慢,不但不符合此期望,還可能會讓使用者感到失望。此類不佳體驗可能會導致使用者在 Play 商店中對您的應用程式給予負評,或甚至解除安裝應用程式。

本頁面提供有助於最佳化應用程式啟動時間的資訊,包括發布程序內部總覽、如何分析啟動效能,以及一些常見的啟動時間問題和排解提示。

瞭解不同的應用程式啟動狀態

應用程式啟動可能有三種狀態:冷啟動、暖啟動或熱啟動。每種狀態都會影響顯示應用程式所需的時間長度。冷啟動就表示應用程式從頭開始啟動。在其他狀態中,系統需要將執行中的應用程式從背景移至前景。

建議您一律以冷啟動為假設狀態來進行最佳化。這麼做也可以改善暖啟動和熱啟動的效能。

如要最佳化應用程式快速啟動程序,建議您瞭解各系統和應用程式層級所發生的情況,以及各狀態如何互動。

決定應用程式啟動的兩項重要指標為初始顯示時間 (TTID)完整繪製時間 (TTFD)。TTID 是顯示第一個影格所需的時間,而 TTFD 是應用程式完整互動所需的時間。這兩項指標一樣重要,因為 TTID 能讓使用者知道應用程式正在載入,而 TTFD 則表示要等多久才可實際使用應用程式。如果其中一項時間過長,使用者可能會在應用程式載入完畢前就退出應用程式。

冷啟動

冷啟動是指應用程式從頭開始啟動,這表示系統程序會在冷啟動開始後才建立應用程式程序。冷啟動是指在裝置啟動後首次次啟動應用程式,或是系統終止應用程式後首次啟動應用程式。

這種啟動方式很難將啟動時間縮減到最少,因為相較於其他啟動狀態,系統和應用程式需要執行更多工作。

在冷啟動開始時,系統會執行以下三項工作:

  1. 載入並啟動應用程式。
  2. 啟動後立即顯示應用程式的空白起始視窗。
  3. 建立應用程式程序

系統建立應用程式程序後,應用程式程序就會負責進行後續階段:

  1. 建立應用程式物件。
  2. 啟動主執行緒。
  3. 建立主要活動。
  4. 加載檢視畫面。
  5. 對畫面建立版面配置。
  6. 執行初始繪圖。

應用程式程序完成第一個繪圖後,系統程序會關閉目前顯示的背景視窗,並以主要活動取代。此時使用者就可以開始使用應用程式。

圖 1 顯示系統和應用程式程序如何交互處理。

圖 1 以視覺化方式呈現應用程式冷啟動的重要部分。

應用程式建立和活動建立期間可能會出現的效能問題。

應用程式建立

應用程式啟動時,畫面中會持續顯示空白的啟動視窗,直到系統完成應用程式繪圖作業為止。此時,系統程序會切換應用程式的啟動視窗,讓使用者能與應用程式互動。

如果在應用程式中覆寫 Application.onCreate(),系統就會在應用程式物件中叫用 onCreate() 方法。之後,應用程式就會產生「主執行緒」(也稱為 UI 執行緒),然後透過建立主要活動進行工作。

此時,系統層級和應用程式層級的程序會根據應用程式生命週期階段進行。

活動建立

應用程式程序建立活動後,該活動會執行下列作業:

  1. 初始化值。
  2. 呼叫建構函式。
  3. 依照目前的活動生命週期狀態呼叫回呼方法,例如:Activity.onCreate()

一般而言,onCreate() 方法對於載入時間會造成最大的影響,因為這會執行負載最高的工作:載入和加載檢視畫面,以及初始化執行活動所需的物件。

暖啟動

暖啟動包含冷啟動期間的部分作業,而且負載比熱啟動高。有許多潛在狀態可視為暖啟動,例如以下情況:

  • 使用者先退出再重新啟動應用程式。此時程序可能仍會繼續執行,但應用程式必須呼叫 onCreate(),才能從頭開始重新建立活動。

  • 系統從記憶體中清除應用程式,然後使用者再重新啟動應用程式。程序和活動必須重新啟動,但由於 onCreate() 中傳入的例項狀態組合已經儲存,因此可以減輕系統的工作量。

熱啟動

相較於冷啟動,應用程式熱啟動作業耗用的資源較少。進行熱啟動程序時,系統會將活動移至前景。如果應用程式的所有活動仍留在記憶體中,應用程式就不需要重複初始化物件、版面配置加載和轉譯。

不過,如果有些記憶體已因回應記憶體修整事件 (例如 onTrimMemory()) 而遭到清除,就需要重新建立這些物件才能回應熱啟動事件。

熱啟動顯示的畫面行為與冷啟動的情況相同,系統程序會顯示空白畫面,直到應用程式完成活動轉譯為止。

圖 2. 此圖表顯示多種不同的啟動狀態及其各自的程序,並從第一個繪製的影格開始顯示每種狀態。

如何在 Perfetto 中查看應用程式啟動作業

如要對應用程式啟動問題偵錯,建議您先判斷應用程式啟動階段涉及哪些作業。如要在 Perfetto 中查看應用程式的所有啟動階段,請按照下列步驟操作:

  1. 在 Perfetto 中,找出「Android App Startups 」衍生指標列。如果沒看到,請嘗試使用裝置端系統追蹤應用程式擷取追蹤記錄。

    圖 3:Perfetto 中的「Android App Startups」衍生指標片段。
  2. 按一下相關聯的片段,然後按下 m 鍵選取片段。系統會將片段以括弧括起,表示所費時間。「Current selection」分頁中也會顯示時間長度。

  3. 將滑鼠游標懸停在資料列上,點選出現的圖釘圖示,即可固定「Android App Startups」列。

  4. 捲動至相關應用程式的資料列,然後按一下第一個儲存格,展開資料列。

  5. 按下 w 放大主要執行緒,通常位於頂端 (按下 s, a, d 鍵即可縮小、向左移動和向右移動)。

    圖 4.「Android App Startups」衍生指標片段,位於應用程式的主要執行緒旁。
  6. 衍生指標片段可讓您更輕鬆地查看應用程式啟動階段中的作業,以便進一步偵錯。

使用指標檢查及改善啟動程序

為了正確診斷開始時間的效能,您可以追蹤顯示應用程式啟動所需時間的指標。Android 提供多種可以讓您得知應用程式出現問題的方法,還能幫助您診斷問題原因。Android Vitals 會提醒您發生問題,診斷工具則可協助您診斷問題。

使用啟動指標的優點

Android 會使用「初始顯示時間 (TTID)」和「完整顯示時間 (TTFD)」指標,為冷啟動和暖啟動應用程式進行最佳化調整。Android 執行階段 (ART) 會使用這些指標中的資料,以有效率的方式預先編譯程式碼,針對未來的啟動程序進行最佳化。

啟動速度越快,就越能吸引使用者持續與應用程式互動,進而減少提前結束、重新啟動執行個體或離開並前往其他應用程式的情形。

Android Vitals

當應用程式啟動時間過長時,Android Vitals 會透過 Play 管理中心 發出提醒,藉此改善應用程式的效能。

Android Vitals 會將以下的應用程式啟動時間視為過長:

  • 啟動所需時間超過 5 秒。
  • 啟動所需時間超過 2 秒。
  • 啟動所需時間超過 1.5 秒。

Android Vitals 會使用「初始顯示時間 (TTID)」指標。如要瞭解 Google Play 如何收集 Android Vitals 資料,請參閱 Play 管理中心說明文件

初始顯示時間

初始顯示時間 (TTID) 是指顯示應用程式 UI 第一個影格所需的時間。這項指標可測量應用程式產生第一個影格所需的時間,包括在冷啟動期間初始化、在冷或暖啟動期間建立活動,以及顯示第一個影格。降低應用程式的 TTID 使用率,可讓使用者快速查看應用程式啟動畫面,有助於改善使用者體驗。Android 架構會自動回報每個應用程式的 TTID。針對應用程式啟動最佳化時,建議您實作 reportFullyDrawn,取得 TTFD 相關資訊。

TTID 測量為時間值,代表包含下列事件序列的總經過時間:

  • 啟動程序。
  • 初始化物件。
  • 建立並初始化活動。
  • 加載版面配置。
  • 首次繪製應用程式。

擷取 TTID

如要找出 TTID,請在 Logcat 指令列工具中搜尋包含 Displayed 值的輸出行。這個值是 TTID 且類似下列範例,其中 TTID 為 3s534 毫秒:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms

如要在 Android Studio 中找出 TTID,請透過篩選器下拉式選單停用 Logcat 檢視畫面中的篩選器,然後找出 Displayed 時間 (如圖 5 所示)。由於系統伺服器 (非應用程式本身) 會提供此記錄,因此必須停用篩選器。

圖 5. 已停用的篩選器和 logcat 中的 Displayed 值。

在所有資源皆已載入並顯示前,Logcat 輸出內容中的 Displayed 指標不一定會擷取所需時間。如果資源沒有在版面配置檔案中加入參照,或是屬於應用程式為物件初始化而建立的資源,則不會計算在內。由於這些資源會以內嵌程序載入,而且不會封鎖應用程式的初始顯示,因此會予以排除。

有時 Logcat 輸出內容的 Displayed 行中會包含總時間的額外欄位。例如:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms (total +1m22s643ms)

在這種情況下,首次測量僅適用於首次繪製的活動。total 時間測量從應用程式處理程序開始,而且可能包含首次開始,但未在螢幕上顯示的任何活動。只有在單一活動和總啟動時間出現差異時,系統才會顯示 total 時間測量結果。

我們建議在 Android Studio 中使用 Logcat。但如果您未使用 Android Studio,也可以透過 adb 殼層活動管理員指令執行應用程式,以評估 TTID。範例如下:

adb [-d|-e|-s <serialNumber>] shell am start -S -W
com.example.app/.MainActivity
-c android.intent.category.LAUNCHER
-a android.intent.action.MAIN

Displayed 指標會如往常顯示在 Logcat 輸出內容中。終端機視窗會顯示以下內容:

Starting: Intent
Activity: com.example.app/.MainActivity
ThisTime: 2044
TotalTime: 2044
WaitTime: 2054
Complete

-c-a 是選用引數,可讓您指定 <category><action>

完整顯示時間

完整顯示時間 (TTFD) 是指應用程式向使用者顯示互動所需的時間。回報為顯示應用程式 UI 第一個影格所需的時間,以及在初始影格顯示後以非同步載入內容所需的時間。一般來說,這會是應用程式從網路或磁碟載入的主要內容,由應用程式回報。換句話說,TTID 包含 TTID,以及應用程式可供使用所需的時間。保持應用程式的 TTFD 偏低,讓使用者快速與應用程式互動,有助於改善使用者體驗。

系統會在 Choreographer 呼叫活動的 onDraw() 方法,以及知道活動首次呼叫時,判定 TTID。不過,每個應用程式的行為不盡相同,因此系統無法判斷何時應判斷 TTFD。為判定 TTFD,應用程式必須在達到完整繪製狀態時發出信號。

擷取 TTFD

如要找出 TTFD,請呼叫 ComponentActivityreportFullyDrawn() 方法,指出已完成繪製的狀態。reportFullyDrawn 方法會回報應用程式繪製完成且處於可用狀態的時間。TTFD 是指從系統收到應用程式啟動意圖到呼叫 reportFullyDrawn() 所經過的時間。如果您未呼叫 reportFullyDrawn(),就不會回報 TTFD 值。

如要評估 TTFD,請在完全繪製 UI 和所有資料後呼叫 reportFullyDrawn()。在第一個活動的視窗首次繪製及顯示 (如系統測量結果) 之前,請勿呼叫 reportFullyDrawn(),因為系統會回報測量時間。換句話說,如果在系統偵測到 TTID 之前呼叫 reportFullyDrawn(),系統會將 TTID 和 TTFD 回報為相同的值,而這個值就是 TTID 值。

使用 reportFullyDrawn() 時,Logcat 會顯示如以下範例所示的輸出內容,其中 TTFD 為 1s54 毫秒:

system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms

Logcat 輸出內容有時會包含 total 時間,如「初始顯示時間」所述。

如果顯示時間比平常慢,可以嘗試在啟動過程中找出瓶頸。

您可以在基本情況下,在您知道已完成完整繪製狀態的情況下,使用 reportFullyDrawn() 來表示已完成繪製的狀態。不過,如果背景執行緒必須在完成完整繪製狀態前完成背景工作,您就必須延遲 reportFullyDrawn(),才能取得更準確的 TTFD 測量結果。如要瞭解如何延遲 reportFullyDrawn(),請參閱下節。

提高啟動時間準確度

如果您的應用程式正在執行延遲載入,且初始顯示內容不包含所有資源 (例如應用程式從網路擷取圖片時),建議您等到應用程式可供使用後再呼叫 reportFullyDrawn,這樣您可以將清單填入作業納入基準測試時間中。

舉例來說,如果 UI 包含 RecyclerView 或 Lazy 清單等動態清單,填入清單的背景工作可能是在清單首次繪製後才完成,也因此是在 UI 標示為已完成繪製後才完成。在這類情況下,基準測試就不會納入清單填入作業。

如要將清單填入作業納入基準測試時間,請使用 getFullyDrawnReporter() 取得 FullyDrawnReporter,然後在應用程式程式碼中新增回報器。在背景工作完成清單填入作業後,請釋放回報器。

等到所有新增的回報器都已釋放後,FullyDrawnReporter 才會呼叫 reportFullyDrawn() 方法。因此請等到背景程序完成後再新增回報器,這樣一來,時間就也會納入在啟動時間資料內填入清單所需的時間。對使用者而言,這並不會改變應用程式行為,但可讓啟動時間資料包含填入清單所需的時間。無論工作順序為何,系統都會在完成所有工作後才呼叫 reportFullyDrawn()

以下範例說明如何在每項背景工作都各自註冊回報器的情況下,同時執行多項這類工作:

Kotlin

class MainActivity : ComponentActivity() {

    sealed interface ActivityState {
        data object LOADING : ActivityState
        data object LOADED : ActivityState
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            var activityState by remember {
                mutableStateOf(ActivityState.LOADING as ActivityState)
            }
            fullyDrawnReporter.addOnReportDrawnListener {
                activityState = ActivityState.LOADED
            }
            ReportFullyDrawnTheme {
                when(activityState) {
                    is ActivityState.LOADING -> {
                        // Display the loading UI.
                    }
                    is ActivityState.LOADED -> {
                        // Display the full UI.
                    }
                }
            }
            SideEffect {
                lifecycleScope.launch(Dispatchers.IO) {
                    fullyDrawnReporter.addReporter()

                    // Perform the background operation.

                    fullyDrawnReporter.removeReporter()
                }
                lifecycleScope.launch(Dispatchers.IO) {
                    fullyDrawnReporter.addReporter()

                    // Perform the background operation.

                    fullyDrawnReporter.removeReporter()
                }
            }
        }
    }
}

Java

public class MainActivity extends ComponentActivity {
    private FullyDrawnReporter fullyDrawnReporter;

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

        fullyDrawnReporter = getFullyDrawnReporter();
        fullyDrawnReporter.addOnReportDrawnListener(() -> {
            // Trigger the UI update.
            return Unit.INSTANCE;
        });

        new Thread(new Runnable() {
            @Override
            public void run() {
                fullyDrawnReporter.addReporter();

                // Do the background work.

               fullyDrawnReporter.removeReporter();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                fullyDrawnReporter.addReporter();

                // Do the background work.

                fullyDrawnReporter.removeReporter();
            }
        }).start();
    }
}

如果應用程式使用 Jetpack Compose,您可以使用下列 API 表示已完成繪製的狀態:

  • ReportDrawn:表示可組合項可立即準備好進行互動。
  • ReportDrawnWhen:使用 list.count > 0 等述詞,表示可組合項何時可準備好進行互動。
  • ReportDrawnAfter:採用暫停方法,完成後就會表示可組合項已準備好進行互動。
找出瓶頸

如要尋找瓶頸,可以使用 Android Studio CPU 分析器。詳情請參閱「使用 CPU 分析器查看 CPU 活動」。

此外,您也可以透過應用程式和活動 onCreate() 方法中的內嵌追蹤記錄,深入瞭解潛在瓶頸。如要瞭解內嵌追蹤記錄,請參閱 Trace 函式的說明文件和「系統追蹤總覽」。

解決常見問題

本節說明會影響應用程式啟動效能的幾個問題。這些問題主要與初始化應用程式、活動物件,以及載入畫面有關。

大量應用程式初始化

當程式碼覆寫 Application 物件,並在初始化該物件的過程中執行大量工作或複雜邏輯時,可能會影響啟動效能。如果 Application 子類別會執行當下還不需完成的初始化作業,應用程式就可能會在啟動過程中浪費時間。

某些初始化作業可能完全沒必要執行,例如當應用程式為了回應意圖,而實際上已啟動時,將主要活動的狀態資訊初始化就是一項非必要的作業。如果使用意圖,應用程式只會使用先前初始化狀態資料的子集。

應用程式初始化期間的其他挑戰包括具影響力或多餘的垃圾收集事件,或與初始化同時進行磁碟 I/O,進一步封鎖初始化程序。使用 Dalvik 執行階段時,垃圾收集是特別需要考量的部分;Android 執行階段 (ART) 會同時執行垃圾收集,將作業的影響降到最低。

診斷問題

您可以利用方法追蹤記錄或內嵌追蹤記錄,嘗試診斷問題所在。

方法追蹤記錄

執行 CPU 分析器會顯示,callApplicationOnCreate() 方法最終會呼叫 com.example.customApplication.onCreate 方法。如果工具顯示這些方法需要很長的時間才能完成執行,請深入瞭解當下進行中的作業為何。

內嵌追蹤記錄

使用內嵌追蹤記錄調查可能的問題情況,包括:

  • 應用程式的初始 onCreate() 函式。
  • 應用程式初始化的任何全域單例模式物件。
  • 瓶頸期間可能會發生的任何磁碟 I/O、去序列化或密集迴圈。

問題解決方案

不論問題原因是非必要初始化作業還是磁碟 I/O,解決方案都是延遲初始化。換句話說,請只初始化立即需要的物件。請採用單例模式,不要建立全域靜態物件,讓應用程式僅在首次需要物件的時候初始化該物件。

此外,建議您使用 Hilt 等依附元件插入架構,在首次插入物件時建立物件和依附元件。

如果應用程式會使用內容供應器,在啟動程序初始化應用程式元件,建議您改用 App Startup 程式庫

大量活動初始化

活動製作通常需要大量的高負載工作。通常這種工作都可以進行最佳化以提升效能。這類常見問題包括:

  • 加載大型或複雜的版面配置。
  • 禁止在磁碟或網路 I/O 上繪圖。
  • 載入和解碼點陣圖。
  • 光柵處理 VectorDrawable 物件。
  • 活動的其他子系統初始化。

診斷問題

在這種情況下,方法追蹤記錄和內嵌追蹤記錄也非常實用。

方法追蹤記錄

使用 CPU 分析器時,請留意應用程式的 Application 子類別建構函式和 com.example.customApplication.onCreate() 方法。

如果工具顯示這些方法需要很長的時間才能完成執行,請深入瞭解當下進行中的作業為何。

內嵌追蹤記錄

使用內嵌追蹤記錄調查可能的問題情況,包括:

  • 應用程式的初始 onCreate() 函式。
  • 應用程式初始化的任何全域單例模式物件。
  • 瓶頸期間可能會發生的任何磁碟 I/O、去序列化或密集迴圈。

問題解決方案

潛在瓶頸有很多,以下是其中兩個常見的問題及補救方式:

  • 檢視區塊階層越大,應用程式就需要越多時間加載。如要解決這個問題,您可以採取以下兩個步驟:
    • 減少多餘或巢狀版面配置,簡化您的檢視區塊階層。
    • 請勿加載無須在啟動期間顯示的使用者介面部分,改用 ViewStub 物件做為子階層的預留位置,讓應用程式可在更合適的時間加載。
  • 在主執行緒完成所有資源初始化作業也可能會降低啟動速度。您可以按照下列步驟解決這個問題:
    • 移動所有資源初始化作業,讓應用程式可在不同的執行緒上逐步執行。
    • 允許應用程式載入及顯示檢視畫面,然後再更新依附於點陣圖和其他資源的視覺屬性。

自訂啟動畫面

如果您之前曾使用以下其中一種方法,在 Android 11 (API 級別 30) 以下版本中實作自訂啟動畫面,可能會發現啟動時需要更多時間:

  • 使用 windowDisablePreview 主題屬性關閉啟動期間系統繪製的初始空白畫面。
  • 使用專屬 Activity

自 Android 12 起,必須遷移至 SplashScreen API。此 API 可加快啟動時間,並可讓您以下列方式微調啟動畫面:

此外,compat 程式庫會向後移植 SplashScreen API,藉此獲得回溯相容性,並讓所有 Android 版本的啟動畫面顯示一致的外觀和風格。

詳情請參閱「啟動畫面遷移指南」。