應用的啟動速度對一個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、TraceViewTraceview是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 Time和Thread Time的選項,其中Wall Clock Time的意思是方法執行的實際耗時,而Thread Time指的是CPU耗時。我們平時說的最佳化更多的是最佳化CPU時間,當有IO操作時,用Thread Time來分析耗時更合理
此外,使用TraceView需要關注TraceView的執行時開銷,因為它自身耗時較長,就有可能會帶偏我們的最佳化方向。
3.2、SystraceSystrace結合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開發面試專題資料,高階進階架構資料。
這些都是我閒暇時還會反覆翻閱的精品資料。可以有效地幫助大家掌握知識、理解原理。當然你也可以拿去查漏補缺,提升自身的競爭力。