請保持淡定,分析代碼,記?。盒阅芎苤匾?/p>
啟動時間優(yōu)化
毫無疑問,應用的啟動速度越快越好。
本文可以幫助你優(yōu)化應用的啟動時間:首先描述應用啟動過程的內(nèi)部機制;然后討論如何分析啟動性能;最后,列舉了一些常見的影響啟動時間的問題,并就如何解決這些問題給出一些提示。
第 1 部分:啟動過程內(nèi)部機制
應用的啟動可能為三種狀態(tài)之一,不同狀態(tài)的啟動時長是不一樣的。三種狀態(tài)分別為:冷啟動(cold start),暖啟動(warm start),熱啟動(lukewarm start)。冷啟動即應用從零開始加載運行,而其它狀態(tài)則是應用從后臺運行回到前臺運行。建議始終基于冷啟動的假設進行優(yōu)化,因為這樣做同樣提升了另兩種啟動狀態(tài)的表現(xiàn)。
要使得應用能快速啟動,首先要理解應用以不同狀態(tài)啟動時,系統(tǒng)和應用內(nèi)發(fā)生了什么,以及它們是如何交互的。
1) 冷啟動 (cold start)
冷啟動狀態(tài):系統(tǒng)不存在該應用的進程,啟動應用才創(chuàng)建出應用的進程。冷啟動一般指的就是應用在開機后或者被系統(tǒng)停止后的第一次啟動過程。因為系統(tǒng)和應用在冷啟動時需要做更多的工作,所以減少它的啟動時間的挑戰(zhàn)是最大的。
冷啟動初始時,系統(tǒng)完成三個任務:
- 啟動和加載應用(這里泛指的是應用本身)
- 創(chuàng)建應用的專屬進程
- 啟動后立刻顯示啟動視圖(通常是個空白屏)
一旦系統(tǒng)創(chuàng)建了應用的專屬進程,該進程開始創(chuàng)建應用:
- 創(chuàng)建應用對象
- 啟動主線程 (MainThread)
- 創(chuàng)建 Main Activity
- 加載視圖 (Inflating views)
- 渲染布局 (Laying out)
- 執(zhí)行初始繪制
一旦應用完成了第一次繪制,系統(tǒng)進程就把當前顯示的啟動視圖切換為應用的界面,這時用戶就可以開始使用應用了。
下圖展示了系統(tǒng)和應用啟動時相互之間的關系:

以上流程中的大部分由系統(tǒng)來控制,出現(xiàn)性能問題的地方往往在 Application 和 Activity 的創(chuàng)建 (onCreate) 過程中。我們先仔細看下這兩個創(chuàng)建過程。
a) Application 的創(chuàng)建

上文說到,當你啟動應用時,屏幕將“立即”出現(xiàn)空白屏幕,這個空白屏幕將在應用完成首屏的繪制時切換為應用的首屏視圖,然后允許用戶開始與應用進行交互。而應用的創(chuàng)建是從 Application.onCreate() 開始的。
如果你在應用中重載了 Application.onCreate(),系統(tǒng)將先調(diào)用應用的該方法。大型的 App 通常會在這里做大量的通用組件、三方 SDK 的初始化操作。
然后應用程序生成主線程——也被稱為 UI 線程,并開始創(chuàng)建 Main Activity。
b) Activity 的創(chuàng)建

應用創(chuàng)建 Activity 的過程為:
- 初始化(Activity init)
- 調(diào)用構造函數(shù)
- 調(diào)用當前生命周期的回調(diào)方法,例如
Activity.onCreate()
通常情況下,onCreate() 方法對加載時間的影響最大,因為它執(zhí)行了開銷最重的工作:加載、渲染和初始化 Activity 所需要的對象,如果布局過于復雜很可能導致嚴重的啟動性能問題。
在這之后,系統(tǒng)和應用按各自的生命周期運行著。
2) 暖啟動(warm start)
應用程序的暖啟動與冷啟動類似,但比冷啟動開銷低。在暖啟動中,系統(tǒng)只需要把 Activity 切換到前臺運行。如果應用的該 Activity 之前駐留在內(nèi)存中,那么應用程序就不用重新初始化對象和渲染布局。
但是,如果由于響應了低內(nèi)存事件,例如在 onTrimMemory() 方法中清除了資源對象,那么這些對象就需要在熱啟動時重新創(chuàng)建。
暖啟動與冷啟動的顯示情況是一致的:系統(tǒng)進程顯示空白屏幕,直到應用程序已經(jīng)完成 Activity 的渲染。
3) 熱啟動(lukewarm start)
熱啟動為冷啟動的過程操作的子集,而且開銷比暖啟動稍小。以下這些情況可以認為是熱啟動:
用戶退出應用,但隨后重新啟動它。應用的進程還在運行,但應用必須重新從
onCreate()開始創(chuàng)建 Activity。系統(tǒng)從內(nèi)存中清除了應用(非用戶主動),然后用戶重新啟動它。進程和 Activity 需要重新啟動,但
onCreate()將接收到保存狀態(tài)的 Bundle。事實上,savedInstanceState在用戶未主動銷毀 Activity 時系統(tǒng)就會調(diào)用。
第 2 部分:剖析啟動性能
為了正確評估啟動時的表現(xiàn),你需要跟蹤應用啟動到顯示需要多長時間。下圖展示了應用初始顯示的時間和完全顯示的時間的定義。

1) 查看初始顯示的時間
a) Displayed
從 Android 4.4(API 19) 開始,logcat 的輸出包括了一行 Displayed 的值。這個值表示了應用啟動進程到 Activity 完成屏幕繪制經(jīng)過的時間。經(jīng)過的時間包括以下事件,按順序為:
- 啟動進程
- 初始化對象
- 創(chuàng)建和初始化 Activity
- 布局渲染
- 完成第一次繪制
報告的日志行看起來類似于下面的例子:
I/ActivityManager: Displayed com.android.contacts/.activities.PeopleActivity: +612ms
如果您在終端使用 logcat,可以直接找到這一行,當然,為了方便需要使用 grep 進行查找。而如果使用 Android Studio 查看,你必須在你的 logcat 視圖中禁用過濾器,因為這是系統(tǒng)打的日志而不是應用本身。一旦您完成了過濾器設置,就可以輕松地搜索到該行查看時間。下圖展示了如何禁用過濾器,及 logcat 窗口顯示 Displayed 時間的例子。

Displayed 顯示的時間是到第一次繪制完成的時候,它并不包括不被布局文件及初始化對象所引用的資源的加載時間,因為這個加載是一個內(nèi)部過程,不阻塞應用初始內(nèi)容的顯示。
b) ADB Shell Activity Manager
你也可以使用 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 一樣地顯示如下內(nèi)容:
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
通過可選參數(shù) -c 和 -a 可以指定 Intent 的 <category> 和 <action>。
- ThisTime:最后一個啟動的 Activity 的啟動耗時
- TotalTime:現(xiàn)在的所有的 Activity 的啟動耗時
- WaitTime:ActivityManagerService 啟動 App 的 Activity 時的總時間,包括前 Activity 的 onPause() 和現(xiàn)在 Activity 的啟動
2) 查看完全顯示的時間
a) reportFullyDrawn()
你可以使用 reportFullyDrawn() 方法來測量應用啟動到所有資源和視圖層次結構的完整顯示之間所經(jīng)過的時間,該方法在應用使用延遲加載的情況下是很有用的。
在延遲加載時,應用在初始的繪圖之后,異步加載資源,然后更新視圖。如果由于延遲加載,應用的初始顯示并不包括所有的資源,你可能會考慮將所有的資源和視圖的完全加載和顯示作為一個單獨的指標。例如:你的用戶界面可能已經(jīng)完成了文本的加載,但又必須從網(wǎng)絡獲取圖像。
為了解決這個問題,你可以手動調(diào)用reportFullyDrawn(),讓系統(tǒng)知道你的 Activity 完成了它的延遲加載。當您使用此方法,logcat 將顯示出從創(chuàng)建應用對象到調(diào)用 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 的圖標高亮時為啟動計時的起點,記錄此時刻到你想要的停止的時刻之間的時間就可以了。簡單來說,就是錄制一個視頻,使用逐幀查看的視頻播放器方便地記錄下你想查看的任意起止時刻。
如果通過以上四種方法測量出應用啟動時間,你發(fā)現(xiàn)啟動時間比預期要慢,你可以嘗試著找出啟動過程中的瓶頸。
3) 識別性能瓶頸
定位性能問題需要用到以下兩種工具:Method Tracer 工具和 Systrace 工具。
a) Method Tracer 工具
在 Android Studio 的 CPU Monitor 欄中,提供了 Method Tracer 工具。

首先需要啟動要監(jiān)控的應用,在 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 可以用來了解程序的流程及方法的運行時間的比例,其計時時間不可直接作為應用性能的表現(xiàn)。
使用 Android Studio 打開 trace 文件,如果是用 CPU Monitor 生成的 trace 文件,Android Studio 會自動打開它,你將得到如下形式的視圖:

| 列名 | 具體含義 |
|---|---|
| Name | 方法名 |
| Invocation Count | 方法調(diào)用次數(shù) |
| Inclusive Time (microseconds) | 該方法及其調(diào)用的子方法的耗時 |
| Exclusive Time (microseconds) | 該方法(不包含調(diào)用的子方法)的耗時 |
圖表的 x 坐標可以選擇 Wall Clock Time 或者 Thread Time,其中前者表示方法調(diào)用到返回結果真實的 CPU 時間,后者表示線程調(diào)度的時間,如果線程不連續(xù)執(zhí)行,那么被中斷的時間將被排除,所以將小于前者的統(tǒng)計。另外,一般通過搜索方法名稱以快速定位到圖表中該方法的位置。關于 Method Tracer 及其視圖的更多信息,請參閱:Method Tracer。
也可以使用 DDMS 打開 trace 文件,其展示的視圖如下所示:

各列名稱及其含義與 Android Studio 的圖示基本類似。
還可以使用 dmtracedump 工具解析生成 html 文件如下圖(dmtracedump 可以生成圖片,但往往混亂到看不出順序,有興趣的可以自行查閱相關資料):

從以上三種方式展示的 trace 文件結果來看,結果中包含了 JDK 函數(shù),第三方庫函數(shù),以及 Android SDK 中函數(shù),如果想僅分析應用中的方法調(diào)用順序信息,可以根據(jù) trace 文件過濾出當前應用下的方法信息。目前 GitHub 上有一個 Windows 平臺下的分析應用方法耗時的 swing 工具,其使用方法很簡單:
- 將 sdk\platform-tools 下的 dmtracedump 添加到系統(tǒng)環(huán)境變量
- 基于 jdk 1.8 環(huán)境運行 Method-trace-analysis.jar
- 直接導入 .trace 文件,一鍵分析(注意:trace 文件路徑不要包含空格)
該工具的思路基于:一個能讓你了解所有函數(shù)調(diào)用順序以及函數(shù)耗時的 Android 庫(無需侵入式代碼),該庫核心就是 2 個 build.gradle 中的 task 基于 dmtracedump 工具對 trace 文件進行解析、過濾。
Method Trace Tool 得到了良好的展示效果,如圖:

以上 trace 文件的幾種展示方式可以讓你了解到關于應用中方法的調(diào)用順序及耗時占比信息(注意:該耗時信息不代表真正使用場景下的耗時,所以時間比例是個更有用的信息),基于以上信息可以分析出一個方法或者一個環(huán)節(jié)是否成為了性能瓶頸。
b) Systrace 工具
另一個跟蹤的方法就是 Systrace 的使用了。
Systrace 是 Android 4.1 及以上版本提供的性能數(shù)據(jù)采樣和分析工具。它可以幫助開發(fā)者收集 Android 關鍵子系統(tǒng)(如:surfaceflinger、WindowManagerService 等 Framework 部分關鍵模塊、服務, View 系統(tǒng))的運行信息,從而幫助開發(fā)者更直觀地分析系統(tǒng)瓶頸,改進性能。
Systrace 的功能包括跟蹤系統(tǒng)的 I/O 操作、內(nèi)核工作隊列、 CPU 負載等,很好收集分析 UI 顯示性能的數(shù)據(jù)。 Systrace 工具可以跟蹤、收集、檢查定時信息,可以很直觀地查看 CPU 周期消耗的具體時間,顯示每個線程和進程的跟蹤信息,使用了不同的顏色來突出問題的嚴重性,并提供了解決這些問題的一些建議。
使用方法:
- 收集 trace 數(shù)據(jù)(具體可查看:Systrace Walkthrough)
Steps for starting Systrace
Steps for creating a trace.png
收集 trace 數(shù)據(jù)還可以通過命令行的方式,使用命令行配置好后多次使用可以快速得到數(shù)據(jù),不用每次手動點擊去收集。
$ cd android-sdk/platform-tools/systrace
$ python systrace.py --time=10 -o mynewtrace.html sched gfx view wm
關于命令行的參數(shù)及配置請查看:Systrace command reference
-
使用 Chrome 打開 trace.html 文件,使用 WASD 進行縮放、移動查看
Systrace display after zooming in on a long-running frame.png
具體的相關的信息分析可從網(wǎng)上查找經(jīng)驗總結博客。
注意:由于 Systrace 是以系統(tǒng)的角度返回一些信息,并不能定位到具體的耗時的方法,要進一步獲取 CPU 被占用的原因,就需要使用另一個分析工具 Traceview。
剛才說到 Systrace 收集展示的是系統(tǒng)的信息,實際上在 4.3 之后,可以通過插入代碼的方式,在 Systrace 里顯示想要查看的 API 的耗時以及調(diào)用關系。舉個例子:
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 來追蹤應用的代碼片段,有兩個需要注意的地方:
- 這兩個 API 需要放在同一個線程里
- 這兩個 API 需要成對出現(xiàn),而且每一個 endSection 都只會與最近的 beginSection 對應
了解更多關于 Systrace 的信息,請參閱 Trace 功能的參考文檔,以及 Systrace 工具的介紹。
第 3 部分:常見問題
本節(jié)討論幾個常見的影響應用啟動性能的問題。主要是關注應用與 Activity 對象的初始化以及畫面的加載。
1) Application 初始化開銷大
正如上文所述,Application 的創(chuàng)建過程中,如果執(zhí)行復雜的邏輯或者初始化大量的對象,將會影響應用的啟動體驗。具體而言,就是你繼承了 Application 并在初始化時執(zhí)行了不必要的代碼,比如:初始化 MainActivity 的狀態(tài)信息;創(chuàng)建了大量臨時變量導致 GC(GC 在 ART 下影響很?。?;執(zhí)行磁盤 I/O 操作(這甚至就會直接阻塞應用的執(zhí)行);反序列化操作;多重循環(huán)等等。
解決問題的方法
懶加載:只初始化那些必要的對象,而其他的全局靜態(tài)對象移動到一個單例模式中。此外,可以考慮依賴注入框架 Dagger2 來創(chuàng)建對象及其依賴關系。
2) Activity 初始化開銷大
Activity 的創(chuàng)建中除了要避免 Application 創(chuàng)建中提到的問題,還需要注意以下問題:
- 加載極其復雜的布局
- 主線程中出現(xiàn)磁盤或網(wǎng)絡 I/O
- 加載和解碼 Bitmap
- 渲染多個 VectorDrawable 對象。
解決問題的方法
這部分的問題要具體分析解決,常見的共通的兩個問題的解決辦法如下:
- 視圖層次過深:
- 減少冗余、嵌套的布局層次。
- 不布局繪制不可見的 UI,而是使用 ViewStub 對象在適當?shù)臅r間布局繪制。
- 大量的資源初始化:
- 調(diào)整資源初始化的位置,可以在不同的線程執(zhí)行懶加載。
- 加載部分視圖,然后再加載大的位圖和其他資源。
3) 啟動界面
文章開始就說到應用啟動時會立即顯示啟動界面,而這通常是個白屏,你不妨給應用設置一個與主界面類似的啟動畫面,這樣做可以向用戶隱藏這個啟動過程,用戶會感受到應用已經(jīng)在運行了,顯示的界面就是應用的一部分或者說是流程的一部分。
有一個粗暴的辦法是使用 windowDisablePreview 主題屬性來去除應用啟動時的空白屏。但這種方法會讓用戶點擊之后覺得沒有響應,而不知道應用已經(jīng)開始啟動了,這種體驗不好,基本不會采用該方法。
解決問題的方法
使用 Activity 的 windowBackground 屬性,在啟動時顯示簡單的自定義的畫面。
首先創(chuàng)建一個要在啟動時顯示的畫面,可以像如下所示:
<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() 之前調(diào)用 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);
// ...
}
}


