天天看點

Android應用性能優化最佳實踐.2.5 啟動優化

<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的類名,代碼如下:

&lt;application

android:name=".gmfapplication"

android:allowbackup="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:theme="@style/apptheme" &gt;

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&lt;string, long&gt; mtimetag = new hashmap&lt;string, long&gt;();

    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() &gt; 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() &lt;= 0){

            glog.e(tag,"mtimetag is empty!");

            return;

        iterator iterator = mtimetag.keyset().iterator();

        while (iterator != null &amp;&amp; iterator.hasnext()){

            string tag = (string)iterator.next();

            glog.d(tag,tag + ":" +  mtimetag.get(tag));

    public hashmap&lt;string, long&gt; 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&lt;integer,timemonitor&gt; timemonitorlist = null;

    public synchronized static timemonitormanager getinstance(){

        if(mtimemonitormanager == null){

            mtimemonitormanager = new timemonitormanager();

        return mtimemonitormanager;

    public timemonitormanager(){

        timemonitorlist = new hashmap&lt;integer,timemonitor&gt;();

    // 初始化某個打點子產品

    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"。