1. 程式人生 > >Android 效能優化——啟動時間優化指南

Android 效能優化——啟動時間優化指南

請保持淡定,分析程式碼,記住:效能很重要。

毫無疑問,應用的啟動速度越快越好。

本文可以幫助你優化應用的啟動時間:首先描述應用啟動過程的內部機制;然後討論如何分析啟動效能;最後,列舉了一些常見的影響啟動時間的問題,並就如何解決這些問題給出一些提示。

第 1 部分:啟動過程內部機制

應用的啟動可能為三種狀態之一,不同狀態的啟動時長是不一樣的。三種狀態分別為:冷啟動(cold start),暖啟動(warm start),熱啟動(lukewarm start)。冷啟動即應用從零開始載入執行,而其它狀態則是應用從後臺執行回到前臺執行。建議始終基於冷啟動的假設進行優化,因為這樣做同樣提升了另兩種啟動狀態的表現。

要使得應用能快速啟動,首先要理解應用以不同狀態啟動時,系統和應用內發生了什麼,以及它們是如何互動的。

1) 冷啟動 (cold start)

冷啟動狀態:系統不存在該應用的程序,啟動應用才創建出應用的程序。冷啟動一般指的就是應用在開機後或者被系統停止後的第一次啟動過程。因為系統和應用在冷啟動時需要做更多的工作,所以減少它的啟動時間的挑戰是最大的。

冷啟動初始時,系統完成三個任務:

  • 啟動和載入應用(這裡泛指的是應用本身)
  • 建立應用的專屬程序
  • 啟動後立刻顯示啟動檢視(通常是個空白屏)

一旦系統建立了應用的專屬程序,該程序開始建立應用:

  1. 建立應用物件
  2. 啟動主執行緒 (MainThread)
  3. 建立 Main Activity
  4. 載入檢視 (Inflating views)
  5. 渲染布局 (Laying out)
  6. 執行初始繪製

一旦應用完成了第一次繪製,系統程序就把當前顯示的啟動檢視切換為應用的介面,這時使用者就可以開始使用應用了。

下圖展示了系統和應用啟動時相互之間的關係:

系統和應用啟動流程

以上流程中的大部分由系統來控制,出現效能問題的地方往往在 Application 和 Activity 的建立 (onCreate) 過程中。我們先仔細看下這兩個建立過程。

a) Application 的建立

Application onCreate

上文說到,當你啟動應用時,螢幕將“立即”出現空白螢幕,這個空白螢幕將在應用完成首屏的繪製時切換為應用的首屏檢視,然後允許使用者開始與應用進行互動。而應用的建立是從 Application.onCreate()

開始的。

如果你在應用中過載了 Application.onCreate(),系統將先呼叫應用的該方法。大型的 App 通常會在這裡做大量的通用元件、三方 SDK 的初始化操作。

然後應用程式生成主執行緒——也被稱為 UI 執行緒,並開始建立 Main Activity。

b) Activity 的建立

Paste_Image.png

應用建立 Activity 的過程為:

  1. 初始化(Activity init)
  2. 呼叫建構函式
  3. 呼叫當前生命週期的回撥方法,例如 Activity.onCreate()

通常情況下,onCreate() 方法對載入時間的影響最大,因為它執行了開銷最重的工作:載入、渲染和初始化 Activity 所需要的物件,如果佈局過於複雜很可能導致嚴重的啟動效能問題。

在這之後,系統和應用按各自的生命週期執行著。

2) 暖啟動(warm start)

應用程式的暖啟動與冷啟動類似,但比冷啟動開銷低。在暖啟動中,系統只需要把 Activity 切換到前臺執行。如果應用的該 Activity 之前駐留在記憶體中,那麼應用程式就不用重新初始化物件和渲染布局。

但是,如果由於響應了低記憶體事件,例如在 onTrimMemory() 方法中清除了資源物件,那麼這些物件就需要在熱啟動時重新建立。

暖啟動與冷啟動的顯示情況是一致的:系統程序顯示空白螢幕,直到應用程式已經完成 Activity 的渲染。

3) 熱啟動(lukewarm start)

熱啟動為冷啟動的過程操作的子集,而且開銷比暖啟動稍小。以下這些情況可以認為是熱啟動:

  1. 使用者退出應用,但隨後重新啟動它。應用的程序還在執行,但應用必須重新從 onCreate() 開始建立 Activity。

  2. 系統從記憶體中清除了應用(非使用者主動),然後使用者重新啟動它。程序和 Activity 需要重新啟動,但 onCreate() 將接收到儲存狀態的 Bundle。事實上,savedInstanceState 在使用者未主動銷燬 Activity 時系統就會呼叫。

第 2 部分:剖析啟動效能

為了正確評估啟動時的表現,你需要跟蹤應用啟動到顯示需要多長時間。下圖展示了應用初始顯示的時間和完全顯示的時間的定義。

兩種顯示時間

1) 檢視初始顯示的時間

a) Displayed

從 Android 4.4(API 19) 開始,logcat 的輸出包括了一行 Displayed 的值。這個值表示了應用啟動程序到 Activity 完成螢幕繪製經過的時間。經過的時間包括以下事件,按順序為:

  1. 啟動程序
  2. 初始化物件
  3. 建立和初始化 Activity
  4. 佈局渲染
  5. 完成第一次繪製

報告的日誌行看起來類似於下面的例子:

I/ActivityManager: Displayed com.android.contacts/.activities.PeopleActivity: +612ms

如果您在終端使用 logcat,可以直接找到這一行,當然,為了方便需要使用 grep 進行查詢。而如果使用 Android Studio 檢視,你必須在你的 logcat 檢視中禁用過濾器,因為這是系統打的日誌而不是應用本身。一旦您完成了過濾器設定,就可以輕鬆地搜尋到該行檢視時間。下圖展示瞭如何禁用過濾器,及 logcat 視窗顯示 Displayed 時間的例子。

Android Studio logcat 視窗

Displayed 顯示的時間是到第一次繪製完成的時候,它並不包括不被佈局檔案及初始化物件所引用的資源的載入時間,因為這個載入是一個內部過程,不阻塞應用初始內容的顯示。

b) ADB Shell Activity Manager

adb shell am start -S -W com.android.contacts/.activities.PeopleActivity
-c android.intent.category.LAUNCHER
-a android.intent.action.MAIN

你的終端視窗就像顯示 Displayed 一樣地顯示如下內容:

Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.android.contacts/.activities.PeopleActivity }
Status: ok
Activity: com.android.contacts/.activities.PeopleActivity
ThisTime: 701
TotalTime: 701
WaitTime: 718
Complete

通過可選引數 -c 和 -a 可以指定 Intent 的 <category> 和 <action>。
* ThisTime:最後一個啟動的 Activity 的啟動耗時
* TotalTime:現在的所有的 Activity 的啟動耗時
* WaitTime:ActivityManagerService 啟動 App 的 Activity 時的總時間,包括前 Activity 的 onPause() 和現在 Activity 的啟動

2) 檢視完全顯示的時間

a) reportFullyDrawn()

你可以使用 reportFullyDrawn() 方法來測量應用啟動到所有資源和檢視層次結構的完整顯示之間所經過的時間,該方法在應用使用延遲載入的情況下是很有用的。

在延遲載入時,應用在初始的繪圖之後,非同步載入資源,然後更新檢視。如果由於延遲載入,應用的初始顯示並不包括所有的資源,你可能會考慮將所有的資源和檢視的完全載入和顯示作為一個單獨的指標。例如:你的使用者介面可能已經完成了文字的載入,但又必須從網路獲取影象。

為了解決這個問題,你可以手動呼叫reportFullyDrawn(),讓系統知道你的 Activity 完成了它的延遲載入。當您使用此方法,logcat 將顯示出從建立應用物件到呼叫 reportFullyDrawn() 方法的時間。下面是 logcat 的輸出的例子:

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

b) screenrecord

還有一種測量啟動時間的方法值得一提,因為這種方法雖然繁瑣但可以很直觀檢視起止位置的時間,那就是使用 screenrecord 命令。該命令可以直接錄製螢幕,通過以下命令啟動:

adb shell screenrecord --bugreport /sdcard/launch.mp4

在手機上操作,點選 App,等待其顯示,必要時可以多等待一會兒,然後使用 Ctrl + c 停止命令,就得到了想要的視訊了。使用命令匯出視訊:

adb pull /sdcard/launch.mp4

接著就可以使用一個能逐幀檢視的視訊播放器——例如 QuickTime 播放器來檢視視訊,一般地,認為 App 的圖示高亮時為啟動計時的起點,記錄此時刻到你想要的停止的時刻之間的時間就可以了。簡單來說,就是錄製一個視訊,使用逐幀檢視的視訊播放器方便地記錄下你想檢視的任意起止時刻。

如果通過以上四種方法測量出應用啟動時間,你發現啟動時間比預期要慢,你可以嘗試著找出啟動過程中的瓶頸。

3) 識別效能瓶頸

定位效能問題需要用到以下兩種工具:Method Tracer 工具和 Systrace 工具。

a) Method Tracer 工具

在 Android Studio 的 CPU Monitor 欄中,提供了 Method Tracer 工具。

Android Monitor 中的 Method Tracer

首先需要啟動要監控的應用,在 Android Studio 下方的 Android Monitor 中選擇該應用的程序(圖中長方框位置),就可以看到 Memory Monitor / CPU Monitor / Network Monitor 都開始工作起來。

如果要使用 Method Trace 功能,只需要點選 Start Method Tracing(圖中小方框),在手機上進行操作之後,再次點選它停止 Method Trace,稍等片刻就能在工程的 captures 資料夾中找到 .trace 檔案了。

由以上流程可以知道對於冷啟動而言是無法在正確的時間啟動該工具以獲得日誌資訊的。這種情況下可以在程式碼中合適的位置,例如 onCreate()onWindowFocusChanged 中,分別新增 android.os.Debug.startMethodTracing()android.os.Debug.stopMethodTracing() 方法來生成 trace 檔案,該檔案生成在 sdcard 根目錄下或者應用的目錄中。

Note: 執行 Method Trace 將明顯地影響應用的執行速率。 所以 Method Trace 可以用來了解程式的流程及方法的執行時間的比例,其計時時間不可直接作為應用效能的表現。

使用 Android Studio 開啟 trace 檔案,如果是用 CPU Monitor 生成的 trace 檔案,Android Studio 會自動開啟它,你將得到如下形式的檢視:

Android Studio 中 trace 檔案的展示

列名 具體含義
Name 方法名
Invocation Count 方法呼叫次數
Inclusive Time (microseconds) 該方法及其呼叫的子方法的耗時
Exclusive Time (microseconds) 該方法(不包含呼叫的子方法)的耗時

圖表的 x 座標可以選擇 Wall Clock Time 或者 Thread Time,其中前者表示方法呼叫到返回結果真實的 CPU 時間,後者表示執行緒排程的時間,如果執行緒不連續執行,那麼被中斷的時間將被排除,所以將小於前者的統計。另外,一般通過搜尋方法名稱以快速定位到圖表中該方法的位置。關於 Method Tracer 及其檢視的更多資訊,請參閱:Method Tracer

也可以使用 DDMS 開啟 trace 檔案,其展示的檢視如下所示:

DDMS 中 trace 檔案的展示

各列名稱及其含義與 Android Studio 的圖示基本類似。

還可以使用 dmtracedump 工具解析生成 html 檔案如下圖(dmtracedump 可以生成圖片,但往往混亂到看不出順序,有興趣的可以自行查閱相關資料):

dmtracedump 解析 trace 檔案

從以上三種方式展示的 trace 檔案結果來看,結果中包含了 JDK 函式,第三方庫函式,以及 Android SDK 中函式,如果想僅分析應用中的方法呼叫順序資訊,可以根據 trace 檔案過濾出當前應用下的方法資訊。目前 GitHub 上有一個 Windows 平臺下的分析應用方法耗時的 swing 工具,其使用方法很簡單:

  • 將 sdk\platform-tools 下的 dmtracedump 新增到系統環境變數
  • 基於 jdk 1.8 環境執行 Method-trace-analysis.jar
  • 直接匯入 .trace 檔案,一鍵分析(注意:trace 檔案路徑不要包含空格)

該工具的思路基於:一個能讓你瞭解所有函式呼叫順序以及函式耗時的 Android 庫(無需侵入式程式碼),該庫核心就是 2 個 build.gradle 中的 task 基於 dmtracedump 工具對 trace 檔案進行解析、過濾。

Method Trace Tool 得到了良好的展示效果,如圖:

Method Trace Tool 中 trace 檔案的展示

以上 trace 檔案的幾種展示方式可以讓你瞭解到關於應用中方法的呼叫順序及耗時佔比資訊(注意:該耗時資訊不代表真正使用場景下的耗時,所以時間比例是個更有用的資訊),基於以上資訊可以分析出一個方法或者一個環節是否成為了效能瓶頸。

b) Systrace 工具

另一個跟蹤的方法就是 Systrace 的使用了。

Systrace 是 Android 4.1 及以上版本提供的效能資料取樣和分析工具。它可以幫助開發者收集 Android 關鍵子系統(如:surfaceflinger、WindowManagerService 等 Framework 部分關鍵模組、服務, View 系統)的執行資訊,從而幫助開發者更直觀地分析系統瓶頸,改進效能。

Systrace 的功能包括跟蹤系統的 I/O 操作、核心工作佇列、 CPU 負載等,很好收集分析 UI 顯示效能的資料。 Systrace 工具可以跟蹤、收集、檢查定時資訊,可以很直觀地檢視 CPU 週期消耗的具體時間,顯示每個執行緒和程序的跟蹤資訊,使用了不同的顏色來突出問題的嚴重性,並提供瞭解決這些問題的一些建議。

使用方法:

  1. 收集 trace 資料還可以通過命令列的方式,使用命令列配置好後多次使用可以快速得到資料,不用每次手動點選去收集。

$ cd android-sdk/platform-tools/systrace
$ python systrace.py --time=10 -o mynewtrace.html sched gfx view wm

關於命令列的引數及配置請檢視:Systrace command reference
2. 使用 Chrome 開啟 trace.html 檔案,使用 WASD 進行縮放、移動檢視
Systrace display after zooming in on a long-running frame.png

具體的相關的資訊分析可從網上查詢經驗總結部落格。

注意:由於 Systrace 是以系統的角度返回一些資訊,並不能定位到具體的耗時的方法,要進一步獲取 CPU 被佔用的原因,就需要使用另一個分析工具 Traceview。

剛才說到 Systrace 收集展示的是系統的資訊,實際上在 4.3 之後,可以通過插入程式碼的方式,在 Systrace 裡顯示想要檢視的 API 的耗時以及呼叫關係。舉個例子:

public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {

    ...

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Trace.beginSection("MyAdapter.onCreateViewHolder");
        MyViewHolder myViewHolder;
        try {
            myViewHolder = MyViewHolder.newInstance(parent);
        } finally {
            Trace.endSection();
        }
        return myViewHolder;
    }

   @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        Trace.beginSection("MyAdapter.onBindViewHolder");
        try {
            try {
                Trace.beginSection("MyAdapter.queryDatabase");
                RowItem rowItem = queryDatabase(position);
                mDataset.add(rowItem);
            } finally {
                Trace.endSection();
            }
            holder.bind(mDataset.get(position));
        } finally {
            Trace.endSection();
        }
    }

…

}

通過 Trace.beginSection 和 Trace.endSection 來追蹤應用的程式碼片段,有兩個需要注意的地方:

  1. 這兩個 API 需要放在同一個執行緒裡
  2. 這兩個 API 需要成對出現,而且每一個 endSection 都只會與最近的 beginSection 對應

瞭解更多關於 Systrace 的資訊,請參閱 Trace 功能的參考文件,以及 Systrace 工具的介紹。

第 3 部分:常見問題

本節討論幾個常見的影響應用啟動效能的問題。主要是關注應用與 Activity 物件的初始化以及畫面的載入。

1) Application 初始化開銷大

正如上文所述,Application 的建立過程中,如果執行復雜的邏輯或者初始化大量的物件,將會影響應用的啟動體驗。具體而言,就是你繼承了 Application 並在初始化時執行了不必要的程式碼,比如:初始化 MainActivity 的狀態資訊;建立了大量臨時變數導致 GC(GC 在 ART 下影響很小);執行磁碟 I/O 操作(這甚至就會直接阻塞應用的執行);反序列化操作;多重迴圈等等。

解決問題的方法

懶載入:只初始化那些必要的物件,而其他的全域性靜態物件移動到一個單例模式中。此外,可以考慮依賴注入框架 Dagger2 來建立物件及其依賴關係。

2) Activity 初始化開銷大

Activity 的建立中除了要避免 Application 建立中提到的問題,還需要注意以下問題:
* 載入極其複雜的佈局
* 主執行緒中出現磁碟或網路 I/O
* 載入和解碼 Bitmap
* 渲染多個 VectorDrawable 物件。

解決問題的方法

這部分的問題要具體分析解決,常見的共通的兩個問題的解決辦法如下:

  1. 檢視層次過深:
    • 減少冗餘、巢狀的佈局層次。
    • 不佈局繪製不可見的 UI,而是使用 ViewStub 物件在適當的時間佈局繪製。
  2. 大量的資源初始化:
    • 調整資源初始化的位置,可以在不同的執行緒執行懶載入。
    • 載入部分檢視,然後再載入大的點陣圖和其他資源。

3) 啟動介面

文章開始就說到應用啟動時會立即顯示啟動介面,而這通常是個白屏,你不妨給應用設定一個與主介面類似的啟動畫面,這樣做可以向用戶隱藏這個啟動過程,使用者會感受到應用已經在運行了,顯示的介面就是應用的一部分或者說是流程的一部分。

有一個粗暴的辦法是使用 windowDisablePreview 主題屬性來去除應用啟動時的空白屏。但這種方法會讓使用者點選之後覺得沒有響應,而不知道應用已經開始啟動了,這種體驗不好,基本不會採用該方法。

解決問題的方法

使用 Activity 的 windowBackground 屬性,在啟動時顯示簡單的自定義的畫面。

首先建立一個要在啟動時顯示的畫面,可以像如下所示:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
  <!-- The background color, preferably the same as your normal theme -->
  <item android:drawable="@android:color/white"/>
  <!-- Your product logo - 144dp color version of your app icon -->
  <item>
    <bitmap
      android:src="@drawable/product_logo_144dp"
      android:gravity="center"/>
  </item>
</layer-list>

然後在自定義一個 style:

<style name="AppTheme.Launcher" parent="@style/PeopleTheme">
    <item name="android:windowBackground">@drawable/start_activity_background</item>
</style>

在 AndroidManifest 檔案中 Activity 的屬性裡設定該 style:

<activity ...
android:theme="@style/AppTheme.Launcher" />

這樣子在應用啟動時顯示的畫面就是你的自定義的畫面了。但進入 Activity 後要正確的設定回正確的 style。

最簡單的方法是在 super.onCreate() 之前呼叫 setTheme(R.style.AppTheme),如下所示:

public class MyMainActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // Make sure this is before calling super.onCreate
    setTheme(R.style.Theme_MyApp);
    super.onCreate(savedInstanceState);
    // ...
  }
}