1. 前言
首先說(shuō)明一下應(yīng)用的幾種啟動(dòng)方式
- 冷啟動(dòng):系統(tǒng)不存在此 APP 的進(jìn)程够挂,此時(shí)需要重新創(chuàng)建進(jìn)程、Application俭缓、Activity等刑赶,然后是 measure禁荒、layout、draw 過(guò)程
- 溫啟動(dòng):用戶按 HOME 鍵后角撞,如果 Activity 沒(méi)有被回收呛伴,啟動(dòng)應(yīng)用也只是喚醒到前臺(tái),不需要走初始化流程
- 熱啟動(dòng):系統(tǒng)存在此 APP 的進(jìn)程谒所,比如用戶按 Back 鍵热康,或者按 Home鍵后 Activity 被回收了,此時(shí)由于進(jìn)程存在劣领,所以不會(huì)初始化 Application姐军,只需要?jiǎng)?chuàng)建 Activity 并 measure、layout尖淘、draw奕锌。
最近有個(gè)需求需要統(tǒng)計(jì)App的啟動(dòng)時(shí)間,在查閱了一些資料后總結(jié)有如下三種方案
- 通過(guò) adb 的 am 命令獲取
- 通過(guò) adb 的 logcat 命令獲取
- 通過(guò)在Application和業(yè)務(wù)的第一個(gè)Activity埋點(diǎn)進(jìn)行統(tǒng)計(jì)
2. 通過(guò) adb 的 am 命令獲取
網(wǎng)上大部分都是這種方案村生,可以通過(guò) adb 的命令
adb shell am start -W com.gtr.sdkdemo/com.gtr.test.MainActivity
這個(gè)命令的輸出日志如下:
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
幾個(gè)時(shí)間參數(shù)的講解:
- WaitTime 返回從 startActivity 到應(yīng)用第一幀完全顯示這段時(shí)間. 就是總的耗時(shí)惊暴,包括前一個(gè)應(yīng)用 Activity pause 的時(shí)間和新應(yīng)用啟動(dòng)的時(shí)間;
- ThisTime 表示一連串啟動(dòng) Activity 的最后一個(gè) Activity 的啟動(dòng)耗時(shí)趁桃;
- TotalTime 表示新應(yīng)用啟動(dòng)的耗時(shí)辽话,包括新進(jìn)程的啟動(dòng)和 Activity 的啟動(dòng),但不包括前一個(gè)應(yīng)用Activity pause的耗時(shí)卫病。
所以只關(guān)心 TotalTime 參數(shù)就可以了油啤。但是問(wèn)題來(lái)了:
- 首先這個(gè)是shell命令,能不能通過(guò) Runtime 來(lái)進(jìn)行調(diào)用這個(gè)命令我沒(méi)試過(guò)蟀苛,極有可能是不行的
- 這個(gè)命令需要新起一個(gè)Activity來(lái)統(tǒng)計(jì)益咬,在已運(yùn)行應(yīng)用中肯定不可能新起Activity來(lái)統(tǒng)計(jì),因?yàn)樾缕鹂隙ㄊ菬釂?dòng)的帜平,啟動(dòng)時(shí)間不準(zhǔn)
因此幽告,這個(gè)方案被無(wú)情拋棄
3. 通過(guò) adb 的 logcat 命令 獲取
在一次無(wú)意的瀏覽 StackOverFlow 過(guò)程中,看到有個(gè)大牛給了一個(gè)提示:應(yīng)用啟用時(shí)罕模,會(huì)輸出一行日志
adb logcat -s ActivityManager:I | grep Displayed
我試了一下评腺,果不其然:
I/ActivityManager( 949): Displayed com.gtr.sdkdemo/com.gtr.test.MainActivity: +303ms (total +1s546ms)
再啟動(dòng)一次,發(fā)現(xiàn)還會(huì)出來(lái)一條
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
也就是說(shuō)淑掌,只需要讀取最后一條數(shù)據(jù)就OK了蒿讥。激動(dòng)的我馬上封裝了一個(gè)異步獲取類來(lái)試了一下,源代碼如下
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();
}
}
}
}
這個(gè)類提供了延時(shí)異步獲取的方式抛腕,因?yàn)?logcat 命令是會(huì)阻塞 shell 進(jìn)程的芋绸,如果在主線程直接讀取的話,會(huì)造成主線程阻塞担敌。
在設(shè)計(jì)這個(gè)類的時(shí)候摔敛,也踩了一些坑,在這里稍微總結(jié)一下以防以后忘了
- Runtime 的 exec 方法只是執(zhí)行 shell 語(yǔ)句全封,并不是 shell 解釋器马昙,因此一些管道符桃犬、重定向符是不生效的。也就是說(shuō) |grep 我無(wú)效的行楞,只能手動(dòng)在程序中判斷
- android 用 exec 執(zhí)行 logcat 中會(huì)自動(dòng)過(guò)濾掉非本應(yīng)用的一些日志攒暇,可能是出于安全著想
- exec(String) 和 exec(String[]) 是等效的,最終都會(huì)將 String 參數(shù)轉(zhuǎn)換為 String[] 參數(shù)
- Progress 的 waitFor 方法會(huì)阻塞當(dāng)前線程子房,直到 shell 子進(jìn)程結(jié)束形用,如果 shell 子進(jìn)程一直不結(jié)束,則會(huì)造成死鎖证杭。
- BufferedReader 的 readLine 和 close 方法都會(huì)阻塞田度。起初我是用 BufferedReader 的 readLine 在子線程讀取數(shù)據(jù)的,但是在主線程調(diào)用 close 方法來(lái)解除 子線程的阻塞狀態(tài)時(shí)發(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;
}
}
}
這樣做就非常危險(xiǎn)了,也就是說(shuō) BufferedReader 除非讀到末尾的 '\n' 字符琢歇,否則是不能主動(dòng)結(jié)束其阻塞狀態(tài)的兰怠。而且主線程想結(jié)束子線程的阻塞狀態(tài)調(diào)用 close 方法還可能把主線程給阻塞了。
后來(lái)我才用 DataInputStream 來(lái)替換的李茫,因?yàn)樗?close 方法沒(méi)有加鎖揭保,不會(huì)被阻塞,并且可以解除子線程的阻塞狀態(tài)
public void close() throws IOException {
in.close();
}
總結(jié)的采坑就到這里差不多了魄宏,運(yùn)行結(jié)果發(fā)現(xiàn)沒(méi)有任何啟動(dòng)時(shí)間數(shù)據(jù)秸侣。原因正如上面說(shuō)的第二點(diǎn),android 用 Runtime 獲取的 logcat 日志信息宠互,屏蔽了非本應(yīng)用的日志味榛,而啟動(dòng)時(shí)間的日志是屬于系統(tǒng)的,所以獲取不到予跌。
4. 通過(guò)在Application和業(yè)務(wù)的第一個(gè)Activity埋點(diǎn)進(jìn)行統(tǒng)計(jì)
上面的兩種方案都以失敗告終搏色,沒(méi)辦法在跟老大溝通后只有犧牲數(shù)據(jù)準(zhǔn)確性了。
- 首先在 Application 的 attachBaseContext 方法記錄開(kāi)始時(shí)間
- 在業(yè)務(wù)的第一個(gè) Activity 的 onWindowFocusChanged 方法記錄結(jié)束時(shí)間
這里解釋一下為什么是 onWindowFocusChanged 而不是 onResume 等其他生命周期券册。因?yàn)?onResume 只是 Activity 的一次步驟频轿,此時(shí)控件只是被 measure 了,但是并沒(méi)有 draw烁焙, 因此此時(shí)并不能被用戶所見(jiàn)航邢,而為了統(tǒng)計(jì)數(shù)據(jù)的準(zhǔn)確性,以用戶所見(jiàn)作為結(jié)束時(shí)間更為恰當(dāng)骄蝇。
然后解釋一下這種方式的優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):可以自由選擇哪一個(gè) Activity 作為業(yè)務(wù)上的“首頁(yè)”膳殷,比如把主頁(yè)作為首頁(yè)而不是啟動(dòng)頁(yè)
- 缺點(diǎn):從用戶點(diǎn)擊 app icon 到 Application 被創(chuàng)建,中間還是有很多步驟的九火,比如冷啟動(dòng)的進(jìn)程創(chuàng)建過(guò)程赚窃,而這個(gè)時(shí)間用此版本是沒(méi)辦法統(tǒng)計(jì)了册招,必須得承受這點(diǎn)數(shù)據(jù)的不準(zhǔn)確性。