安卓(Android)開發(fā)之統(tǒng)計(jì)App啟動(dòng)時(shí)間
前言
作為 Android 開發(fā)者,想必多多少少要接觸啟動(dòng)速度優(yōu)化相關(guān)的事情,當(dāng)用戶越來越多,產(chǎn)品的功能也隨著迭代越來越多,App 逐漸變得臃腫是一件很常見的現(xiàn)象,甚至可以說是不可避免的現(xiàn)象,隨之而來的工作就是優(yōu)化 App 性能,其中最主要的一項(xiàng)就是啟動(dòng)速度優(yōu)化。但本文的主角并不是啟動(dòng)速度優(yōu)化,而是啟動(dòng)時(shí)間統(tǒng)計(jì)。
一、啟動(dòng)類型
工欲善其事,必先利其器。想要優(yōu)化 App 的啟動(dòng)速度,必須有準(zhǔn)確衡量啟動(dòng)時(shí)間的方法,否則優(yōu)化完之后效果怎樣,自己都不知道,說出去別人也不信服不是。在做 App 啟動(dòng)時(shí)間統(tǒng)計(jì)之前,當(dāng)然必須弄明白有哪些啟動(dòng)類型,每種啟動(dòng)類型的特點(diǎn)。通常來說,在安卓中應(yīng)用的啟動(dòng)方式分為以下幾種:
1、冷啟動(dòng):當(dāng)啟動(dòng)應(yīng)用時(shí),后臺(tái)沒有該應(yīng)用的進(jìn)程,這時(shí)系統(tǒng)會(huì)重新創(chuàng)建一個(gè)新的進(jìn)程分配給該應(yīng)用,這個(gè)啟動(dòng)方式就是冷啟動(dòng)。冷啟動(dòng)因?yàn)橄到y(tǒng)會(huì)重新創(chuàng)建一個(gè)新的進(jìn)程分配給它,所以會(huì)先創(chuàng)建和初始化 Application 類,再創(chuàng)建和初始化 MainActivity 類,最后顯示在界面上。
2、熱啟動(dòng):當(dāng)啟動(dòng)應(yīng)用時(shí),后臺(tái)已有該應(yīng)用的進(jìn)程(例:按back鍵、home鍵,應(yīng)用雖然會(huì)退出,但是該應(yīng)用的進(jìn)程是依然會(huì)保留在后臺(tái),可進(jìn)入任務(wù)列表查看),所以在已有進(jìn)程的情況下,這種啟動(dòng)會(huì)從已有的進(jìn)程中來啟動(dòng)應(yīng)用,這個(gè)方式叫熱啟動(dòng)。熱啟動(dòng)因?yàn)闀?huì)從已有的進(jìn)程中來啟動(dòng),所以熱啟動(dòng)就不會(huì)走 Application 這步了,而是直接走 MainActivity,所以熱啟動(dòng)的過程不必創(chuàng)建和初始化 Application,因?yàn)橐粋€(gè)應(yīng)用從新進(jìn)程的創(chuàng)建到進(jìn)程的銷毀,Application 只會(huì)初始化一次。
3、首次啟動(dòng):首次啟動(dòng)嚴(yán)格來說也是冷啟動(dòng),之所以把首次啟動(dòng)單獨(dú)列出來,一般來說,首次啟動(dòng)時(shí)間會(huì)比非首次啟動(dòng)要久,首次啟動(dòng)會(huì)做一些系統(tǒng)初始化工作,如緩存目錄的生產(chǎn),數(shù)據(jù)庫的建立,SharedPreference的初始化,如果存在多 dex 和插件的情況下,首次啟動(dòng)會(huì)有一些特殊需要處理的邏輯,而且對(duì)啟動(dòng)速度有很大的影響,所以首次啟動(dòng)的速度非常重要,畢竟影響用戶對(duì) App 的第一映像。
二、本地啟動(dòng)時(shí)間的統(tǒng)計(jì)方式
如果是本地調(diào)試的話,統(tǒng)計(jì)啟動(dòng)時(shí)間還是很簡單的,通過命令行方式即可:
adb shell am start -w packagename/activity
輸出的結(jié)果類似于:
$ adb shell am start -W com.speed.test/com.speed.test.HomeActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.speed.test/.HomeActivity }
Status: ok
Activity: com.speed.test/.HomeActivity
ThisTime: 496
TotalTime: 496
WaitTime: 503
Complete
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í)。
開發(fā)者一般只要關(guān)心 TotalTime 即可,這個(gè)時(shí)間才是自己應(yīng)用真正啟動(dòng)的耗時(shí)。
三、線上啟動(dòng)時(shí)間的統(tǒng)計(jì)方式
當(dāng) App 發(fā)到線上之后,想要統(tǒng)計(jì) App 在用戶手機(jī)上的啟動(dòng)速度,就不能通過命令行的方式進(jìn)行統(tǒng)計(jì)了,基本上都是通過打 Log 的方式將啟動(dòng)時(shí)間發(fā)送上來。那么在什么位置加啟動(dòng)時(shí)間統(tǒng)計(jì)的 Log 就尤為重要,Log 添加的位置直接決定啟動(dòng)時(shí)間統(tǒng)計(jì)的是否準(zhǔn)確,同樣也會(huì)影響啟動(dòng)速度優(yōu)化效果的判斷。要想找到合適準(zhǔn)確的位置記錄啟動(dòng)時(shí)間的 Log,就需要了解應(yīng)用的啟動(dòng)流程,和各個(gè)生命周期函數(shù)的調(diào)用順序。下面來分析下到底在什么位置打 Log 記錄啟動(dòng)時(shí)間比較合適。
應(yīng)用的主要啟動(dòng)流程
關(guān)于 App 啟動(dòng)流程的文章很多,文章底部有一些啟動(dòng)流程相關(guān)的參考文章,這里只列出大致流程如下:
1、通過 Launcher 啟動(dòng)應(yīng)用時(shí),點(diǎn)擊應(yīng)用圖標(biāo)后,Launcher 調(diào)用 startActivity 啟動(dòng)應(yīng)用。
2、Launcher Activity 最終調(diào)用 Instrumentation 的 execStartActivity 來啟動(dòng)應(yīng)用。
3、Instrumentation 調(diào)用 ActivityManagerProxy (ActivityManagerService 在應(yīng)用進(jìn)程的一個(gè)代理對(duì)象) 對(duì)象的 startActivity 方法啟動(dòng) Activity。
4、到目前為止所有過程都在 Launcher 進(jìn)程里面執(zhí)行,接下來 ActivityManagerProxy 對(duì)象跨進(jìn)程調(diào)用 ActivityManagerService (運(yùn)行在 system_server 進(jìn)程)的 startActivity 方法啟動(dòng)應(yīng)用。
5、ActivityManagerService 的 startActivity 方法經(jīng)過一系列調(diào)用,最后調(diào)用 zygoteSendArgsAndGetResult 通過 socket 發(fā)送給 zygote 進(jìn)程,zygote 進(jìn)程會(huì)孵化出新的應(yīng)用進(jìn)程。
6、zygote 進(jìn)程孵化出新的應(yīng)用進(jìn)程后,會(huì)執(zhí)行 ActivityThread 類的 main 方法。在該方法里會(huì)先準(zhǔn)備好 Looper 和消息隊(duì)列,然后調(diào)用 attach 方法將應(yīng)用進(jìn)程綁定到 ActivityManagerService,然后進(jìn)入 loop 循環(huán),不斷地讀取消息隊(duì)列里的消息,并分發(fā)消息。
7、ActivityManagerService 保存應(yīng)用進(jìn)程的一個(gè)代理對(duì)象,然后 ActivityManagerService 通過代理對(duì)象通知應(yīng)用進(jìn)程創(chuàng)建入口 Activity 的實(shí)例,并執(zhí)行它的生命周期函數(shù)。
總結(jié)過程就是:用戶在 Launcher 程序里點(diǎn)擊應(yīng)用圖標(biāo)時(shí),會(huì)通知 ActivityManagerService 啟動(dòng)應(yīng)用的入口 Activity, ActivityManagerService 發(fā)現(xiàn)這個(gè)應(yīng)用還未啟動(dòng),則會(huì)通知 Zygote 進(jìn)程孵化出應(yīng)用進(jìn)程,然后在這個(gè)應(yīng)用進(jìn)程里執(zhí)行 ActivityThread 的 main 方法。應(yīng)用進(jìn)程接下來通知 ActivityManagerService 應(yīng)用進(jìn)程已啟動(dòng),ActivityManagerService 保存應(yīng)用進(jìn)程的一個(gè)代理對(duì)象,這樣 ActivityManagerService 可以通過這個(gè)代理對(duì)象控制應(yīng)用進(jìn)程,然后 ActivityManagerService 通知應(yīng)用進(jìn)程創(chuàng)建入口 Activity 的實(shí)例,并執(zhí)行它的生命周期函數(shù)。
生命周期函數(shù)執(zhí)行流程
上面的啟動(dòng)流程是 Android 提供的機(jī)制,作為開發(fā)者我們需要清楚或者至少了解其中的過程和原理,但我們并不能在這過程中做什么文章,我們能做的恰恰是從上述過程中最后一步開始,即 ActivityManagerService 通過代理對(duì)象通知應(yīng)用進(jìn)程創(chuàng)建入口 Activity 的實(shí)例,并執(zhí)行它的生命周期函數(shù)開始,我們的啟動(dòng)時(shí)間統(tǒng)計(jì)以及啟動(dòng)速度優(yōu)化也是從這里開始。下面是 Main Activity 的啟動(dòng)流程:
-> Application 構(gòu)造函數(shù) -> Application.attachBaseContext() -> Application.onCreate() -> Activity 構(gòu)造函數(shù) -> Activity.setTheme() -> Activity.onCreate() -> Activity.onStart -> Activity.onResume -> Activity.onAttachedToWindow -> Activity.onWindowFocusChanged
如果打 Log 記錄 App 的啟動(dòng)時(shí)間,那么至少要記錄兩個(gè)點(diǎn),一個(gè)起始時(shí)間點(diǎn),一個(gè)結(jié)束時(shí)間點(diǎn)。
起始時(shí)間點(diǎn)
起始時(shí)間點(diǎn)比較容易記錄:如果記錄冷啟動(dòng)啟動(dòng)時(shí)間一般可以在 Application.attachBaseContext() 開始的位置記錄起始時(shí)間點(diǎn),因?yàn)樵谶@之前 Context 還沒有初始化,一般也干不了什么事情,當(dāng)然這個(gè)是要視具體情況來定,其實(shí)只要保證在 App 的具體業(yè)務(wù)邏輯開始執(zhí)行之前記錄起始時(shí)間點(diǎn)即可。如果記錄熱啟動(dòng)啟動(dòng)時(shí)間點(diǎn)可以在 Activity.onRestart() 中記錄起始時(shí)間點(diǎn)。
結(jié)束時(shí)間點(diǎn)
結(jié)束時(shí)間點(diǎn)理論上要選在 App 顯示出第一屏界面的時(shí)候,但是在什么位置 App 顯示出第一屏界面呢?網(wǎng)上很多文章說在 Activity 的 onResume 方法執(zhí)行完成之后,Activity 就對(duì)用戶可見了,實(shí)際上并不是,一個(gè) Activity 走完onCreate onStart onResume 這幾個(gè)生命周期之后,只是完成了應(yīng)用自身的一些配置,比如 Activity 主題設(shè)置 window 屬性的設(shè)置 View 樹的建立,但是其實(shí)后面還需要各個(gè) View 執(zhí)行 measure layout draw等。所以在 OnResume 中記錄結(jié)束時(shí)間點(diǎn)的 Log 并不準(zhǔn)確,大家可以注意一下上面流程中最后一個(gè)函數(shù) Activity.onWindowFocusChanged,下面是它的注釋:
/**
*Called when the current {@link Window} of the activity gains or loses
* focus. This is the best indicator of whether this activity is visible
* to the user. The default implementation clears the key tracking
* state, so should always be called.
...
*/
通過注釋我們可以看到,這個(gè)函數(shù)是判斷 activity 是否可見的最佳位置,所以我們可以在 Activity.onWindowFocusChanged 記錄應(yīng)用啟動(dòng)的結(jié)束時(shí)間點(diǎn),不過需要注意的是該函數(shù),在 Activity 焦點(diǎn)發(fā)生變化時(shí)就會(huì)觸發(fā),所以要做好判斷,去掉不需要的情況。
總結(jié)
以上就是關(guān)于安卓(Android)開發(fā)之統(tǒng)計(jì)App啟動(dòng)時(shí)間的全部內(nèi)容,本文的內(nèi)容小編覺得還是很重要的,還是那句話:工欲善其事,必先利其器,準(zhǔn)備工作做的充分,做事自然有理有據(jù)。希望本文的內(nèi)容對(duì)大家有所幫助。
相關(guān)文章
Flutter 移動(dòng)程序安全性提高的八個(gè)建議
這篇文章主要為大家介紹了Flutter 移動(dòng)程序安全性提高建議詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
Android實(shí)現(xiàn)底部彈出的對(duì)話框功能
這篇文章主要介紹了Android實(shí)現(xiàn)底部彈出的對(duì)話框功能,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05
Android 單例模式實(shí)現(xiàn)可復(fù)用數(shù)據(jù)存儲(chǔ)的詳細(xì)過程
本文介紹了如何使用單例模式實(shí)現(xiàn)一個(gè)可復(fù)用的數(shù)據(jù)存儲(chǔ)類,該類可以存儲(chǔ)不同類型的數(shù)據(jù),并提供統(tǒng)一的接口來訪問這些數(shù)據(jù),通過雙重檢查鎖定機(jī)制,該類在多線程環(huán)境下是線程安全的,感興趣的朋友跟隨小編一起看看吧2025-02-02
詳解Android的OkHttp包編寫異步HTTP請(qǐng)求調(diào)用的方法
OkHttp支持Callback異步回調(diào)來實(shí)現(xiàn)線程的非阻塞,下面我們就來詳解Android的OkHttp包編寫異步HTTP請(qǐng)求調(diào)用的方法,需要的朋友可以參考下2016-07-07
android自定義view實(shí)現(xiàn)推箱子小游戲
這篇文章主要為大家詳細(xì)介紹了android自定義view實(shí)現(xiàn)推箱子小游戲,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04

