<b>2.5 啟動優化</b>
<b></b>
随着應用的功能越來越豐富、啟動時需要初始化的工作多、界面的元素複雜等,啟動速度不可避免地受到影響,比如一開始單擊時出現黑屏或者白屏,甚至在低端機型上出現假死的現象,本節通過學習應用的啟動流程、啟動速度的監控,發現影響啟動速度的問題所在,并優化啟動的邏輯,提高應用的啟動速度。
2.5.1 應用啟動流程
android應用程式的載體是apk檔案,其中包含了元件和資源,apk檔案可能運作在一個獨立的程序中,也有可能産生多個程序,還可以多個apk運作在同一個程序中,可以通過不同的方式來實作。但有兩點需要注意,第一,每個應用隻對應一個application對象,并且啟動應用一定會産生一個application對象;第二,應用程式可視化元件activity是應用的基本組成之一,是以要分析啟動的性能,就有必要了解這兩個對象的工作流程和生命周期。
1.?application
application是android系統架構中的一個系統元件,android程式啟動時,系統會建立一個application對象,用來存儲系統的一些資訊。android系統會自動在每個程式運作時建立一個application類的對象,并且隻建立一個,可以了解為application是一個單例類。
應用可以不指定一個具體的application,系統會自動建立,但一般在開發中都會建立一個繼承于系統application的類實作一些功能,比如一些資料庫的建立、子產品的初始化等。但這個派生類必須在androidmanifest.xml中定義好,在application标簽增加name屬性,并添加自己的application的類名,代碼如下:
<application
android:name=".gmfapplication"
android:allowbackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/apptheme" >
androidmanifest.xml中的application标簽很多,這些标簽的說明在官網有詳細的介紹,這裡就不做講解。
啟動application時,系統會建立一個pid,即程序id,所有的activity都會在此程序上運作。在application建立時初始化全局變量,同一個應用的所有activity都可以取到這些全局變量的值,application對象的生命周期是整個程式中最長的,它的生命周期就等于這個應用程式的生命周期,因為它是全局的單例的,是以在不同的activity或者service中獲得的對象都是同一個對象。是以在安卓中要避免使用靜态變量來存儲長久儲存的值,可以用application,但并不建議使用太多的全局變量。
androidmanifest.xml檔案上的application标簽指定了重寫的application類後,看看該類可以重載的幾個抽象接口,代碼清單2-7是一個自定義的application。
代碼清單2-7 自定義的application
public class gmfapplication extends application {
private static context mcontext = null;
@override
protected void attachbasecontext(context base) {
super.attachbasecontext(base);
mcontext = this;
}
public void oncreate() {
super.oncreate();
public void onterminate() {
super.onterminate();
public void onconfigurationchanged(configuration newconfig) {
super.onconfigurationchanged(newconfig);
public void onlowmemory() {
super.onlowmemory();
public void ontrimmemory(int level) {
super.ontrimmemory(level);
public static context getcontext(){
return mcontext;
}
從代碼清單2-7中可以看到幾個重要的抽象接口,這些接口的調用時機如下:
attachbasecontext(context base):得到應用上下文的context,在應用建立時首先調用。
oncreate():應用建立時調用,晚于attachbasecontext()方法。
onterminate():應用結束時調用。
onconf?igurationchanged():系統配置發生變化時調用。
onlowmemory():系統低記憶體時調用。
ontrimmemory(int level):系統要求應用釋放記憶體時調用,level為級别。
從上面的抽象方法可以看出,這些方法都在這個應用生命周期之中,attachbasecontext和oncreate在應用建立時必須調用,而其他需要滿足一定的觸發時機。
在開發過程中,盡量使用application中的context執行個體,因為使用activity中的context可能會導緻記憶體洩漏。也可以使用activity的getapplicationcontext方法。
2.?activity
activity大家都非常熟悉了,這裡也不做太多解釋,隻需要了解它的生命周期,因為這在啟動優化的過程中非常重要。
在activity的生命周期中,系統會按類似于階梯金字塔的順序調用一組核心的生命周期方法,如圖2-37所示。也就是說,activity生命周期的每個階段就是金字塔上的一階。當系統建立一個新activity執行個體時,每個回調方法會将activity狀态向頂端移動一階。金字塔頂端是activity在前台運作并且使用者可以與其互動的時間點。當使用者開始離開activity時,系統調用其他方法在金字塔中将activity狀态下移,進而銷毀activity。在有些情況下,activity将隻在金字塔中部分下移并等待(如當使用者切換到其他應用時),activity可從該點開始移回頂端(如果使用者傳回到該activity),并在使用者停止的位置繼續。
圖2-37 activty生命周期金字塔模型
大多數應用包含若幹不同的activity,使用者可通過這些activity執行不同的操作。無論activity是使用者單擊應用圖示時建立的主activity,還是應用在響應使用者操作時開始的其他activity,系統都會調用其oncreate()方法建立activity的每個新執行個體。是以必須實作oncreate()方法,執行後,在activity整個生命周期中隻需要出現一次基本應用啟動邏輯。例如,oncreate()的實作應定義使用者界面并且可能執行個體化某些類範圍變量、聲明使用者界面(在xml布局檔案中定義)、定義成員變量,以及配置某些ui。
oncreate()方法包括一個savedinstancestate參數,在有關重新建立activity中非常有用。
從application和activity的介紹中,可以總結出應用啟動的流程,如圖2-38所示。
其中,啟動分為兩種類型:冷啟動和熱啟動。
冷啟動:因為系統會重新建立一個新的程序配置設定給它,是以會先建立和初始化application類,再建立和初始化main-activity類(包括一系列的測量、布局、繪制),最後顯示在界面上,如圖2-38所示。
熱啟動:因為會從已有的程序中啟動,是以熱啟動不會再建立和初始化application,而是直接建立和初始化mainactivity(包括一系列的測量、布局、繪制),即application隻會初始化一次,隻包含activity中的生命周期流程。
2.5.2 啟動耗時監測
因為一個應用在啟動或者跳入某個頁面時是否流暢,時間是否太長,僅僅通過肉眼來觀察是非常不準确的,并且在不同裝置和環境會有完全不同的表現,是以要準确知道耗時,就需要有效準确的資料,首先通過shell來擷取啟動耗時。
1.?adb shell am
應用啟動的時間會受到很多因素的影響,比如首次安裝後需要解壓apk檔案,繪制時gpu的耗時等,是以在應用層很難擷取到啟動耗時,但借助adb可以得到準确的啟動時間。
使用adb shell獲得應用真實的啟動時間,代碼如下:
adb shell am start -w [packagename]/[packagename.appstartactivity]
執行後可以得到三個時間:
thistime:一般和totaltime時間一樣,如果在應用啟動時開了一個過度的全透明的頁面(activity)預先處理一些事,再顯示出首頁面(activity),這樣将比totaltime小。
totaltime:應用的啟動時間,包括建立程序+application初始化+activity初始化到界面顯示。
waittime:一般比totaltime大些,包括系統影響的耗時。
但這個方法隻能得到固定的某一個階段的耗時,不能得到具體哪個方法的耗時,下面介紹第二個方案:代碼打點輸出耗時。
2.?代碼打點
通過代碼打點來準确擷取記錄每個方法的執行時間,知道哪些地方耗時,然後再有針對性地優化,下面通過一個簡單的例子來講解打點的方案。
以下代碼是一個統計耗時的資料結構,通過這個資料結構記錄整個過程的耗時情況。
public class timemonitor {
private final string tag = "timemonitor";
private int monitorid = -1;
// 儲存一個耗時統計子產品的各種耗時,tag對應某一個階段的時間
private hashmap<string, long> mtimetag = new hashmap<string, long>();
private long mstarttime = 0;
public timemonitor(int id) {
glog.d(tag,"init timemonitor id:" + id);
monitorid = id;
public int getmonitorid() {
return monitorid;
public void startmoniter() {
// 每次重新啟動,都需要把前面的資料清除,避免統計到錯誤的資料
if (mtimetag.size() > 0) {
mtimetag.clear();
}
mstarttime = system.currenttimemillis();
// 打一次點,tag交線需要統計的上層自定義
public void recodingtimetag(string tag) {
// 檢查是否儲存過相同的tag
if (mtimetag.get(tag) != null) {
mtimetag.remove(tag);
long time = system.currenttimemillis() - mstarttime;
glog.d(tag, tag + ":" + time);
mtimetag.put(tag, time);
public void end(string tag,boolean writelog){
recodingtimetag(tag);
end(writelog);
public void end(boolean writelog) {
if (writelog) {
// 寫入到本地檔案
testshowdata();
public void testshowdata(){
if(mtimetag.size() <= 0){
glog.e(tag,"mtimetag is empty!");
return;
iterator iterator = mtimetag.keyset().iterator();
while (iterator != null && iterator.hasnext()){
string tag = (string)iterator.next();
glog.d(tag,tag + ":" + mtimetag.get(tag));
public hashmap<string, long> gettimetags() {
return mtimetag;
這個對象可以用在很多需要統計的地方,不僅可以統計應用啟動的耗時,還可以統計其他子產品,如統計一個activity的啟動耗時和一個fragment的啟動耗時。流程為:在建立這個對象時,需要傳入一個id,這個id是需要統計的子產品或者一個生命周期流程的id,id自定義并且是唯一的,一個timemonitor對應一個id。其中end(boolean writelog)方法表示這個監控的流程結束,其中writelog表示是否需要寫入本地,建議實作這個方法,可以統計一系列的資料,最好上傳到伺服器,用來監控這個應用在外網的實際啟動狀況。
上傳到伺服器時建議抽樣上報,比如根據使用者id的尾号來抽樣上報,雖然不影響性能,但還是盡量不要全部上報,用背景下發抽樣比較好。
比如現在要統計啟動應用在各階段的耗時,就自定義一個id,為了使代碼更好管理,編寫一個專門定義所有id的類,友善以後的維護,代碼如下:
public class timemonitorconfig {
// 應用啟動耗時
public static final int time_monitor_id_application_start = 1;
因為耗時統計可能會在多個子產品和類中需要打點,是以需要一個單例類來管理各個耗時統計的資料,這裡使用了一個單例類來實作:timemonitormanager,代碼如下:
public class timemonitormanager {
private static timemonitormanager mtimemonitormanager = null;
private static context mcontext = null;
private hashmap<integer,timemonitor> timemonitorlist = null;
public synchronized static timemonitormanager getinstance(){
if(mtimemonitormanager == null){
mtimemonitormanager = new timemonitormanager();
return mtimemonitormanager;
public timemonitormanager(){
timemonitorlist = new hashmap<integer,timemonitor>();
// 初始化某個打點子產品
public void resettimemonitor(int id){
if(timemonitorlist.get(id) != null){
timemonitorlist.remove(id);
gettimemonitor(id);
// 擷取打點器
public timemonitor gettimemonitor(int id){
timemonitor monitor = timemonitorlist.get(id);
if(monitor == null){
monitor = new timemonitor(id);
timemonitorlist.put(id,monitor);
return monitor;
在有需要的地方通過這個方法進行打點,為了得到有效的資料,總結起來主要在兩個方面需要打點:
應用程式的生命周期節點,如application的oncreate、activity或fragment的回調函(oncreate、onresume等)。
啟動時需要初始化的重要方法,如資料庫初始化、讀取本地的一些資料等。
其他耗時的一些算法。
例如,在啟動時加入統計,在application和第一個activity加入打點統計,結合前面講過的啟動生命周期,首先進入的是application的attachbasecontext()方法,然後在oncreate結束時打第一個點,在appstartactivity結束打第二個點,在appstartactivity中的onstart()打最後一個點,代碼如下:
application:
@override
protected void attachbasecontext(context base) {
super.attachbasecontext(base); timemonitormanager.getinstance().resettimemonitor(timemonitorconfig.time_monitor_id_application_start);
public void oncreate() {
super.oncreate();
initmodule(); timemonitormanager.getinstance().gettimemonitor(timemonitorconfig.time_monitor_id_application_start).recodingtimetag("applicationcreate");
第一個activity:
protected void oncreate(bundle savedinstancestate) {
timemonitormanager.getinstance().gettimemonitor(timemonitorconfig.time_monitor_id_application_start).recodingtimetag("appstartactivity_create");
super.oncreate(savedinstancestate);
setcontentview(r.layout.activity_app_start);
mlogo = (imageview) this.findviewbyid(r.id.logo);
// mstarthandler.sendemptymessagedelayed(0,1000);
// useanimation();
useanimator();
timemonitormanager.getinstance().gettimemonitor(timemonitorconfig.time_monitor_id_application_start).recodingtimetag("appstartactivity_createover");
protected void onstart() {
super.onstart();
timemonitormanager.getinstance().gettimemonitor(timemonitorconfig.time_monitor_id_application_start).end("appstartactivity_start",false);
結果如下所示:
可以在項目中核心基類的關鍵回調函數和核心方法加入打點,另外插樁也是一種不錯的方式。
2.5.3 啟動優化方案
在android應用開發中,應用啟動速度對使用者體驗非常重要,也是一個應用給使用者的第一個性能方面的體驗,是以應用啟動優化是非常有必要的。應用啟動優化的核心思想就是要快,在啟動過程中做盡量少的事。但是應用功能越豐富,子產品越多,需要初始化的地方也越多,導緻了應用啟動變慢。
為了優化啟動的速度,首先要了解啟動時做了什麼,來看一個例子,啟動源碼中性能優化的啟動應用(應用源碼在http://github.com/lyc7898/androidtech),通過打點,統計從單擊打開應用到首頁顯示完成的時間,後面章節會講到打點的注意事項和具體實作。表2-4是通過打點擷取到這個應用啟動時,各個子產品占用的時間。
因為這個應用比較簡單,沒有什麼子產品和資料,是以大頭是在繪制工作上,也就是閃屏頁(顯示啟動logo的頁面)和進入後首頁的布局上,其次是一些初始化工作,但實際上一個稍大型的應用,子產品初始化和資料準備工作的占比會高很多,因為這塊的優化也是非常有必要。
從總體上看,啟動主要完成三件事:ui布局、繪制和資料準備,是以啟動速度的優化就是需要優化這三個過程,我們也可以通過一些啟動界面政策進行優化。接下來從啟動耗時最高的ui布局和啟動加載邏輯兩個方向優化,達到降低啟動耗時的目的。因為這個應用非常簡單,是以不具有代碼性,但優化的流程和方案是通用的。
1.?ui布局
這個應用啟動過程為:啟動應用→application初始化→appstartactivity→homepageactivity。appstartactivity是應用的閃屏頁(也叫啟動頁),我們看到大部分應用都有這麼一個頁面,為什麼要有閃屏頁呢?閃屏頁的存在主要有兩個好處:一是可以作為品牌宣傳展示,如節日營運或熱點事件營運,也可以做廣告展示(不要太低端);其二,因為閃屏一般需要停留一段時間,在這段時間可以做很多事情,比如底層子產品的初始化、資料的預拉取等。
首先需要優化appstartactivity的布局,從前面的章節可以知道,要提高顯示的效率,一是減少布局層級,二是避免過度繪制,因為前面已經有很詳細的例子了,這裡不做過多介紹,優化的步驟如下:
使用prof?ile gpu rendering檢查啟動時是否有嚴重的掉幀,見2.2.1節。
使用hierarchy view檢查布局檔案(xml)分析布局并優化,見2.3.1節。
2.?啟動加載邏輯優化
一個應用越大,涉及的子產品越多,包含的服務甚至程序就會越多,如網絡子產品的初始化、底層資料初始化等,這些加載都需要提前準備好,有些不必要的就不要放到應用中。可以用以下四個次元分整理啟動的各個點:
必要且耗時:啟動初始化,考慮用線程來初始化。
必要不耗時:首頁繪制。
非必要耗時:資料上報、插件初始化。
非必要不耗時:不用想,這塊直接去掉,在需要用的時再加載。
把資料整理出來後,按需實作加載邏輯,采取分步加載、異步加載、延期加載政策,如圖2-39所示。
圖2-39 啟動優化方向
要提高應用的啟動速度,核心思想是在啟動過程中少做事情,越少越好。
在應用中,增加啟動預設圖或者自定義一個theme,在activity首先使用一個預設的界面可以解決部分啟動短暫黑屏問題,如android:theme="@style/theme.appstartload"。