應用程式啟動時間

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

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

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

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

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

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

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

冷啟動

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

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

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

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

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

  1. 建立應用程式物件。
  2. 啟動主執行緒。
  3. 建立主要活動。
  4. 初始化 UI。
  5. 在畫面上繪製 UI。

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

如要進一步瞭解 Compose 中的版面配置階段,請參閱「Jetpack Compose 階段」。

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

應用程式建立

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

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

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

活動建立

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

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

一般而言,onCreate 方法對於載入時間會造成最大的影響。在 Jetpack Compose 應用程式中,onCreate 方法的負擔通常來自初始組合。當應用程式呼叫 setContent 並叫用頂層可組合函式時,就會發生這種情況。如果 UI 階層結構複雜或深入,或是可組合項在主執行緒上執行大量計算,都可能導致組合時間變長。

暖啟動

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

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

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

熱啟動

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

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

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

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

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

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

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

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

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

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

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

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

分析 Jetpack Compose 應用程式時,您可以使用組合追蹤功能,查看 UI 效能的精細詳細資料。請特別留意主執行緒中以 Choreographer#doFrame 開頭的追蹤區段。尋找 Compose:recomposeCompose:layoutCompose:draw 等切片。這些指標會顯示應用程式花費多少時間編寫、測量、放置及繪製特定元件。

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

為了正確診斷開始時間的效能,您可以追蹤顯示應用程式啟動所需時間的指標。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 是以時間值表示,代表總經過時間,包括下列事件順序:

  • 啟動程序。
  • 初始化物件。
  • 建立及初始化代管活動。
  • 初始化 UI。
  • 首次繪製應用程式。

擷取 TTID

如要找出 TTID,請在 Logcat 指令列工具中,搜尋包含名為 Displayed 值的輸出行。這個值是 TTID,看起來類似以下範例,其中 TTID 為 3s534ms:

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

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

圖 4. 停用篩選器,然後在 Logcat 中尋找 Displayed 值。

在所有資源皆已載入並顯示前,Logcat 輸出內容中的 Displayed 指標不一定會擷取所需時間。如果資源沒有在初始組合中加入參照 (例如非同步載入的資料或圖片),或是屬於應用程式為物件初始化而建立的資源,則不會計算在內。由於這些資源會以內嵌程序載入,而且不會封鎖應用程式的初始顯示,因此會予以排除。 如要進一步瞭解資源,請參閱「Compose 中的資源」。

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

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

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

建議您在 Android Studio 中使用 Logcat,但如果您未使用 Android Studio,也可以使用 adb shell 活動管理員指令執行應用程式,以測量 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 第一個影格所需的時間,以及在顯示初始影格後以非同步方式載入的內容,一併計入 TTFD。一般來說,這是應用程式從網路或磁碟載入的主要內容,由應用程式回報。換句話說,TTFD 包含 TTID,以及應用程式可供使用的時間。維持偏低的 TTFD 有助於改善使用者體驗,讓使用者能快速與應用程式互動。

雖然系統可以在主機視窗算繪初始影格時判斷 TTID,但無法自動判斷 TTFD。由於應用程式通常會以非同步方式載入主要內容,因此系統無法得知應用程式何時才能供使用者完整使用。如要判斷 TTFD,應用程式必須在達到完全繪製狀態時向系統發出信號。

擷取 TTFD

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

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

使用 reportFullyDrawn 時,Logcat 會顯示類似下列範例的輸出內容,其中 TTFD 為 1 秒 54 毫秒:

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

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

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

在您知道已達到完全繪製狀態的基本情況下,可以使用 reportFullyDrawn 發出信號。不過,如果背景執行緒必須先完成背景工作,才能達到完全繪製的狀態,您就需要延遲 reportFullyDrawn,才能更準確地評估 TTFD。如要瞭解如何延遲 reportFullyDrawn,請參閱下一個部分。

提高啟動時間準確度

如果應用程式執行延遲載入,且初始顯示畫面未包含所有資源 (例如應用程式從網路擷取圖片時),您可能需要延遲呼叫 reportFullyDrawn,直到應用程式可用為止,這樣才能將清單填入作業納入基準時間。

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

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

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

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

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

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

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 {
                fullyDrawnReporter.addReporter()
                lifecycleScope.launch(Dispatchers.IO) {
                    // Perform the background operation.
                    fullyDrawnReporter.removeReporter()
                }
                fullyDrawnReporter.addReporter()
                lifecycleScope.launch(Dispatchers.IO) {
                    // Perform the background operation.
                    fullyDrawnReporter.removeReporter()
                }
            }
        }
    }
}
找出瓶頸

如要尋找瓶頸,可以使用 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 程式庫

大量活動初始化

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

  • 初始化大型或複雜的 UI。
  • 可組合函式中的大量初始化作業。
  • 禁止在磁碟或網路 I/O 上繪圖。
  • 載入和解碼點陣圖。
  • 光柵處理 VectorDrawable 物件。
  • 應用程式代管活動的其他子系統初始化。

診斷問題

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

方法追蹤記錄

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

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

內嵌追蹤記錄

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

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

問題解決方案

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

  • UI 階層越大,應用程式初始化所需的時間就越長。 解決這個問題時,請考慮下列步驟:
    • 減少多餘或巢狀可組合函式,簡化 UI 階層。
    • 避免在啟動期間進行不必要的組合和重組。
    • 延遲組合非重要 UI。
  • 在主執行緒完成所有資源初始化作業也可能會降低啟動速度。您可以按照下列步驟解決這個問題:
    • 移動所有資源初始化作業,讓應用程式可在不同的執行緒上逐步執行。
    • 先載入並顯示含有預留位置資料的 UI,然後再更新依附於點陣圖和其他資源的視覺屬性。

如要進一步瞭解重組,請參閱「重組」和「取得重組計數」。

自訂啟動畫面

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

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

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

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

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