Android自定義ClassLoader耗時(shí)問(wèn)題追查

最近在優(yōu)化西瓜視頻客戶(hù)端冷啟動(dòng)速度時(shí)黄选,發(fā)現(xiàn)在關(guān)閉插件 ClassLoader 注入的情況下,啟動(dòng)速度提升了300ms左右碍庵,但是西瓜在啟動(dòng)階段并沒(méi)有使用到插件巩那,那么這么大的耗時(shí)是怎么來(lái)的呢?

猜原因

首先看下西瓜目前使用的插件 ClassLoader 是怎么注入的玄糟,大致代碼如下:

image

代碼大致意思是在 PathClassLoader 和 BootClassLoader 之間插入了一個(gè) DelegateClassLoader勿她,而在 DelegateClassLoader 的 findClass 方法中去執(zhí)行插件 Class 的加載。

為了方便驗(yàn)證阵翎,寫(xiě)一個(gè)簡(jiǎn)單的測(cè)試Demo逢并,測(cè)試加載一個(gè)類(lèi)的耗時(shí):

image

以小米Max2,Android7.1.1機(jī)型為例郭卫,測(cè)試不注入和注入 DelegateClassLoader 加載一個(gè)類(lèi)的耗時(shí):

不注入:60μs

注入后:472μs

差不多慢了8倍砍聊,測(cè)試了幾款手機(jī)基本數(shù)據(jù)都差不多,但是4.x手機(jī)上這兩種情況下耗時(shí)差別卻很小贰军。

DelegateClassLoader.findClass耗時(shí)玻蝌?

因?yàn)殡p親委托機(jī)制,所以宿主中所有類(lèi)的加載都會(huì)走到 DelegateClassLoader.findClass 中词疼,但是 DelegateClassLoader 中因?yàn)椴淮嬖谒拗黝?lèi)灶伊,所以必然找不到,因此一個(gè)宿主類(lèi)的加載會(huì)多調(diào)用了一次無(wú)用的 findClass 方法寒跳,一次findClass的調(diào)用會(huì)帶來(lái)如此大的耗時(shí)?于是將 DelegateClassLoader 代碼精簡(jiǎn)成下面這樣的:

image

這樣竹椒,DelegateClassLoader 中沒(méi)有做任何插件類(lèi)加載的邏輯童太,只是做了一個(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í)差異的話(huà)爆惧,那么多調(diào)用兩次是不是就是16倍的差異?

于是嘗試注入兩個(gè) DelegateClassLoader锨能,類(lèi)似這樣:

image

但是結(jié)果還是8倍左右的耗時(shí)差異扯再,并非16倍,這么說(shuō)不是方法調(diào)用帶來(lái)的性能損耗址遇。

自定義ClassLoader耗時(shí)熄阻?

所以猜測(cè)可能是系統(tǒng)對(duì) PathClassLoader 有什么優(yōu)化?然后直接構(gòu)造一個(gè)空的 PathClassLoader 注入到 PathClassLoader 和 BootClassLoader 中間倔约,類(lèi)似這樣:

<pre style="margin: 0px; padding: 0px; border-radius: 8px; background: rgb(255, 255, 255); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; border-width: 0px; border-style: initial; border-color: initial; font-variant-numeric: inherit; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93);">

image

神奇的8倍耗時(shí)差異沒(méi)了秃殉!所以真的是系統(tǒng)對(duì) PathClassLoader 有優(yōu)化?

帶著這個(gè)疑問(wèn)我們來(lái)看下 ClassLoader 的源碼,以 Android 7.1.1 源碼為例钾军。

ClassLoader#loadClass

首先來(lái)看下源頭鳄袍,ClassLoader 的 loadClass 源碼,核心代碼如下:

image

大致流程是先調(diào)用 findLoadedClass 嘗試從已加載的 class 中查找吏恭,然后再調(diào)用父 ClassLoader 的 loadClass 查找拗小,如果依然沒(méi)有找到的話(huà),最后再調(diào)用自己的 findClass 加載砸泛。

在 JVM 中十籍,類(lèi)第一次加載時(shí),肯定之前是沒(méi)有加載過(guò)的唇礁,因此 findLoadedClass 應(yīng)該是返回 null 的勾栗,而 BootClassLoader 中只有系統(tǒng)類(lèi),因此宿主類(lèi)的加載應(yīng)該是調(diào)用了 PathClassLoader#findClass 加載的盏筐。

PathClassLoader#findClass

那么我們?cè)賮?lái)看看 PathClassLoader#findClass 的源碼围俘,調(diào)用鏈大致如下:

image

如果說(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</g> 中是給加載好的 class 設(shè)置 ClassLoader宿亡,兩個(gè)方法對(duì) ClassLoader 的操作看上去是不存在任何優(yōu)化的,理論上不會(huì)導(dǎo)致性能損耗纳令,這里不再貼代碼挽荠。

如果不是 findClass 里有優(yōu)化,難道在 ClassLoader#findLoadedClass 里平绩?

ClassLoader#findLoadedClass

再來(lái)看看 ClassLoader#findLoadedClass 的源碼圈匆,調(diào)用鏈大致如下:

image

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

image

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

image

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

這樣就會(huì)走到第二個(gè)分支(第25行) ClassLinker::FindClassInPathClassLoader 中


image

這里主要分為兩個(gè)部分:

  • 第一部分:從37行開(kāi)始嘹锁,反射從 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í)行類(lèi)的加載面粮,整個(gè)流程其實(shí)就是在 c 層把 Java 層的 PathClassLoader#findClass 邏輯走了一遍;

  • 第二部分:采用遞歸的方式继低,從 BootClassLoader 開(kāi)始依次到 PathClassLoader 逐個(gè)調(diào)用 FindClassInPathClassLoader熬苍,直到找到 class 為止,相當(dāng)于把 Java 層 ClassLoader 的雙親委托加載 class 的機(jī)制在 c 層做了一遍袁翁,這個(gè)其實(shí)是 ART 上對(duì) class 加載做的一個(gè)優(yōu)化柴底,但是在 Dalvik 中是沒(méi)有這段邏輯的,可以參考/dalvik/native/java_lang_VMClassLoader.cpp粱胜。

重點(diǎn)來(lái)了柄驻!因?yàn)樯厦媸褂玫搅朔瓷錂C(jī)制取 PathClassLoader 中的字段,為了保證這套機(jī)制不出問(wèn)題焙压,這里面加了個(gè)校驗(yàn):


image

如果 ClassLoader 鏈中存在不認(rèn)識(shí)的 ClassLoader鸿脓,也就是說(shuō) ClassLoader 的類(lèi)不是 BootClassLoader 和 PathClassLoader,那么就認(rèn)為加載類(lèi)失敗涯曲。當(dāng)然這里加載失敗的話(huà)野哭,并不會(huì)影響最終類(lèi)加載結(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)類(lèi)加載機(jī)制疟位,而 JVM 默認(rèn)的類(lèi)加載機(jī)制存在多次 JNI 調(diào)用,JNI 調(diào)用本身性能是比直接方法調(diào)用耗時(shí)高幾倍的喘垂,這里不再詳細(xì)展開(kāi)甜刻,因此也就能解釋前面所說(shuō)的幾倍的耗時(shí)差異了。

參考

  • Android N混合編譯與對(duì)熱補(bǔ)丁影響解析
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末正勒,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌咽白,老刑警劉巖闺魏,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蜕径,警方通過(guò)查閱死者的電腦和手機(jī)两踏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)兜喻,“玉大人梦染,你說(shuō)我怎么就攤上這事∑咏裕” “怎么了帕识?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)遂铡。 經(jīng)常有香客問(wèn)我肮疗,道長(zhǎng),這世上最難降的妖魔是什么忧便? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任族吻,我火速辦了婚禮,結(jié)果婚禮上珠增,老公的妹妹穿的比我還像新娘超歌。我一直安慰自己,他們只是感情好蒂教,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布巍举。 她就那樣靜靜地躺著,像睡著了一般凝垛。 火紅的嫁衣襯著肌膚如雪懊悯。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天梦皮,我揣著相機(jī)與錄音炭分,去河邊找鬼。 笑死剑肯,一個(gè)胖子當(dāng)著我的面吹牛捧毛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播让网,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼呀忧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了溃睹?” 一聲冷哼從身側(cè)響起而账,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎因篇,沒(méi)想到半個(gè)月后泞辐,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體笔横,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年铛碑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了狠裹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡汽烦,死狀恐怖涛菠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情撇吞,我是刑警寧澤俗冻,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站牍颈,受9級(jí)特大地震影響迄薄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜煮岁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一讥蔽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧画机,春花似錦冶伞、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至荚醒,卻和暖如春芋类,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背界阁。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工侯繁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泡躯。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓贮竟,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親精续。 傳聞我的和親對(duì)象是個(gè)殘疾皇子坝锰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

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