Android:獲取APP啟動(dòng)時(shí)間的踩坑經(jīng)歷

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é)有如下三種方案

  1. 通過(guò) adb 的 am 命令獲取
  2. 通過(guò) adb 的 logcat 命令獲取
  3. 通過(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é)一下以防以后忘了

  1. Runtime 的 exec 方法只是執(zhí)行 shell 語(yǔ)句全封,并不是 shell 解釋器马昙,因此一些管道符桃犬、重定向符是不生效的。也就是說(shuō) |grep 我無(wú)效的行楞,只能手動(dòng)在程序中判斷
  2. android 用 exec 執(zhí)行 logcat 中會(huì)自動(dòng)過(guò)濾掉非本應(yīng)用的一些日志攒暇,可能是出于安全著想
  3. exec(String) 和 exec(String[]) 是等效的,最終都會(huì)將 String 參數(shù)轉(zhuǎn)換為 String[] 參數(shù)
  4. Progress 的 waitFor 方法會(huì)阻塞當(dāng)前線程子房,直到 shell 子進(jìn)程結(jié)束形用,如果 shell 子進(jìn)程一直不結(jié)束,則會(huì)造成死鎖证杭。
  5. 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)確性。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末考榨,一起剝皮案震驚了整個(gè)濱河市跨细,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌河质,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件震叙,死亡現(xiàn)場(chǎng)離奇詭異掀鹅,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)媒楼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)乐尊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人划址,你說(shuō)我怎么就攤上這事扔嵌。” “怎么了夺颤?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵痢缎,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我世澜,道長(zhǎng)独旷,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任寥裂,我火速辦了婚禮嵌洼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘封恰。我一直安慰自己麻养,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布诺舔。 她就那樣靜靜地躺著鳖昌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪混萝。 梳的紋絲不亂的頭發(fā)上遗遵,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音逸嘀,去河邊找鬼车要。 笑死,一個(gè)胖子當(dāng)著我的面吹牛崭倘,可吹牛的內(nèi)容都是我干的翼岁。 我是一名探鬼主播类垫,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼琅坡!你這毒婦竟也來(lái)了悉患?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤榆俺,失蹤者是張志新(化名)和其女友劉穎售躁,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體茴晋,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡陪捷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了诺擅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片市袖。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖烁涌,靈堂內(nèi)的尸體忽然破棺而出苍碟,到底是詐尸還是另有隱情,我是刑警寧澤撮执,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布微峰,位于F島的核電站,受9級(jí)特大地震影響二打,放射性物質(zhì)發(fā)生泄漏县忌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一继效、第九天 我趴在偏房一處隱蔽的房頂上張望症杏。 院中可真熱鬧,春花似錦瑞信、人聲如沸厉颤。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)逼友。三九已至,卻和暖如春秤涩,著一層夾襖步出監(jiān)牢的瞬間帜乞,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工筐眷, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留黎烈,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像照棋,于是被迫代替她去往敵國(guó)和親资溃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 用兩張圖告訴你烈炭,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料溶锭? 從這篇文章中你...
    hw1212閱讀 12,730評(píng)論 2 59
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,162評(píng)論 25 707
  • 女兒小的時(shí)候不太喜歡穿裙子霹疫,給她買(mǎi)的沒(méi)穿幾次就不合穿了驻售。直到上幼兒園的時(shí)候。孩子開(kāi)始慢慢喜歡上裙子更米,而且會(huì)想...
    LEE婷閱讀 314評(píng)論 0 2
  • 80/20法則:世界80%的財(cái)富來(lái)至于20%的人80/20法則是由意大利經(jīng)濟(jì)家帕累托提出的,也就是大家所熟悉的帕累...
    mifia閱讀 1,677評(píng)論 0 99
  • 最近有不少朋友問(wèn)我哪幾款面膜好用栏笆?其實(shí)面膜這東西見(jiàn)仁見(jiàn)智‰款式蛉加,功效,各有特色缸逃。想要選擇合適的面膜针饥,還是得跟據(jù)自己...
    小狐貍和大臉貓閱讀 5,221評(píng)論 17 178