首頁>技術>

應用的啟動速度對一個APP來說至關重要,會直接影響到使用者體驗,如果啟動速度過慢會導致使用者的流失,本文就啟動速度最佳化分析,為最佳化啟動速度提供一些思路。

1、應用啟動分類

應用有三種啟動狀態,每種狀態都會影響應用向用戶顯示所需的時間:冷啟動、溫啟動和熱啟動。在冷啟動中,應用從頭開始啟動。在另外兩種狀態中,系統需要將後臺執行的應用帶入前臺。

1.1、冷啟動

app冷啟動可以分為兩個階段

第一階段

1、載入並啟動app

2、啟動後立即顯示一個空白的啟動視窗

3、建立app程序

第二階段

1、建立app物件

2、啟動主執行緒

3、建立主Activity

4、載入佈局

5、佈置螢幕

6、首幀繪製

一旦應用程序完成首幀繪製,系統程序就會換掉當前顯示的後臺視窗,替換為主Activity。此時,使用者可以開始使用應用

冷啟動中,作為開發者,能干預的部分主要是Application的OnCreate階段和Activity的onCreate階段,如下圖:

1.2、熱啟動

熱啟動時,系統將activity放到前臺。如果應用程式的所有activity存在記憶體中,則應用程式可以避免重複物件初始化、渲染、繪製操作

1.3、溫啟動

溫啟動時,由於app的程序仍然存在,執行的是冷啟動的第二階段

1、建立app物件

2、啟動主執行緒

3、建立主Activity

4、載入佈局

5、佈置螢幕

6、首幀繪製

溫啟動常見場景:

1、使用者退出應用後又重啟應用。程序可能繼續執行。但應用從呼叫Activity的onCreate方法開始重新執行

2、記憶體不足,系統將應用殺死,然後使用者重新啟動。程序和Activity需要重啟,但傳遞到onCreate的已儲存的例項bundle對於完成啟動有一定幫助

接下來來看下如何獲得啟動時間~

2、獲取啟動時間2.1、使用adb命令獲取

adb shell am start -W [packageName]/[packageName.xxActivity]

adb shell am start -W com.tg.test.gp/com.test.demo.MainActivityStarting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.tg.test.gp/com.test.demo.MainActivity }Status: okLaunchState: COLDActivity: com.tg.test.gp/com.test.demo.MainActivityThisTime: 1344TotalTime: 1344WaitTime: 1346Complete

ThisTime: 最後一個Activity啟動耗時

TotalTime: 啟動時經歷的所有Activity總耗時

WaitTime: AMS啟動所有Activity的總時間(包括啟動目標應用前的)

2.2、使用程式碼打點
public class LaunchTimer {    private static long sTime;    public static void startRecord() {        sTime = System.currentTimeMillis();    }    public static void endRecord() {        endRecord("");    }    public static void endRecord(String msg) {        long cost = System.currentTimeMillis() - sTime;        LogUtils.i(msg + "cost " + cost);    }}

該方式一般為Application初始化attachBaseContext方法打啟動開始時間戳,應用使用者可操作介面完全展示可操作後打結束時間戳,兩時間差即為啟動耗時

但這種方式並不優雅,如果啟動邏輯複雜,方法很多的情況下,最好採用aop進行最佳化。

3、應用啟動最佳化分析工具3.1、TraceView

Traceview是Android自帶的效能分析工具,可圖形化展示方法呼叫時間,呼叫棧,還可以檢視所有執行緒資訊,對分析方法耗時,呼叫鏈是非常好的工具

使用方法是採用程式碼埋點的方式:

1、在開始收集的位置,執行Debug.startMethodTracing("app_trace"),其中引數為自定義檔名

2、在結束收集的位置,執行Debug.endMethodTracing()

檔案生成路徑為/sdcard/Android/data/包名/files/檔名.trace,使用Android Studio可以開啟該檔案

舉個例子:

我們來看下testAdd這個方法的耗時

public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        testAdd(3, 5);    }    private void testAdd(int a, int b) {        Debug.startMethodTracing("app_trace");        int c = a + b;        Log.i("Restart", "c =  a + b = " + c);        Debug.stopMethodTracing();    }}

執行程式後,找到/sdcard/Android/data/com.restart.perference/files/app_trace.trace檔案,在AndroidStudio中雙擊它,是可以解析出檔案中的資訊的,開啟後如下:

下面來看下,CallChart, FlameChart, Top Down, Bottom Up分別是怎麼用的。首先要選擇好時間區域,像本案例中,因為程式很簡單,能分析的區域比較小,選中它後,可得到下圖:

Call Chart得到的圖形中,可以看到程式整個呼叫棧,同時可以看到方法耗時。

比如圖中的testAdd方法,先後呼叫了Debug.startMethodTracing、Log.i、Debug.stopMethodTracing方法。同時從圖中可以看出startMethodTracing方法比stopMethodTracing方法耗時長。在實際最佳化中,找到哪個方法耗時長,針對性最佳化是非常有作用的。

分析時,第三方程式,系統程式碼我們一般是最佳化不了的,CallChart非常貼心地用顏色幫我們區分哪些是我們自己寫的程式碼。橙色的是系統API,藍色一般是第三方API,綠色的才是我們自己寫的。比如圖中的testAdd方法,我們自己寫的是可以透過調整最佳化的。

Flame Chart是Call Chart的倒序圖,作用相似,圖形如下:

Top Down可以看到每個方法內部呼叫了哪些方法,以及每個方法的耗時,耗時佔比,相比於Call Chart的圖形查詢,Top Down則是更具體,有具體的方法耗時資料

圖中的Total代表方法總共的耗時,self代表方法內非方法呼叫的程式碼耗時,Children代表方法內呼叫的其他方法的耗時。

同樣以testAdd方法為例,testAdd方法總耗時為3840us,佔程式執行時間97%,testAdd方法中自身程式碼耗時為342us,佔比為8.65%,testAdd方法中呼叫的其他方法總共耗時3498us,佔比88.52%

private void testAdd(int a, int b) {        Debug.startMethodTracing("app_trace");//算到Children中        int c = a + b;//這一句是算在self耗時中,耗時其實很短        Log.i("Restart", "c =  a + b = " + c);//算到Children中        Debug.stopMethodTracing();//算到Children中}

Bottom Up是Top Down的倒序圖,可以看方法是被哪個方法呼叫的

TraceView中還有個選項值得注意,

在右上角有個Wall Clock TimeThread Time的選項,其中Wall Clock Time的意思是方法執行的實際耗時,而Thread Time指的是CPU耗時。我們平時說的最佳化更多的是最佳化CPU時間,當有IO操作時,用Thread Time來分析耗時更合理

此外,使用TraceView需要關注TraceView的執行時開銷,因為它自身耗時較長,就有可能會帶偏我們的最佳化方向。

3.2、Systrace

Systrace結合Android核心的資料,生成HTML報告

systrace在Android/sdk/platform-tools/systrace目錄中。使用前需要安裝python,因為systrace是用python生成html

報告的,命令如下:

python systrace.py -b 32768 -t 10 -a 包名 -o perference.html sched gfx view wm am app

具體引數可參考:developer.android.google.cn/studio/prof…

執行命令後,開啟報告,顯示如下

要使用chrome瀏覽器開啟,否則可能會顯示白屏。如果使用chrome也顯示白屏,可在chrome瀏覽器中輸入chrome:tracing, 再Load檔案進去顯示

檢視圖時,A鍵是左移,D鍵右移, S鍵縮小,W鍵放大

4、常用最佳化4.1、啟動載入常見最佳化策略

一個應用越大,涉及模組越多,包含的服務甚至程序就會越多,如網路模組的初始化,底層資料初始化等,這些載入都需要提前準備好,有些不必要的就不要放到應用中。通常可以從以下四個維度整理啟動的各個點:

1、必要且耗時:啟動初始化,考慮用執行緒來初始化

2、必要不耗時:不用處理

3、非必要耗時,資料上報、外掛初始化,按需處理

4、非必要不耗時:直接去掉,有需要的時候再載入

將應用啟動時要執行的內容按上述分類,按需實現載入邏輯。那麼常見的最佳化載入策略有哪些呢?

非同步載入:耗時多的載入放到子執行緒中非同步執行

延遲載入: 非必須的資料延遲載入

提前載入:利用ContentProvider提前進行初始化

下面分別介紹非同步載入和延遲載入的一些常用處理

4.2、非同步載入

非同步載入,簡單來說,就是使用子執行緒非同步載入。在實際場景中,啟動時常常需要對各種第三方庫做初始化操作。透過將初始化放到子執行緒中進行,可以大大加快啟動。

但是通常,有些業務邏輯是要再第三方庫的初始化後才能正常執行的,這時候如果只是簡單的放到子執行緒中跑,不做限制就很可能出現在沒初始化完成就跑業務邏輯,導致異常。

這種較為複雜的情況下,可以採用CountDownLatch處理,或者是使用啟動器的思想處理。

CountDownLatch使用

class MyApplication extends Application {    // 執行緒等待鎖    private CountDownLatch mCountDownLatch = new CountDownLatch(1);    // CPU核數    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();    // 核心執行緒數    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));    void onCreate() {		ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);        service.submit(new Runnable() {            @Override public void run() {            	//初始化weex,因為Activity載入佈局要用到需要提前初始化完成                initWeex();                mCountDownLatch.countDown();            }        });                service.submit(new Runnable() {            @Override public void run() {            	//初始化Bugly,無需關心是否在介面繪製前初始化完                initBugly();            }        });                //提交其他庫初始化,此處省略。。。				try {            //等待weex初始化完才走完onCreate            mCountDownLatch.await();		} catch (Exception e) {            e.printStackTrace();		}    }}

使用CountDownLatch在初始化的邏輯不復雜的情況下推薦使用。但如果初始化的幾個庫之間又有相互依賴,邏輯複雜的情況下,則推薦使用載入器的方式。

啟動器

啟動器的核心如下:

充分利用CPU多核能力,自動梳理並順序執行任務;程式碼Task化,將啟動任務抽象成各個task;根據所有任務依賴關係排序生成一個有向無環圖;多執行緒按照執行緒優先順序順序執行

具體實現可參考:https://github.com/NoEndToLF/AppStartFaster

4.3、延遲載入

有些第三方庫的初始化其實優先順序並不高,可以按需載入。或者是利用IdleHandler在主執行緒空閒的時候進行分批初始化。

按需載入可根據具體情況實現,這裡不做贅述。這裡介紹下使用IdleHandler的使用

    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {        @Override        public boolean queueIdle() {            //當return true時,會移除掉該IdleHandler,不再回調,當為false,則下次主執行緒空閒時會再次回撥            return false;        }    };

使用IdleHandler做分批初始化,為什麼要分批?當主執行緒空閒時,執行IdleHandler,但如果IdleHandler內容太多,則還是會導致卡頓。因此最好是將初始化操作分批在主執行緒空閒時進行

public class DelayInitDispatcher {    private Queue<Task> mDelayTasks = new LinkedList<>();    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {        @Override        public boolean queueIdle() {            //每次執行一個Task,實現分批進行            if(mDelayTasks.size()>0){                Task task = mDelayTasks.poll();                new DispatchRunnable(task).run();            }            //當為空時,返回false,移除IdleHandler            return !mDelayTasks.isEmpty();        }    };	    //新增初始化任務    public DelayInitDispatcher addTask(Task task){        mDelayTasks.add(task);        return this;    }	    //給主執行緒新增IdleHandler    public void start(){        Looper.myQueue().addIdleHandler(mIdleHandler);    }}
4.4、提前載入

上述方案中初始化最快的時機都是在Application的onCreate中進行,但還有更早的方式。ContentProvider的onCreate是在Application的attachBaseContext和onCreate方法中間進行的。也就是說它比Application的onCreate方法更早執行。所以可以利用這點來對第三方庫的初始化進行提前載入。

androidx-startup使用

如何使用:第一步,寫一個類實現Initializer,泛型為返回的例項,如果不需要的話,就寫Unitclass TimberInitializer : Initializer<Unit> {	    //這裡寫初始化執行的內容,並返回初始化例項    override fun create(context: Context) {        if (BuildConfig.DEBUG) {            Timber.plant(Timber.DebugTree())            Timber.d("TimberInitializer is initialized.")        }    }    //這裡寫初始化的東西依賴的另外的初始化器,沒有的時候返回空List    override fun dependencies(): List<Class<out Initializer<*>>> {        return emptyList()    }}第二步,在AndroidManifest中宣告provider,並配置meta-data寫初始化的類<provider    android:name="androidx.startup.InitializationProvider"    android:authorities="com.test.pokedex.androidx-startup"    android:exported=“false"    //這裡寫merge是因為其他模組可能也有同樣的provider宣告,做合併操作    tools:node="merge">    //當有相互依賴的情況下,寫頂層的初始化器就可以,其依賴的會自動搜尋到    <meta-data        android:name="com.test.pokedex.initializer.TimberInitializer"        android:value="androidx.startup" /></provider>
4.5、其他最佳化

1、在應用中,增加啟動預設圖或者自定義一個Theme,在Activity首先使用一個預設的介面解決部分啟動短暫黑白屏問題。如android:theme="@style/Theme.AppStartLoad"

5、小結

1、冷啟動、溫啟動、熱啟動主要進行的處理及他們的區別

2、如何獲取啟動時間,介紹了使用adb命令和程式碼打點這兩種方式

3、如何使用工具找到程式中耗時較長的程式碼,介紹TraceView和Systrace的使用

4、常見的啟動最佳化方式及實現介紹(非同步載入,延遲載入,提前載入等),關鍵思想:非同步、延遲、懶載入

最後

在這裡我也分享一份由幾位大佬一起收錄整理的Flutter進階資料以及Android學習PDF+架構影片+面試文件+原始碼筆記高階架構技術進階腦圖、Android開發面試專題資料,高階進階架構資料。

這些都是我閒暇時還會反覆翻閱的精品資料。可以有效地幫助大家掌握知識、理解原理。當然你也可以拿去查漏補缺,提升自身的競爭力。

5
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 大白話詳解5種網路IO模型