關(guān)于Android中自定義ClassLoader耗時(shí)問(wèn)題的追查
前言
Android中類加載器有BootClassLoader,URLClassLoader,
PathClassLoader,DexClassLoader,BaseDexClassLoader,等都最終繼承自java.lang.ClassLoader
最近在優(yōu)化西瓜視頻客戶端冷啟動(dòng)速度時(shí),發(fā)現(xiàn)在關(guān)閉插件 ClassLoader 注入的情況下,啟動(dòng)速度提升了300ms左右,但是西瓜在啟動(dòng)階段并沒有使用到插件,那么這么大的耗時(shí)是怎么來(lái)的呢?下面話不多說(shuō)了,來(lái)一起看看詳細(xì)的介紹吧。
猜原因
首先看下西瓜目前使用的插件 ClassLoader 是怎么注入的,大致代碼如下:

代碼大致意思是在 PathClassLoader 和 BootClassLoader 之間插入了一個(gè) DelegateClassLoader,而在 DelegateClassLoader 的 findClass 方法中去執(zhí)行插件 Class 的加載。
為了方便驗(yàn)證,寫一個(gè)簡(jiǎn)單的測(cè)試Demo,測(cè)試加載一個(gè)類的耗時(shí):

以小米Max2,Android7.1.1機(jī)型為例,測(cè)試不注入和注入 DelegateClassLoader 加載一個(gè)類的耗時(shí):
不注入:60μs
注入后:472μs
差不多慢了8倍,測(cè)試了幾款手機(jī)基本數(shù)據(jù)都差不多,但是4.x手機(jī)上這兩種情況下耗時(shí)差別卻很小。
DelegateClassLoader.findClass耗時(shí)?
因?yàn)殡p親委托機(jī)制,所以宿主中所有類的加載都會(huì)走到 DelegateClassLoader.findClass 中,但是 DelegateClassLoader 中因?yàn)椴淮嬖谒拗黝?,所以必然找不到,因此一個(gè)宿主類的加載會(huì)多調(diào)用了一次無(wú)用的 findClass 方法,一次findClass的調(diào)用會(huì)帶來(lái)如此大的耗時(shí)?于是將 DelegateClassLoader 代碼精簡(jiǎn)成下面這樣的:

這樣,DelegateClassLoader 中沒有做任何插件類加載的邏輯,只是做了一個(gè)中轉(zhuǎn)到父 ClassLoader 的 loadClass 的操作。
結(jié)果依然是8倍左右的耗時(shí)差距。
java方法調(diào)用耗時(shí)?
上面方案里只是比不注入自定義 ClassLoader 多了一次 DelegateClassLoader.loadClass 方法的調(diào)用,理論上不可能存在這么大的耗時(shí)。如果說(shuō)多調(diào)用一次 java 方法 DelegateClassLoader.loadClass 會(huì)有8倍的耗時(shí)差異的話,那么多調(diào)用兩次是不是就是16倍的差異?
于是嘗試注入兩個(gè) DelegateClassLoader,類似這樣:

但是結(jié)果還是8倍左右的耗時(shí)差異,并非16倍,這么說(shuō)不是方法調(diào)用帶來(lái)的性能損耗。
自定義ClassLoader耗時(shí)?
所以猜測(cè)可能是系統(tǒng)對(duì) PathClassLoader 有什么優(yōu)化?然后直接構(gòu)造一個(gè)空的 PathClassLoader 注入到 PathClassLoader 和 BootClassLoader 中間,類似這樣:

神奇的8倍耗時(shí)差異沒了!所以真的是系統(tǒng)對(duì) PathClassLoader 有優(yōu)化?
帶著這個(gè)疑問(wèn)我們來(lái)看下 ClassLoader 的源碼,以 Android 7.1.1 源碼為例。
ClassLoader#loadClass
首先來(lái)看下源頭,ClassLoader 的 loadClass 源碼,核心代碼如下:

大致流程是先調(diào)用 findLoadedClass 嘗試從已加載的 class 中查找,然后再調(diào)用父 ClassLoader 的 loadClass 查找,如果依然沒有找到的話,最后再調(diào)用自己的 findClass 加載。
在 JVM 中,類第一次加載時(shí),肯定之前是沒有加載過(guò)的,因此 findLoadedClass 應(yīng)該是返回 null 的,而 BootClassLoader 中只有系統(tǒng)類,因此宿主類的加載應(yīng)該是調(diào)用了 PathClassLoader#findClass 加載的。
PathClassLoader#findClass
那么我們?cè)賮?lái)看看 PathClassLoader#findClass 的源碼,調(diào)用鏈大致如下:

如果說(shuō)系統(tǒng)對(duì) ClassLoader 有某些優(yōu)化,那么應(yīng)該只要重點(diǎn)關(guān)注在調(diào)用鏈中有用到 ClassLoader 的地方即可。
整個(gè) findClass 流程中使用到 ClassLoader 的地方并不多,只有 ClassLinker::RegisterDexFile 和 ClassLinker::SetupClass 中使用到了。
- ClassLinker::RegisterDexFile 中是對(duì) ClassLoader 取 class_table 的簡(jiǎn)單操作;
- ClassLinker::SetupClass 中是給加載好的 class 設(shè)置 ClassLoader,兩個(gè)方法對(duì) ClassLoader 的操作看上去是不存在任何優(yōu)化的,理論上不會(huì)導(dǎo)致性能損耗,這里不再貼代碼。
如果不是 findClass 里有優(yōu)化,難道在 ClassLoader#findLoadedClass 里?
ClassLoader#findLoadedClass
再來(lái)看看 ClassLoader#findLoadedClass 的源碼,調(diào)用鏈大致如下:

首先來(lái)看下c層調(diào)用的第一個(gè)方法 VMClassLoader_findLoadedClass :

這里主要有兩個(gè)分支,第一個(gè)分支,第12行調(diào)用 ClassLinker#LookupClass :

這里大致意思是從 ClassLoader 中找到 ClassTable ,然后調(diào)用 ClassTable#Lookup 而這個(gè) ClassTable 里面就保存了已經(jīng)加載過(guò)的類以及啟動(dòng)時(shí)從 app image 中加載的類(app image的作用是記錄已經(jīng)編譯好的“熱代碼”,并且在啟動(dòng)時(shí)一次性把它們加載到緩存,參考Tinker博客)。如果一個(gè)類是首次加載且不在 app image 中,那么這里會(huì)返回 null。
這樣就會(huì)走到第二個(gè)分支(第25行) ClassLinker::FindClassInPathClassLoader 中

這里主要分為兩個(gè)部分:
- 第一部分:從37行開始,反射從 Java 層的 PathClassLoader 取得 DexPathList,然后再反射從 DexPathList 中取得 dexElements,然后再遍歷 dexElements,從每個(gè) Element 中取得 dexFile,然后再?gòu)?DexFile 中取得 mCookie,然后通過(guò) mCookie 得到 c 層的 DexFile,最后調(diào)用 c 層 DexFile#FindClassDef 來(lái)真正的執(zhí)行類的加載,整個(gè)流程其實(shí)就是在 c 層把 Java 層的 PathClassLoader#findClass 邏輯走了一遍;
- 第二部分:采用遞歸的方式,從 BootClassLoader 開始依次到 PathClassLoader 逐個(gè)調(diào)用 FindClassInPathClassLoader,直到找到 class 為止,相當(dāng)于把 Java 層 ClassLoader 的雙親委托加載 class 的機(jī)制在 c 層做了一遍,這個(gè)其實(shí)是 ART 上對(duì) class 加載做的一個(gè)優(yōu)化,但是在 Dalvik 中是沒有這段邏輯的,可以參考/dalvik/native/javalangVMClassLoader.cpp。
重點(diǎn)來(lái)了!因?yàn)樯厦媸褂玫搅朔瓷錂C(jī)制取 PathClassLoader 中的字段,為了保證這套機(jī)制不出問(wèn)題,這里面加了個(gè)校驗(yàn):

如果 ClassLoader 鏈中存在不認(rèn)識(shí)的 ClassLoader,也就是說(shuō) ClassLoader 的類不是 BootClassLoader 和 PathClassLoader,那么就認(rèn)為加載類失敗。當(dāng)然這里加載失敗的話,并不會(huì)影響最終類加載結(jié)果,因?yàn)樵?Java 層 findLoadedClass 失敗后,會(huì)走到 findClass 中的。
結(jié)論
在 Android ART 中默認(rèn)的 ClassLoader 機(jī)制,在 ClassLoader#findLoadedClass 時(shí)就把 JVM 中的 findLoadedClass 和 findClass 兩件事情都做了。但是如果在 class loader 鏈中存在自定義 ClassLoader,那么這個(gè)機(jī)制就會(huì)失效,會(huì)回退到 JVM 默認(rèn)的 ClassLoader 機(jī)制。
回到上面的問(wèn)題,由于我們自定義了 ClassLoader,導(dǎo)致 Art 的 ClassLoader 機(jī)制回退到了 JVM 的默認(rèn)類加載機(jī)制,而 JVM 默認(rèn)的類加載機(jī)制存在多次 JNI 調(diào)用,JNI 調(diào)用本身性能是比直接方法調(diào)用耗時(shí)高幾倍的,這里不再詳細(xì)展開,因此也就能解釋前面所說(shuō)的幾倍的耗時(shí)差異了。
參考
Android N混合編譯與對(duì)熱補(bǔ)丁影響解析
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Android 中ContentProvider的實(shí)例詳解
這篇文章主要介紹了Android 中ContentProvider的實(shí)例詳解的相關(guān)資料,希望通過(guò)本文大家能掌握這部分內(nèi)容,需要的朋友可以參考下2017-09-09
Android中Activity過(guò)渡動(dòng)畫的實(shí)例講解
在android5.0 以上版本中,google為我們提供了幾種activity切換的過(guò)渡動(dòng)畫,目的是為了讓 activity 切換轉(zhuǎn)場(chǎng)更加美觀,下面這篇文章主要給大家介紹了關(guān)于Android中Activity過(guò)渡動(dòng)畫的相關(guān)資料,需要的朋友可以參考下2021-11-11
Android 使用AsyncTask實(shí)現(xiàn)斷點(diǎn)續(xù)傳
這篇文章主要介紹了Android 使用AsyncTask實(shí)現(xiàn)斷點(diǎn)續(xù)傳的實(shí)例代碼,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2018-05-05
AndroidManifest.xml uses-feature功能詳解
這篇文章主要介紹了AndroidManifest.xml uses-feature功能,較為詳細(xì)的分析了Android屬性過(guò)濾操作的功能與相關(guān)技巧,需要的朋友可以參考下2016-10-10
基于Android實(shí)現(xiàn)仿QQ5.0側(cè)滑
本課程將帶領(lǐng)大家通過(guò)自定義控件實(shí)現(xiàn)QQ5.0側(cè)滑菜單,課程將循序漸進(jìn),首先實(shí)現(xiàn)最普通的側(cè)滑菜單,然后引入屬性動(dòng)畫與拖動(dòng)菜單效果相結(jié)合,最終實(shí)現(xiàn)QQ5.0側(cè)滑菜單效果。通過(guò)本課程大家會(huì)對(duì)側(cè)滑菜單有更深層次的了解,通過(guò)自定義控件和屬性動(dòng)畫打造千變?nèi)f化的側(cè)滑菜單效果2015-12-12
Android實(shí)現(xiàn)跨進(jìn)程接口回掉的方法
這篇文章主要給大家介紹了關(guān)于Android如何實(shí)現(xiàn)跨進(jìn)程接口回掉的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
實(shí)時(shí)獲取股票數(shù)據(jù)的android app應(yīng)用程序源碼分享
本文我們分享一個(gè)實(shí)時(shí)獲取股票數(shù)據(jù)的android app應(yīng)用程序源碼分享,可以作為學(xué)習(xí)使用,本文貼出部分重要代碼,需要的朋友可以參考下本文2015-09-09
關(guān)于Fragment?already?added問(wèn)題的解決方案
這篇文章主要介紹了關(guān)于Fragment?already?added問(wèn)題的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10

