前言
之前我們說(shuō)了啟動(dòng)優(yōu)化的一些常用方法,但是有的小伙伴就很不屑了:
“這些方法很久之前就知道了,不知道說(shuō)點(diǎn)新東西琳要?比如App Startup闲礼?能對(duì)啟動(dòng)優(yōu)化有幫助嗎牍汹?”
ok,既然你誠(chéng)心誠(chéng)意的發(fā)問了柬泽,那我就大發(fā)慈悲的告訴你:俺也不知道??
慎菲。
走吧,一起瞅瞅這個(gè)App Startup
吧锨并,是不是真的能給我們的啟動(dòng)帶來(lái)優(yōu)化呢露该?
(想看結(jié)果的可以直接跳到最后的實(shí)踐
和總結(jié)
階段)
Contentprovider中初始化
想必大家都了解,很多三方庫(kù)都需要在Application
中進(jìn)行初始化第煮,并順便獲取到Application
的上下文解幼。
但是也有的庫(kù)不需要我們自己去初始化,它偷偷摸摸就給初始化了包警,用到的方法就是使用ContentProvider
進(jìn)行初始化撵摆,定義一個(gè)ContentProvider
,然后在onCreate拿到上下文揽趾,就可以進(jìn)行三方庫(kù)自己的初始化工作了台汇。而在APP的啟動(dòng)流程中,有一步就是要執(zhí)行到程序中所有注冊(cè)過(guò)的ContentProvider
的onCreate方法,所以這個(gè)庫(kù)的初始化就默默完成了苟呐。
這種做法確實(shí)給集成庫(kù)的開發(fā)者們帶來(lái)了很大的便利痒芝,現(xiàn)在很多庫(kù)都用到了這種方法,比如Facebook牵素,F(xiàn)irebase
严衬,這里拿Facebook
舉例看看他的ContentProvider:
<provider
android:name="com.facebook.internal.FacebookInitProvider"
android:authorities="${applicationId}.FacebookInitProvider"
android:exported="false" />
public final class FacebookInitProvider extends ContentProvider {
private static final String TAG = FacebookInitProvider.class.getSimpleName();
@Override
@SuppressWarnings("deprecation")
public boolean onCreate() {
try {
FacebookSdk.sdkInitialize(getContext());
} catch (Exception ex) {
Log.i(TAG, "Failed to auto initialize the Facebook SDK", ex);
}
return false;
}
//...
}
可以看到,在Fackbook的sdk中笆呆,定義了一個(gè)FacebookInitProvider
请琳,并且在onCreate
中進(jìn)行了初始化。所以我們才無(wú)需單獨(dú)對(duì)Facebook的sdk進(jìn)行初始化赠幕。
雖然更方便了俄精,但是這種做法有給啟動(dòng)優(yōu)化帶來(lái)什么好處嗎?我們一起再回顧下之前的啟動(dòng)流程研究下榕堰,截取一部分:
- ...
- attachBaseContext
- Application attach
- installContentProviders
- Application onCreate
- Looper.loop
- Activity onCreate竖慧,onResume
這其中installContentProviders
方法就是用來(lái)啟動(dòng)并執(zhí)行各個(gè)ContentProvider
的onCreate
方法的,它會(huì)在Application
的onCreate
方法之前執(zhí)行逆屡。
所以這些庫(kù)只是把Application
的三方庫(kù)初始化工作提前放到ContentProvider
中了圾旨,并不會(huì)減少啟動(dòng)耗時(shí),反而會(huì)增加啟動(dòng)耗時(shí)魏蔗。
怎么說(shuō)呢砍的?因?yàn)椴煌膸?kù)就定義了不同的ContentProvider
類,多了這么多ContentProvider
莺治,ContentProvider
作為四大組件之一廓鞠,啟動(dòng)也是耗時(shí)的,自然也就增加App啟動(dòng)消耗的時(shí)間了谣旁。
這時(shí)候就需要App Startup
來(lái)對(duì)此情況進(jìn)行優(yōu)化了~
官網(wǎng)簡(jiǎn)介
The App Startup library provides a straightforward, performant way to initialize components at application startup. Both library developers and app developers can use App Startup to streamline startup sequences and explicitly set the order of initialization.Instead of defining separate content providers for each component you need to initialize, App Startup allows you to define component initializers that share a single content provider. This can significantly improve app startup time.
主要說(shuō)了兩點(diǎn)特性:
- 可以共享單個(gè)Contentprovider诫惭。
- 可以明確地設(shè)置初始化順序。
可以共享單個(gè)Contentprovider
這一點(diǎn)功能就能解決剛才的問題了蔓挖,不同的庫(kù)不再需要去啟動(dòng)多個(gè)Contentprovider
了夕土,而是共享同一個(gè)Contentprovider
。
這樣就至少不會(huì)增加啟動(dòng)耗時(shí)了瘟判。
怎么操作呢怨绣?假如我們是FacebookSDK
設(shè)計(jì)者,我們就來(lái)改一下剛才的FacebookSDK
拷获,集成App Startup
:
//導(dǎo)入庫(kù)
implementation "androidx.startup:startup-runtime:1.0.0"
// Initializes facebooksdk.
class FacebookSDKInitializer : Initializer<Unit> {
private val TAG = "FacebookSDKInitializer"
override fun create(context: Context): Unit {
try {
FacebookSdk.sdkInitialize(context)
} catch (ex: Exception) {
Log.i(TAG, "Failed to auto initialize the Facebook SDK", ex)
}
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
//AndroidManifest.xml中定義
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.FacebookSDKInitializer"
android:value="androidx.startup" />
</provider>
實(shí)現(xiàn)了Initializer
接口篮撑,然后在onCreate方法中進(jìn)行初始化即可,只要所有的庫(kù)都按照這個(gè)標(biāo)準(zhǔn)來(lái)初始化匆瓜,而不是自己?jiǎn)为?dú)自定義ContentProvider
赢笨,那么確實(shí)可以減少啟動(dòng)耗時(shí)未蝌。
其中,tools:node="merge"
標(biāo)簽就是用來(lái)合并所有申明了InitializationProvider
的ContentProvider
茧妒。
等等萧吠,Initializer
接口還有一個(gè)方法dependencies,這又是干啥的呢桐筏?
可以明確地設(shè)置初始化順序
這也就是App Startup的第二個(gè)特性了纸型,可以設(shè)置初始化順序。
可以想象梅忌,按照上述做法狰腌,所有庫(kù)都這樣設(shè)定了,那么都會(huì)在同一個(gè)ContentProvider
也就是androidx.startup.InitializationProvider
中初始化牧氮,但是如果我需要設(shè)定不同庫(kù)的初始化順序怎么辦呢琼腔?
比如上述的facebook
初始化,我需要設(shè)定在另一個(gè)庫(kù)WorkManager
之后運(yùn)行踱葛,那么我們就可以重寫dependencies
方法:
class FacebookSDKInitializer : Initializer<Unit> {
private val TAG = "FacebookSDKInitializer"
override fun create(context: Context): Unit {
try {
FacebookSdk.sdkInitialize(context)
} catch (ex: Exception) {
Log.i(TAG, "Failed to auto initialize the Facebook SDK", ex)
}
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return listOf(WorkManagerInitializer::class.java)
}
}
不錯(cuò)吧展姐,這樣設(shè)定之后,三方庫(kù)的初始化順序就變成了:
WorkManager初始化 -> FacebookSDK初始化剖毯。
實(shí)踐出真理
說(shuō)了這么多,從理論上來(lái)說(shuō)教馆,確實(shí)App Startup
減少了耗時(shí)逊谋,畢竟將多個(gè)ContentProvider
融合成了一個(gè),那么我們秉著“實(shí)踐才是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)”土铺,就來(lái)實(shí)踐看看耗時(shí)減少了多少胶滋。
該怎么統(tǒng)計(jì)這個(gè)啟動(dòng)時(shí)間呢?一般有以下幾個(gè)方案:
如果是Application和Activity的時(shí)間可以通過(guò)
TraceView悲敷、systrace
等 的方式進(jìn)行時(shí)間統(tǒng)計(jì)究恤,但是ContentProvider
的初始化在Application之前,不適用我們這次實(shí)踐后德。Android官方提供了一個(gè)可以統(tǒng)計(jì)線上應(yīng)用啟動(dòng)時(shí)間的工具——
Android Vitals
部宿,它可以在GooglePlay管理中心顯示應(yīng)用啟動(dòng)過(guò)長(zhǎng)情況的啟動(dòng)時(shí)間,很顯然這個(gè)也不適用于我們瓢湃,這個(gè)必須上線到Googleplay
理张。視頻錄制。如果是線下的app绵患,我們可以采用
視頻錄制
的方法準(zhǔn)確測(cè)量啟動(dòng)時(shí)間雾叭,也就是通過(guò)判定視頻的每一幀截圖來(lái)知曉什么時(shí)候app啟動(dòng)了,然后統(tǒng)計(jì)這個(gè)啟動(dòng)時(shí)間落蝙。具體做法就是使用adb shell screenrecord
命令進(jìn)行屏幕錄制然后分析視頻织狐,有興趣的小伙伴可以網(wǎng)上找找資料暂幼,這里就不細(xì)說(shuō)了。最后移迫,就是用系統(tǒng)自帶的統(tǒng)計(jì)時(shí)間
TotalTime
旺嬉。
這個(gè)時(shí)間是Android源碼中幫我們計(jì)算的,可統(tǒng)計(jì)到Activity的啟動(dòng)時(shí)間起意,如果我們?cè)贖ome頁(yè)執(zhí)行命令鹰服,也就能得到一個(gè)冷啟動(dòng)的時(shí)間。雖然這個(gè)時(shí)間不是很準(zhǔn)確揽咕,但是我只需要比較App StartUp使用的的前后時(shí)間大小悲酷,所以也夠用了,開干亲善。
1)測(cè)試2個(gè)ContentProvider
第一次设易,我們測(cè)試2個(gè)ContentProvider
的情況。
<provider
android:name=".appstartup.LibraryAContentProvider"
android:authorities="${applicationId}.LibraryAContentProvider"
android:exported="false" />
<provider
android:name=".appstartup.LibraryBContentProvider"
android:authorities="${applicationId}.LibraryBContentProvider"
android:exported="false" />
安裝到手機(jī)后蛹头,打開應(yīng)用顿肺,Terminal
中輸入命令:
adb shell am start -W -n packagename/packageName.MainActivity
由于每次啟動(dòng)時(shí)間不一,所以我們運(yùn)行五次渣蜗,取平均值:
TotalTime: 927
TotalTime: 938
TotalTime: 948
TotalTime: 934
TotalTime: 937
平均值:936.8
然后注釋剛才的ContentProvider
注冊(cè)代碼屠尊,添加App startup代碼,并注冊(cè):
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.studynote.appstartup.LibraryAInitializer"
android:value="androidx.startup" />
<meta-data android:name="com.example.studynote.appstartup.LibraryBInitializer"
android:value="androidx.startup" />
</provider>
運(yùn)行App耕拷,并執(zhí)行命令讼昆,得出啟動(dòng)時(shí)間:
TotalTime: 931
TotalTime: 947
TotalTime: 937
TotalTime: 940
TotalTime: 932
平均值:937.4
咦?骚烧?我手機(jī)壞了嗎浸赫?怎么跟預(yù)想的不一樣啊,結(jié)果耗時(shí)還增加了赃绊?
按道理來(lái)說(shuō)原來(lái)有兩個(gè)ContentProvider
既峡,用了App startup
,集成為一個(gè)碧查,耗時(shí)不應(yīng)該減少么运敢。
其實(shí)這就涉及到ContentProvider
的實(shí)際耗時(shí)了,我在網(wǎng)上找到一張圖忠售,關(guān)于ContentProvider
耗時(shí)者冤,是Google
官方做的統(tǒng)計(jì),圖片來(lái)源于郭神的博客:
可以看到這里統(tǒng)計(jì)的1個(gè)ContentProvider
耗時(shí)2ms
左右档痪,10ContentProvider
耗時(shí)6ms左右涉枫。
所以我們只減少了一個(gè)ContentProvider的耗時(shí),幾乎可以忽略不計(jì)腐螟。再加上我們用到的App Startup庫(kù)中InitializationProvider
的一些任務(wù)也會(huì)產(chǎn)生耗時(shí)愿汰,比如:
- 會(huì)去遍歷所有
metadata
標(biāo)簽的組件 - 會(huì)通過(guò)反射獲取每個(gè)組件的
Initializer
接口困后,并獲取相應(yīng)的依賴項(xiàng),并進(jìn)行排序
衬廷。
這些操作也是耗時(shí)的摇予,也就是集成App Startup
庫(kù)之后增加的耗時(shí)時(shí)間。所以就有可能會(huì)發(fā)生上面的情況了吗跋,集成App Startup庫(kù)之后啟動(dòng)耗時(shí)反而增多侧戴。
那難道這個(gè)庫(kù)就沒用了嗎?肯定不是的跌宛,當(dāng)ContentProvider的數(shù)量變多酗宋,它的作用就體現(xiàn)出來(lái)了,再試下10個(gè)ContentProvider
的情況疆拘。
2)10個(gè)ContentProvider
首先寫好10個(gè)ContentProvider
蜕猫,并在AndroidManifest.xml中注冊(cè):
<provider
android:name=".appstartup.LibraryAContentProvider"
android:authorities="${applicationId}.LibraryAContentProvider"
android:exported="false" />
<!-- 省略剩下9個(gè)provider注冊(cè)代碼 -->
運(yùn)行五次,取平均值:
TotalTime: 1758
TotalTime: 1759
TotalTime: 1733
TotalTime: 1737
TotalTime: 1747
平均值:1746.8
然后注釋剛才的ContentProvider
注冊(cè)代碼哎迄,添加App startup代碼回右,并注冊(cè):
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.studynote.appstartup.LibraryAInitializer"
android:value="androidx.startup" />
<!--省略剩下9個(gè)meta-data注冊(cè)代碼-->
</provider>
運(yùn)行App,并執(zhí)行命令漱挚,得出啟動(dòng)時(shí)間:
TotalTime: 1741
TotalTime: 1755
TotalTime: 1722
TotalTime: 1739
TotalTime: 1730
平均值:1737.4
可以看到翔烁,這里App Startup的作用就體現(xiàn)了出來(lái),在使用App Startup
之前的啟動(dòng)耗時(shí)是1746.8ms
旨涝,使用之后啟動(dòng)耗時(shí)是1737.4ms
蹬屹,減少了9.4ms
。
所以得出結(jié)論颊糜,當(dāng)集成的庫(kù)使用的ContentProvider
達(dá)到一定個(gè)數(shù)之后,確實(shí)能減少耗時(shí)秃踩,但是減少的不多衬鱼,比如這里我們是10個(gè)ContentProvider
集成App Startup
后能減少的耗時(shí)在10ms
左右,再結(jié)合上圖官方的統(tǒng)計(jì)時(shí)間來(lái)看憔杨,一般一個(gè)項(xiàng)目集成了十幾個(gè)使用ContentProvider的庫(kù)鸟赫,耗時(shí)減少應(yīng)該能在20ms之內(nèi)。
所以我們的App Startup
解決的就是這個(gè)耗時(shí)時(shí)間消别,雖然不多抛蚤,但是也確實(shí)有減少耗時(shí)的功能。
思考
雖然這個(gè)庫(kù)能解決一定的三方庫(kù)初始化
耗時(shí)問題寻狂,但是我覺得還是有很大的局限性
岁经,比如這些問題:
-
本身依賴的庫(kù)就不多
。如果我們的項(xiàng)目本身依賴就不多蛇券,那么有沒有必要去集成這個(gè)呢缀壤?極端情況下樊拓,只依賴了一個(gè)庫(kù),那么還要專門提供一個(gè)InitializationProvider塘慕,是不是又變相的增加了耗時(shí)呢筋夏? -
延時(shí)初始化
。上次我們說(shuō)過(guò)图呢,有些庫(kù)并不需要一開始就初始化条篷,那么我們最好將其延遲初始化,進(jìn)行懶加載蛤织。 -
異步初始化
赴叹。同樣,有些庫(kù)不需要在主線程進(jìn)行初始化瞳筏,那么我們可以對(duì)其進(jìn)行異步初始化稚瘾,從而減少啟動(dòng)耗時(shí)。 -
多個(gè)異步任務(wù)依賴關(guān)系
姚炕。如果有些任務(wù)需要異步執(zhí)行的同時(shí)還有互相的依賴關(guān)系摊欠,該怎么辦呢。
如果我們?cè)谑褂?code>App Startup的時(shí)候柱宦,有以上需求些椒,那么有沒有解決辦法呢?
- 沒有掸刊,也可以說(shuō)有免糕,就是關(guān)閉
App Startup
的初始化動(dòng)作,然后自己進(jìn)行初始化任務(wù)管理忧侧。
這可不是開玩笑石窑,App Startup
的目的只是解決一個(gè)問題,就是多個(gè)ContentProvider
創(chuàng)建的問題蚓炬,通過(guò)一個(gè)統(tǒng)一的ContentProvider
來(lái)形成規(guī)范松逊,減少耗時(shí)。所以它的用法應(yīng)該是針對(duì)各個(gè)三方庫(kù)的設(shè)計(jì)者肯夏,當(dāng)你設(shè)計(jì)一個(gè)庫(kù)的時(shí)候经宏,如果想靜默初始化,就可以接入App Startup驯击。當(dāng)盡量多的庫(kù)遵循這個(gè)要求烁兰,都接入App Startup
的時(shí)候,開發(fā)者的啟動(dòng)耗時(shí)自然就降低了徊都。
但是如果我們有其他的需求沪斟,比如上述說(shuō)到的延遲初始化,異步初始化等問題暇矫,我們就要關(guān)閉部分庫(kù)或者所有庫(kù)的App Startup
的功能币喧,然后自己?jiǎn)为?dú)對(duì)任務(wù)進(jìn)行初始化工作轨域,比如通過(guò)啟動(dòng)器
來(lái)處理各個(gè)初始化任務(wù)的關(guān)系。
如果一個(gè)庫(kù)已經(jīng)集成了App Startup
功能杀餐,我們?cè)撛趺搓P(guān)閉呢干发?這就用到tools:node="remove"
標(biāo)簽了。
<!-- 禁用所有InitializationProvider組件初始化 -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
<!-- 禁用單個(gè)InitializationProvider組件初始化 -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.FacebookSDKInitializer"
android:value="androidx.startup"
tools:node="remove"/>
</provider>
這樣FacebookSDK
就不會(huì)自動(dòng)進(jìn)行初始化了史翘,需要我們手動(dòng)調(diào)用初始化方法枉长。
總結(jié)
1)App Startup
的設(shè)計(jì)是為了解決一個(gè)問題:
- 即不同的庫(kù)使用不同的ContentProvider進(jìn)行初始化,導(dǎo)致ContentProvider太多琼讽,管理雜亂必峰,影響耗時(shí)的問題。
2)App Startup
具體能減少多少耗時(shí)時(shí)間:
- 上面也實(shí)踐過(guò)了钻蹬,如果二三十個(gè)三方庫(kù)都集成了App Startup吼蚁,減少的耗時(shí)大概在20ms以內(nèi)。
3)App Startup
的使用場(chǎng)景應(yīng)該是:
- 針對(duì)三方庫(kù)的設(shè)計(jì)者或者組件化的場(chǎng)景问欠。當(dāng)你設(shè)計(jì)一個(gè)庫(kù)或者一個(gè)組件的時(shí)候肝匆,就可以接入App Startup。當(dāng)盡量多的庫(kù)遵循這個(gè)標(biāo)準(zhǔn)顺献,都接入App Startup的時(shí)候旗国,就能形成一種規(guī)范,App的啟動(dòng)耗時(shí)自然就降低了注整。
4)如果想解決多個(gè)庫(kù)初始化任務(wù)太多導(dǎo)致的啟動(dòng)耗時(shí)
問題:
- 請(qǐng)左轉(zhuǎn)前往各種啟動(dòng)器能曾,比如alibaba/alpha
參考
Android啟動(dòng)時(shí)間—siyu8023
拜拜
感謝大家的閱讀,有一起學(xué)習(xí)的小伙伴可以關(guān)注下我的公眾號(hào)——碼上積木????
每日三問知識(shí)點(diǎn)/面試題肿轨,積少成多寿冕。
這里有一群很好的Android小伙伴,歡迎大家加入~