1. 前言
首先說明一下應(yīng)用的幾種啟動方式
- 冷啟動:系統(tǒng)不存在此 APP 的進程,此時需要重新創(chuàng)建進程、Application、Activity等,然后是 measure、layout、draw 過程
- 溫啟動:用戶按 HOME 鍵后,如果 Activity 沒有被回收,啟動應(yīng)用也只是喚醒到前臺,不需要走初始化流程
- 熱啟動:系統(tǒng)存在此 APP 的進程,比如用戶按 Back 鍵,或者按 Home鍵后 Activity 被回收了,此時由于進程存在,所以不會初始化 Application,只需要創(chuàng)建 Activity 并 measure、layout、draw。
最近有個需求需要統(tǒng)計App的啟動時間,在查閱了一些資料后總結(jié)有如下三種方案
- 通過 adb 的 am 命令獲取
- 通過 adb 的 logcat 命令獲取
- 通過在Application和業(yè)務(wù)的第一個Activity埋點進行統(tǒng)計
2. 通過 adb 的 am 命令獲取
網(wǎng)上大部分都是這種方案,可以通過 adb 的命令
adb shell am start -W com.gtr.sdkdemo/com.gtr.test.MainActivity
這個命令的輸出日志如下:
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.gtr.sdkdemo/com.gtr.test.MainActivity }
Status: ok
Activity: com.gtr.sdkdemo/com.gtr.test.MainActivity
ThisTime: 716
TotalTime: 4680
Complete
幾個時間參數(shù)的講解:
- WaitTime 返回從 startActivity 到應(yīng)用第一幀完全顯示這段時間. 就是總的耗時,包括前一個應(yīng)用 Activity pause 的時間和新應(yīng)用啟動的時間;
- ThisTime 表示一連串啟動 Activity 的最后一個 Activity 的啟動耗時;
- TotalTime 表示新應(yīng)用啟動的耗時,包括新進程的啟動和 Activity 的啟動,但不包括前一個應(yīng)用Activity pause的耗時。
所以只關(guān)心 TotalTime 參數(shù)就可以了。但是問題來了:
- 首先這個是shell命令,能不能通過 Runtime 來進行調(diào)用這個命令我沒試過,極有可能是不行的
- 這個命令需要新起一個Activity來統(tǒng)計,在已運行應(yīng)用中肯定不可能新起Activity來統(tǒng)計,因為新起肯定是熱啟動的,啟動時間不準
因此,這個方案被無情拋棄
3. 通過 adb 的 logcat 命令 獲取
在一次無意的瀏覽 StackOverFlow 過程中,看到有個大牛給了一個提示:應(yīng)用啟用時,會輸出一行日志
adb logcat -s ActivityManager:I | grep Displayed
我試了一下,果不其然:
I/ActivityManager( 949): Displayed com.gtr.sdkdemo/com.gtr.test.MainActivity: +303ms (total +1s546ms)
再啟動一次,發(fā)現(xiàn)還會出來一條
I/ActivityManager( 949): Displayed com.gtr.sdkdemo/com.gtr.test.MainActivity: +303ms (total +1s546ms)
I/ActivityManager( 949): Displayed com.gtr.sdkdemo/com.gtr.test.MainActivity: +590ms
也就是說,只需要讀取最后一條數(shù)據(jù)就OK了。激動的我馬上封裝了一個異步獲取類來試了一下,源代碼如下
public class BaseInfoManager {
private static final int WHAT_START_GET_APP_LAUNCH_TIME = 1000;
private static volatile BaseInfoManager instance;
private HandlerThread mHandlerThread;
private Handler mHandler;
private DataInputStream mReader;
// 數(shù)據(jù)相關(guān)
private List<String> appLaunchTimeList = new ArrayList<>();
private BaseInfoManager() {
}
public static BaseInfoManager getInstance() {
if (instance == null) {
synchronized (BaseInfoManager.class) {
if (instance == null) {
instance = new BaseInfoManager();
}
}
}
return instance;
}
public interface Callback<T> {
void callback(T t);
}
private void runThreadIfNeed() {
if (mHandlerThread == null) {
mHandlerThread = new HandlerThread(BaseInfoManager.class.getSimpleName());
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case WHAT_START_GET_APP_LAUNCH_TIME:
runShellForAppLaunchTime();
break;
}
}
};
}
}
private void destoryThreadIfNeed() {
if (mReader != null) {
try {
mReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
mHandler = null;
}
if (mHandlerThread != null) {
mHandlerThread.quitSafely();
mHandlerThread = null;
}
}
public synchronized void getLaunchAppTimeASync(long delayTime, final Callback<String> callback) {
runThreadIfNeed();
appLaunchTimeList.clear();
mHandler.sendEmptyMessage(WHAT_START_GET_APP_LAUNCH_TIME);
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
destoryThreadIfNeed();
// 獲取最近的一條數(shù)據(jù)
if (appLaunchTimeList.isEmpty()) {
callback.callback("");
} else {
callback.callback(appLaunchTimeList.get(appLaunchTimeList.size() - 1));
}
}
}, delayTime);
}
private void runShellForAppLaunchTime() {
Process logcatProcess = null;
try {
// adb logcat -s ActivityManager:I | grep Displayed
String cmd = "logcat -s ActivityManager:I | grep Displayed";
logcatProcess = Runtime.getRuntime().exec(cmd);
mReader = new DataInputStream(logcatProcess.getInputStream());
String line;
while ((line = mReader.readUTF()) != null) {
appLaunchTimeList.add(line);
}
} catch (IOException e){
// nothing to do
} catch (SecurityException |
IllegalArgumentException |
NullPointerException e) {
e.printStackTrace();
} finally {
if (mReader != null) {
try {
mReader.close();
} catch (IOException e) {
e.printStackTrace();
}
mReader = null;
}
if (logcatProcess != null) {
logcatProcess.destroy();
}
}
}
}
這個類提供了延時異步獲取的方式,因為 logcat 命令是會阻塞 shell 進程的,如果在主線程直接讀取的話,會造成主線程阻塞。
在設(shè)計這個類的時候,也踩了一些坑,在這里稍微總結(jié)一下以防以后忘了
- Runtime 的 exec 方法只是執(zhí)行 shell 語句,并不是 shell 解釋器,因此一些管道符、重定向符是不生效的。也就是說 |grep 我無效的,只能手動在程序中判斷
- android 用 exec 執(zhí)行 logcat 中會自動過濾掉非本應(yīng)用的一些日志,可能是出于安全著想
- exec(String) 和 exec(String[]) 是等效的,最終都會將 String 參數(shù)轉(zhuǎn)換為 String[] 參數(shù)
- Progress 的 waitFor 方法會阻塞當前線程,直到 shell 子進程結(jié)束,如果 shell 子進程一直不結(jié)束,則會造成死鎖。
- BufferedReader 的 readLine 和 close 方法都會阻塞。起初我是用 BufferedReader 的 readLine 在子線程讀取數(shù)據(jù)的,但是在主線程調(diào)用 close 方法來解除 子線程的阻塞狀態(tài)時發(fā)現(xiàn)主線程也被阻塞了。查看代碼才發(fā)現(xiàn) BufferedReader 的 close 方法加了一把鎖。
public void close() throws IOException {
synchronized (lock) {
if (in == null)
return;
try {
in.close();
} finally {
in = null;
cb = null;
}
}
}
這樣做就非常危險了,也就是說 BufferedReader 除非讀到末尾的 '\n' 字符,否則是不能主動結(jié)束其阻塞狀態(tài)的。而且主線程想結(jié)束子線程的阻塞狀態(tài)調(diào)用 close 方法還可能把主線程給阻塞了。
后來我才用 DataInputStream 來替換的,因為它的 close 方法沒有加鎖,不會被阻塞,并且可以解除子線程的阻塞狀態(tài)
public void close() throws IOException {
in.close();
}
總結(jié)的采坑就到這里差不多了,運行結(jié)果發(fā)現(xiàn)沒有任何啟動時間數(shù)據(jù)。原因正如上面說的第二點,android 用 Runtime 獲取的 logcat 日志信息,屏蔽了非本應(yīng)用的日志,而啟動時間的日志是屬于系統(tǒng)的,所以獲取不到。
4. 通過在Application和業(yè)務(wù)的第一個Activity埋點進行統(tǒng)計
上面的兩種方案都以失敗告終,沒辦法在跟老大溝通后只有犧牲數(shù)據(jù)準確性了。
- 首先在 Application 的 attachBaseContext 方法記錄開始時間
- 在業(yè)務(wù)的第一個 Activity 的 onWindowFocusChanged 方法記錄結(jié)束時間
這里解釋一下為什么是 onWindowFocusChanged 而不是 onResume 等其他生命周期。因為 onResume 只是 Activity 的一次步驟,此時控件只是被 measure 了,但是并沒有 draw, 因此此時并不能被用戶所見,而為了統(tǒng)計數(shù)據(jù)的準確性,以用戶所見作為結(jié)束時間更為恰當。
然后解釋一下這種方式的優(yōu)缺點
- 優(yōu)點:可以自由選擇哪一個 Activity 作為業(yè)務(wù)上的“首頁”,比如把主頁作為首頁而不是啟動頁
- 缺點:從用戶點擊 app icon 到 Application 被創(chuàng)建,中間還是有很多步驟的,比如冷啟動的進程創(chuàng)建過程,而這個時間用此版本是沒辦法統(tǒng)計了,必須得承受這點數(shù)據(jù)的不準確性。