一、概述
手機桌面點擊一個應用,用戶希望應用能 及時響應、快速加載。啓動時間過長的應用可能會令用戶失望。這種糟糕的體驗可能會導致用戶在 Play 商店針對您的應用給出很低的評分,甚至完全棄用您的應用。
本篇就來講解如何分析和優化應用的啓動時間。首先介紹啓動過程機制,然後討論如何檢測啓動時間以及分析工具,最後給出通用啓動優化方案。
二、應用啓動流程介紹
根據官方文檔,應用有三種啓動狀態:冷啓動、溫啓動、熱啓動。
冷啓動
冷啓動是指應用從頭開始啓動:系統進程在冷啓動後才創建應用進程。發生冷啓動的情況包括應用自設備啓動後或系統終止應用後首次啓動。例如,通過任務列表手動殺掉應用進程後,又重新啓動應用。熱啓動
熱啓動比冷啓動簡單得多,開銷也更低。在熱啓動中,系統的所有工作就是將您的 Activity 帶到前臺。只要應用的所有 Activity 仍駐留在內存中,應用就不必重複執行進程、應用、activity的創建。例如,按home鍵到桌面,然後又點圖標啓動應用。溫啓動
溫啓動包含了在冷啓動期間發生的部分操作;同時,它的開銷要比熱啓動高。有許多潛在狀態可視爲溫啓動。例如:用戶按返回鍵退出應用後又重新啓動應用。這時進程已在運行,但應用必須通過調用 onCreate() 從頭開始重新創建 Activity。
啓動優化是在 冷啓動 的基礎上進行優化。要優化應用以實現快速啓動,瞭解系統和應用層面的情況以及它們在各個狀態中的互動方式很有幫助。
在冷啓動開始時,系統有三個任務,它們是:
- 加載並啓動應用。
- 在啓動後立即顯示應用的空白啓動窗口。
- 創建應用進程。
系統一創建應用進程,應用進程就負責後續階段:
- 啓動主線程。
- 創建應用對象。
- 創建主 Activity。
- 加載視圖。
- 執行初始繪製。
一旦應用進程完成第一次繪製,系統進程就會換掉當前顯示的後臺窗口(StartingWindow),替換爲主 Activity。此時,用戶可以開始使用應用。
詳細完整的啓動流程分析參考我的文章《Activity的啓動過程詳解(基於10.0源碼)》,這篇從源碼角度介紹了 從點擊應用圖標開始 到添加window後可見 的完整流程。建議閱讀理解後再繼續此篇啓動優化的學習。
下面是官方文檔中的啓動過程流程圖,顯示系統進程和應用進程之間如何交接工作。實際上對啓動流程的簡要概括。
三、優化核心思想
問題來了,啓動優化是對 啓動流程的那些步驟進行優化呢?
這是一個好問題。我們知道,用戶關心的是:點擊桌面圖標後 要儘快的顯示第一個頁面,並且能夠進行交互。 根據啓動流程的分析,顯示頁面能和用戶交互,這是主線程做的事情。那麼就要求 我們不能再主線程做耗時的操作。啓動中的系統任務我們無法干預,能干預的就是在創建應用和創建 Activity 的過程中可能會出現的性能問題。這一過程具體就是:
- Application的attachBaseContext
- Application的onCreate
- activity的onCreate
- activity的onStart
- activity的onResume
activity的onResume方法完成後纔開始首幀的繪製。所以這些方法中的耗時操作我們是要極力避免的。
並且,通常情況下,一個應用的主頁的數據是需要進行網絡請求的,那麼用戶啓動應用是希望快速進入主頁以及看到主頁數據,這也是我們計算啓動結束時間的一個依據。
四、時間檢測
4.1 Displayed
在 Android 4.4(API 級別 19)及更高版本中,logcat 包含一個輸出行,其中包含名爲 “Displayed” 的值。此值代表從啓動進程到在屏幕上完成對應 Activity 的繪製所用的時間。經過的時間包括以下事件序列:
- 啓動進程。
- 初始化對象。
- 創建並初始化 Activity。
- 擴充佈局。
- 首次繪製。
這是我的demo app 啓動的日誌打印,查看
2020-07-13 19:54:38.256 18137-18137/com.hfy.androidlearning I/hfy: onResume begin.
2020-07-13 19:54:38.257 18137-18137/com.hfy.androidlearning I/hfy: onResume end.
2020-07-13 19:54:38.269 1797-16782/? I/WindowManager: addWindow: Window{1402051 u0 com.hfy.androidlearning/com.hfy.demo01.MainActivity}
2020-07-13 19:54:38.391 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s251ms
可見“Displayed”的時間打印是在添加window之後,而添加window是在onResume方法之後。
4.2 adb shell
也可以使用adb命令運行應用來測量初步顯示所用時間:
adb shell am start -W [ApplicationId]/[根Activity的全路徑]
當ApplicationId和package相同時,根Activity全路徑可以省略前面的packageName。
Displayed 指標和前面一樣出現在 logcat 輸出中:
2020-07-14 14:53:05.294 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s98ms
您的終端窗口在adb命令執行後還應顯示以下內容:
hufeiyangdeMacBook-Pro:~ hufeiyang$ adb shell am start -W com.hfy.androidlearning/com.hfy.demo01.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.hfy.androidlearning/com.hfy.demo01.MainActivity }
Status: ok
LaunchState: COLD
Activity: com.hfy.androidlearning/com.hfy.demo01.MainActivity
TotalTime: 2098
WaitTime: 2100
Complete
我們關注TotalTime即可,即應用的啓動時間,包括 創建進程 + Application初始化 + Activity初始化到界面顯示 的過程。
4.3 reportFullyDrawn()
可以使用 reportFullyDrawn() (API19及以上)方法測量從應用啓動到完全顯示所有資源和視圖層次結構所用的時間。什麼意思呢?前面核心思想中提到,主頁數據請求後完全呈現界面的過程也是一個優化點,而前面的“Displayed”、:“TotalTime”的時間統計都是啓動到首幀繪製,那麼如何獲取 從 啓動 到 獲取網絡請求後再次完成刷新 的時間呢?
要解決此問題,您可以手動調用Activity的 reportFullyDrawn()方法,讓系統知道您的 Activity 已完成延遲加載。當您使用此方法時,logcat 顯示的值爲從創建應用對象到調用 reportFullyDrawn() 時所用的時間。使用示例如下:
@Override
protected void onResume() {
super.onResume();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
reportFullyDrawn();
}
}
});
}
}).start();
}
使用子線程睡1秒來模擬數據加載,然後調用reportFullyDrawn(),以下是 logcat 的輸出。
2020-07-14 15:26:00.979 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s133ms
2020-07-14 15:26:01.788 1797-2017/? I/ActivityTaskManager: Fully drawn com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s943ms
4.4 代碼打點
寫一個打點工具類,開始結束時分別記錄,把時間上報到服務器。
此方法可帶到線上,但代碼有侵入性。
開始記錄的位置放在 Application 的 attachBaseContext 方法中,attachBaseContext 是我們應用能接收到的最早的一個生命週期回調方法。
計算啓動結束時間的兩種方式
一種是在 onWindowFocusChanged 方法中計算啓動耗時。
onWindowFocusChanged 方法只是 Activity 的首幀時間,是 Activity 首次進行繪製的時間,首幀時間和界面完整展示出來還有一段時間差,不能真正代表界面已經展現出來了。按首幀時間計算啓動耗時並不準確,我們要的是用戶真正看到我們界面的時間。
正確的計算啓動耗時的時機是要等真實的數據展示出來,比如在列表第一項的展示時再計算啓動耗時。 (在 Adapter 中記錄啓動耗時要加一個布爾值變量進行判斷,避免 onBindViewHolder 方法被多次調用導致不必要的計算。)
//第一個item 且沒有記錄過,就結束打點
if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
mHasRecorded = true;
helper.getView(R.id.xxx).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
helper.getView(R.id.xxx).getViewTreeObserver().removeOnPreDrawListener(this);
LogHelper.i("結束打點!");
return true;
}
});
}
4.5 AOP(Aspect Oriented Programming) 打點
面向切面編程,可以使用AspectJ。例如可以切Application的onCreate方法來計算其耗時。
特點是是對代碼無侵入性、可帶到線上。因爲具體使用不再展開,詳細使用可參考這篇文章《深入探索編譯插樁技術(二、AspectJ)》
五、分析工具介紹
分析方法耗時的工具: Systrace 、 Traceview,兩個是相互補充的關係,我們要在不同的場景下使用不同的工具,這樣才能發揮工具的最大作用。
5.1 Traceview
Traceview 能以圖形的形式展示代碼的執行時間和調用棧信息,而且 Traceview 提供的信息非常全面,因爲它包含了所有線程。
Traceview 的使用可以分爲兩步:開始跟蹤、分析結果。我們來看看具體操作。
通過 Debug.startMethodTracing(tracepath) 開始跟蹤方法,記錄一段時間內的 CPU 使用情況。調用 Debug.stopMethodTracing() 停止跟蹤方法,然後系統就會爲我們生成一個.trace文件,我們可以通過 Traceview 查看這個文件記錄的內容。
文件生成的位置默認在 Android/data/包名/files 下,下面來看一個例子。
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate begin. ");
super.onCreate(savedInstanceState);
//默認生成路徑:Android/data/包名/files/dmtrace.trace
Debug.startMethodTracing();
//也可以自定義路徑
//Debug.startMethodTracing(getExternalFilesDir(null)+"test.trace");
setContentView(R.layout.activity_main);
Intent intent = getIntent();
String name = intent.getStringExtra("name");
Log.i(TAG, "onCreate: name = " + name);
initConfig();
initView();
initData();
...
Debug.stopMethodTracing();
}
在MainActivity的onCreate前後方法中分別調用開始停止記錄方法,運行打開應用進入首頁後,我們定位到 /sdcard/android/data/包名/files/ 目錄下查看文件管理器確實是有.trace文件:
然後雙擊打開:
以圖形來呈現方法跟蹤數據或函數跟蹤數據,其中調用的時間段和時間在橫軸上表示,而其被調用方則在縱軸上顯示。 所以我們可以看到具體的方法及其耗時。
詳細介紹參考官方文檔 《使用 CPU Profiler 檢查 CPU 活動》。
可以看到在onCreate方法中,最耗時的是testHandler方法,它裏面睡了一覺。
5.2 Systrace
Systrace 結合了 Android 內核數據,分析了線程活動後會給我們生成一個非常精確 HTML 格式的報告。
Systrace原理:在系統的一些關鍵鏈路(如SystemServcie、虛擬機、Binder驅動)插入一些信息(Label)。然後,通過Label的開始和結束來確定某個核心過程的執行時間,並把這些Label信息收集起來得到系統關鍵路徑的運行時間信息,最後得到整個系統的運行性能信息。其中,Android Framework 裏面一些重要的模塊都插入了label信息,用戶App中也可以添加自定義的Lable。
Systrace 提供的 Trace 工具類默認只能 API 18 以上的項目中才能使用,如果我們的兼容版本低於 API 18,我們可以使用 TraceCompat。
Systrace 的使用步驟和 Traceview 差不多,分爲下面兩步。
- 調用跟蹤方法
- 查看跟蹤結果
來看示例,在onCreate前後分別使用TraceCompat.beginSection、TraceCompat.endSection方法:
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate begin. ");
super.onCreate(savedInstanceState);
TraceCompat.beginSection("MainActivity onCreate");
Debug.startMethodTracing();//dmtrace.trace
// Debug.startMethodTracing(getExternalFilesDir(null)+"test.trace");
setContentView(R.layout.activity_main);
initConfig();
initView();
initData();
Debug.stopMethodTracing();
TraceCompat.endSection();
}
運行app後,手動殺掉。然後cd 到SDK 目錄下的 platform-tools/systrace 下,使用命令:
python systrace.py -t 10 -o /Users/hufeiyang/trace.html -a com.hfy.androidlearning
其中:-t 10是指跟蹤10秒,-o 表示把文件輸出到指定目錄下,-a 是指定應用包名。
輸入完這行命令後,可以看到開始跟蹤的提示。看到 “Starting tracing ”後,手動打開我們的應用。
示例如下:
hufeiyangdeMacBook-Pro:~ hufeiyang$ cd /Users/hufeiyang/Library/Android/sdk/platform-tools/systrace
hufeiyangdeMacBook-Pro:systrace hufeiyang$ python systrace.py -t 10 -o /Users/hufeiyang/trace.html -a com.hfy.androidlearning
Starting tracing (10 seconds)
Tracing completed. Collecting output...
Outputting Systrace results...
Tracing complete, writing results
Wrote trace HTML file: file:///Users/hufeiyang/trace.html
跟蹤10秒,然後就在指定目錄生成了html文件,我們打開看看:
這裏我們同樣可以看到具體的耗時,以及每一幀渲染耗費的時間。具體參考官方文檔《Systrace 概覽》
小結
Traceview 的兩個特點
- 可埋點
Traceview 的好處之一是可以在代碼中埋點,埋點後可以用 CPU Profiler 進行分析。
因爲我們現在優化的是啓動階段的代碼,如果我們打開 App 後直接通過 CPU Profiler 進行記錄的話,就要求你有單身三十年的手速,點擊開始記錄的時間要和應用的啓動時間完全一致。
有了 Traceview,哪怕你是老年人手速也可以記錄啓動過程涉及的調用棧信息。- 開銷大
Traceview 的運行時開銷非常大,它會導致我們程序的運行變慢。
之所以會變慢,是因爲它會通過虛擬機的 Profiler 抓取我們當前所有線程的所有調用堆棧。
因爲這個問題,Traceview 也可能會帶偏我們的優化方向。
比如我們有一個方法,這個方法在正常情況下的耗時不大,但是加上了 Traceview 之後可能會發現它的耗時變成了原來的十倍甚至更多。Systrace 的兩個特點
- 開銷小
Systrace 開銷非常小,不像 Traceview,因爲它只會在我們埋點區間進行記錄。
而 Traceview 是會把所有的線程的堆棧調用情況都記錄下來。- 直觀
在 Systrace 中我們可以很直觀地看到 CPU 利用率的情況。
當我們發現 CPU 利用率低的時候,我們可以考慮讓更多代碼以異步的方式執行,以提高 CPU 利用率。Traceview 與 Systrace 的兩個區別
- 查看工具
Traceview 分析結果要使用 Profiler 查看。
Systrace 分析結果是在瀏覽器查看 HTML 文件。- 埋點工具類
Traceview 使用的是 Debug.startMethodTracing()。
Systrace 用的是 Trace.beginSection() 和 TraceCompat.beginSection()。
六、啓動優化方案
優化方案有兩個方向:
- 視覺優化,啓動耗時沒有變少,但是啓動過程中給用戶更好的體驗。
- 速度優化,減少主線程的耗時,真實做到快速啓動。
6.1 視覺優化
在《Activity的啓動》中提到,在Activity啓動前會展示一個名字叫StartingWindow的window,這個window的背景是取要啓動Activity的Theme中配置的WindowBackground。
因爲啓動根activity前是需要創建進程等一系列操作,需要一定時間,而展示StartingWindow的目的是 告訴用戶你點擊是有反應的,只是在處理中,然後Activity啓動後,Activity的window就替換掉這個StartingWindow了。如果沒有這個StartingWindow,那麼點擊後就會一段時間沒有反應,給用戶誤解。
而這,就是應用啓動開始時 會展示白屏的原因了。
那麼視覺優化的方案 也就有了:替換第一個activity(通常是閃屏頁)的Theme,把白色背景換成Logot圖,然後再Activity的onCreate中換回來。 這樣啓動時看到的就是你配置的logo圖了。
具體操作一下:
<activity android:name=".MainActivity" android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
這裏我的而第一個activity是MainActivity,配置了theme是R.style.SplashTheme,來看下:
<style name="SplashTheme" parent="AppNoActionBarAlphaAnimTheme">
<item name="android:windowBackground">@drawable/splash_background</item>
</style>
看到 android:windowBackground已經配置成了自定義的drawable,這個就是關鍵點了,而默認是windowBackground是白色。看看自定義的drawable:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque">
<!--兩層-->
<item android:drawable="@android:color/white"/>
<item>
<bitmap
android:src="@drawable/dog"
android:gravity="center"/>
</item>
</layer-list>
drawable的根節點是<layer-list>,然後一層是白色底,一層就是我們的logo圖片了。
最後,在activity的onCreate中把Theme換回R.style.AppTheme即可(要在super.onCreate之前)。
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
}
效果如下:
可以看到,確實視覺上體驗比白屏好很多。
但實際上啓動速度並沒有變快,下面就來看看可以真實提高啓動速度的方案有哪些。
6.2 異步初始化
前面提到 提高啓動速度,核心思想就是 減少主線程的耗時操作。啓動過程中 可控住耗時的主線程 主要是Application的onCreate方法、Activity的onCreate、onStart、onResume方法。
通常我們會在Application的onCreate方法中進行較多的初始化操作,例如第三方庫初始化,那麼這一過程是就需要重點關注。
減少主線程耗時的方法,又可細分爲異步初始化、延遲初始化,即把 主線程任務 放到子線程執行 或 延後執行。 下面就先來看看異步初始化是如何實現的。
執行異步請求,一般是使用線程池,例如:
Runnable initTask = new Runnable() {
@Override
public void run() {
//init task
}
};
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadCount);
fixedThreadPool.execute(initTask);
但是通過線程池處理初始化任務的方式存在三個問題:
- 代碼不夠優雅
假如我們有 100 個初始化任務,那像上面這樣的代碼就要寫 100 遍,提交 100 次任務。- 無法限制在 onCreate 中完成
有的第三方庫的初始化任務需要在 Application 的 onCreate 方法中執行完成,雖然可以用 CountDownLatch 實現等待,但是還是有點繁瑣。- 無法實現存在依賴關係
有的初始化任務之間存在依賴關係,比如極光推送需要設備 ID,而 initDeviceId() 這個方法也是一個初始化任務。
那麼解決方案是啥?啓動器!
LauncherStarter,即啓動器,是針對這三個問題的解決方案,結合CountDownLatch對線程池的再封裝,充分利用CPU多核,自動梳理任務順序
。
使用方式:
- 引入依賴
- 劃分任務,確認依賴和限制關係
- 添加任務,執行啓動
首先依賴引入:
implementation 'com.github.zeshaoaaa:LaunchStarter:0.0.1'
然後把初始化任務劃分成一個個任務;釐清依賴關係,例如任務2要依賴任務1完成後才能開始;還有例如3任務需要在onCreate方法結束前完成;任務4要在主線程執行。
然後添加這些任務,開始任務,設置等待。
具體使用也比較簡單,代碼如下:
public class MyApplication extends Application {
private static final String TAG = "MyApplication";
@Override
public void onCreate() {
super.onCreate();
TaskDispatcher.init(getBaseContext());
TaskDispatcher taskDispatcher = TaskDispatcher.createInstance();
// task2依賴task1;
// task3未完成時taskDispatcher.await()處需要等待;
// test4在主線程執行
//每個任務都耗時一秒
Task1 task1 = new Task1();
Task2 task2 = new Task2();
Task3 task3 = new Task3();
Task4 task4Main = new Task4();
taskDispatcher.addTask(task1);
taskDispatcher.addTask(task2);
taskDispatcher.addTask(task3);
taskDispatcher.addTask(task4Main);
Log.i(TAG, "onCreate: taskDispatcher.start()");
taskDispatcher.start();//開始
taskDispatcher.await();//等task3完成後纔會往下走
Log.i(TAG, "onCreate: end.");
}
private static class Task1 extends Task {
@Override
public void run() {
Log.i(TAG, Thread.currentThread().getName()+" run start: task1");
doTask();
Log.i(TAG, Thread.currentThread().getName()+" run end: task1");
}
}
private static class Task2 extends Task {
@Override
public void run() {
Log.i(TAG, Thread.currentThread().getName()+" run start: task2");
doTask();
Log.i(TAG, Thread.currentThread().getName()+" run end: task2");
}
@Override
public List<Class<? extends Task>> dependsOn() {
//依賴task1,等task1執行完再執行
ArrayList<Class<? extends Task>> classes = new ArrayList<>();
classes.add(Task1.class);
return classes;
}
}
private static class Task3 extends Task {
@Override
public void run() {
Log.i(TAG, Thread.currentThread().getName()+" run start: task3");
doTask();
Log.i(TAG, Thread.currentThread().getName()+" run end: task3");
}
@Override
public boolean needWait() {
//task3未完成時,在taskDispatcher.await()處需要等待。這裏就是保證在onCreate結束前完成。
return true;
}
}
private static class Task4 extends MainTask {
//繼承自MainTask,即保證在主線程執行
@Override
public void run() {
Log.i(TAG, Thread.currentThread().getName()+" run start: task4");
doTask();
Log.i(TAG, Thread.currentThread().getName()+" run end: task4");
}
}
private static void doTask() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
有4個初始化任務,都耗時1秒,若都在主線程執行,那麼會耗時4秒。這裏使用啓動器執行,並且保證了上面描述的任務要求限制。執行完成後日誌如下:
2020-07-17 12:06:20.648 26324-26324/com.hfy.androidlearning I/MyApplication: onCreate: taskDispatcher.start()
2020-07-17 12:06:20.650 26324-26324/com.hfy.androidlearning I/MyApplication: main run start: task4
2020-07-17 12:06:20.651 26324-26427/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-1 run start: task1
2020-07-17 12:06:20.657 26324-26428/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-2 run start: task3
2020-07-17 12:06:21.689 26324-26324/com.hfy.androidlearning I/MyApplication: main run end: task4
2020-07-17 12:06:21.689 26324-26427/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-1 run end: task1
2020-07-17 12:06:21.690 26324-26429/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-3 run start: task2
2020-07-17 12:06:21.697 26324-26428/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-2 run end: task3
2020-07-17 12:06:21.697 26324-26324/com.hfy.androidlearning I/MyApplication: onCreate: end.
2020-07-17 12:06:22.729 26324-26429/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-3 run end: task2
可見主線程耗時只有1秒。 另外,要注意的是,task3、task4一定是在onCreate內完成了,task1、task2都可能是在onCreate結束後一段時間才完成,所以在Activity中就不能使用task1、task2相關的庫了。那麼 在劃分任務,確認依賴和限制關係時就要注意了。
異步初始化就說這麼多,原理部分可直接閱讀源碼,很容易理解。接着看延遲初始化。
6.3 延遲初始化
在 Application 和 Activity 中可能存在優先級不高的初始化任務,可以考慮把這些任務進行 延遲初始化。延遲初始化並不是減少了主線程耗時,而是讓耗時操作讓位、讓資源給UI繪製,將耗時的操作延遲到UI加載完畢後。
那麼問題來了,如何延遲呢?
- 使用new Handler().postDelay()方法、或者view.postDelay()——但是延遲時間不好把握,不知道啥時候UI加載完畢。
- 使用View.getViewTreeObserver().addOnPreDrawListener()監聽——可以保證view繪製完成,但是此時發生交互呢,例如用戶在滑動列表,那麼就會造成卡頓了。
那麼解決方案是啥?延遲啓動器!
延遲啓動器,利用IdleHandler特性,在CPU空閒時執行,對延遲任務進行分批初始化, 這樣 執行時機明確、也緩解界面UI卡頓。 延遲啓動器就是上面的LauncherStarter中的一個類。
public class DelayInitDispatcher {
private Queue<Task> mDelayTasks = new LinkedList<>();
private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
if(mDelayTasks.size()>0){
Task task = mDelayTasks.poll();
new DispatchRunnable(task).run();
}
return !mDelayTasks.isEmpty();
}
};
public DelayInitDispatcher addTask(Task task){
mDelayTasks.add(task);
return this;
}
public void start(){
Looper.myQueue().addIdleHandler(mIdleHandler);
}
}
使用也很簡單,例如在閃屏頁中添加任務開始即可:
//SpalshActivity
DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
protected void onCreate(Bundle savedInstanceState) {
delayInitDispatcher.addTask(new Task() {
@Override
public void run() {
Log.i(TAG, "run: delay task begin");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i(TAG, "run: delay task end");
}
});
delayInitDispatcher.start();
}
經測試,確實是在是在佈局展示後開始任務。但是如果耗時較長(例子中是3秒),過程中滑動屏幕,是不能及時響應的,會感覺到明顯的卡頓。
所以,能異步的task優先使用異步啓動器在Application的onCreate方法中加載,對於不能異步且耗時較少的task,我們可以利用延遲啓動器進行加載。如果任務可以到用時再加載,可以使用懶加載的方式。
IdleHandler原理分析:
//MessageQueue.java
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}
從消息隊列取消息時,如果沒有取到消息,就執行 空閒IdleHandler,執行完就remove。
6.4 Multidex預加載優化
安裝或者升級後 首次 MultiDex 花費的時間過於漫長,我們需要進行Multidex的預加載優化。
5.0以上默認使用ART,在安裝時已將Class.dex轉換爲oat文件了,無需優化,所以應判斷只有在主進程及SDK 5.0以下才進行Multidex的預加載
抖音BoostMultiDex優化實踐:
抖音BoostMultiDex優化實踐:Android低版本上APP首次啓動時間減少80%(一)
快速接入:
- build.gradle的dependencies中添加依賴:
dependencies {
// For specific version number, please refer to app demo
implementation 'com.bytedance.boost_multidex:boost_multidex:1.0.1'
}
- 與官方MultiDex類似,在Application.attachBaseContext的最前面進行初始化即可:
public class YourApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
BoostMultiDex.install(base);
}
6.5 頁面數據預加載
閃屏頁、首頁的數據預加載:閃屏廣告、首頁數據 加載後緩存到本地,下次進入時直接讀取緩存。 首頁讀取緩存到內存的操作還可以提前到閃屏頁。
6.6 頁面繪製優化
閃屏頁與主頁的繪製優化,這裏涉及到繪製優化相關知識了,例如減少佈局層級等。
七、總結
我們先介紹了啓動流程、優化思想、耗時檢測、分析工具,然後給出了常用優化方案:異步初始化、延遲初始化。涉及了很多新知識和工具,一些地方文章中沒有展開,可以參考給出的連接詳細學習。畢竟性能優化是多樣技術知識的綜合使用,需要系統掌握對應工作流程被、分析工具、解決方案,才能對性能進行深層次的優化。
好了,今天就到這裏,歡迎留言討論~
參考與感謝:
.
好了,今天就到這裏,歡迎留言討論~
你的 點贊、評論、收藏、轉發,是對我的巨大鼓勵!
歡迎關注我的公衆號