未經(jīng)博主同意,不得轉載該篇文章
虛擬機概要
? Android在5.0之前使用的是dalvik虛擬機,使用的是純JIT編譯。在android4.4提出了art虛擬機,使用的是AOT編譯,并在android5.0完全取代dalvik。但是在android7.0采用了JIT,AOT,解釋的混合編譯模式。
? 不論研究什么,都要從最原始的,最基礎的東西都是看起。這里只說明dalvik的部分,涉及到dalvik虛擬機的源碼。
? 至于art和dalvik,JIT和AOT的區(qū)別,優(yōu)化,不是本文的重點。傳送門:ART 和 Dalvik
類加載整體流程
- 對dex文件進行驗證并優(yōu)化,并產(chǎn)出Odex文件
- 對Odex文件進行解析,產(chǎn)出DexFile數(shù)據(jù)結構,即將文件形式的數(shù)據(jù)轉換成內(nèi)存中虛擬機可達的數(shù)據(jù)(如果研究過android的ClassLoader源碼,肯定對DexFile不陌生)
- 對指定的類進行加載,在DexFile中提取對應類的字節(jié)碼,產(chǎn)出ClassObject數(shù)據(jù)結構
Dex文件的優(yōu)化
概要
? Dalivk中,dex的優(yōu)化使用的是dexopt,將dex文件優(yōu)化為Odex文件,最終提交給下一步的加載過程。而不是像art的dex2aot一樣,dex2aot是直接將全部的dex文件編譯為native code存儲。Odex文件的本質只是在原dex文件的基礎上進行優(yōu)化,并生成.Odex文件進行存儲,以提高dalvik虛擬機運行的高效性和安全性。需要注意的是整個dex的優(yōu)化過程都是在一個新進程中進行的。
重要的優(yōu)化點如下:
- 建立dex的類索引表——使虛擬機快速find dex中某個類的地址
- 寄存器的內(nèi)存映射——減少odex->DexFile的內(nèi)存映射操作
- 添加依賴庫信息——添加dex需要使用的本地函數(shù)庫
- dex中字節(jié)碼的替換——比如類似編譯中的內(nèi)聯(lián)優(yōu)化
Dex和Odex文件結構對比圖

Dalvik由于是采用JIT及時編譯,因此App在第一次被打開的時候會進行dexopt操作而導致啟動較慢。
函數(shù)執(zhí)行流程
PackageManagerService是用來管理應用安裝,卸載,優(yōu)化等工作的系統(tǒng)服務。和PMS的各種操作最終會通過Java層的Installer—>InstallerConnection—>Socket通信到native的installd.c服務(這個套路在黑域中也有用到)。
InstallerConnection.connect():

InstallerConnection.dexopt():

最終會調(diào)用到dalvik/dexopt/OptMain.cpp

Dex文件的解析
? 虛擬機需要訪問到可讀的Dex數(shù)據(jù)來進行類加載。因此我們需要將dex文件解析成DexFile的內(nèi)存中的數(shù)據(jù)結構。其解析過程實則是將DexFile數(shù)據(jù)結構中的各個成員變量與Dex文件的各個數(shù)據(jù)部分相關聯(lián)。
DexFile的結構體:

? 需要注意的是這一步是在Dex文件優(yōu)化之后,所以從這里開始提到的Dex文件都是Odex文件。具體的解析過程不敘述。接下來的過程就是要從DexFile中加載指定的類,并將其裝入虛擬機的運行時環(huán)境中。
運行時數(shù)據(jù)裝載
? 到這里,我們需要抽象出另一個數(shù)據(jù)結構——ClassObject。我們到這里可以梳理一下流程:

? 至于ClassObject的數(shù)據(jù)結構,特別的長,有興趣的同學可以自己去源碼看看。位置在 dalvik\vm\native\oo\Object.h中。
重點!類加載Java到Native,揭秘unexpectedDEX異常
到這里,我們可以來一次從Java層的類加載到Native層的類加載調(diào)用流程整個分析了。如果不熟悉Android Java層類加載機制的可以看這篇博客:Android動態(tài)類記載
首先,眾所周知,DexClassLoder和PathClassLoder是我們見的比較多的類。區(qū)別就在于前者可以加載任意路徑下的.jar或者.apk,而后者只能加載默認路徑下的dex文件,即/data/dalvik-cache。然后兩者都繼承自BaseDexClassLoader。那這個區(qū)別的原因是什么呢?其實很簡單:


不用解釋都知道了,后者的構造函數(shù)不能設置Odex文件的路徑,因此一般用作系統(tǒng)類(其實最終是BootClassLoader加載的)和應用類。前者可以動態(tài)的設置Odex路徑,所以經(jīng)常在插件化中被使用。
兩者其實就是一個空殼,真正的加載函數(shù)都是調(diào)用自BaseDexClassLoader的父類ClassLoader的loadClass函數(shù):

可以看到,先調(diào)用了findLoadedClass()方法:

先判斷虛擬機是否已經(jīng)加載了這個class,如果加載了就會直接返回。這也是為什么dex插樁流派的熱修復必須要應用重啟才能修復,因為一旦虛擬機加載了某個類,就不會重新再加載。
再看到后面,先調(diào)用了parent的loadClass()函數(shù),如果為空才會調(diào)用自己的findClass()函數(shù)。對,沒錯!優(yōu)雅的雙親委派機制(責任鏈)以及模版方法的設計模式。Android建議我們不要重寫loadClass()方法,去重寫findClass()方法,就是為了遵循這個機制和生態(tài)。因此我們繼續(xù)跟蹤BaseDexClassLoader的findClass()方法:

看到會調(diào)用DexPathList的findClass()方法,而這個DexList其實內(nèi)部維護著一個DexFile的集合。繼續(xù)跟蹤:

可以看到,就是簡單的遍歷DexFile集合,然后去輪詢Class。其實熟悉熱修復的同學看到這里可以說是很輕松的,因為Q空的插樁,微信的全量替換等之熱修復技術都是在這塊做文章。所以繼續(xù)追蹤DexFile中去,這個其實就是前文提到的了:

額,其實沒什么內(nèi)容,但是敏感的察覺到,我們要進入到逼氣十足的Native層了!defineClassNative(name, loader, cookie);。如果你的手上有Dalvik的源碼,可以和我一起深入進去。這個函數(shù)在vm\native\dalivk_system_DexFile.cpp中的Dalvik_dalvik_system_DexFile_defineClass函數(shù)里??峥岬摹!#?/p>

這里只截取了部分,其實很簡單先調(diào)用dvmGetRawDexFileDex或者dvmGetJarFileDex(如果是jar包)方法去給指向DexFile的指針賦值(其實DexFile是DvmDex的一個成員變量),然后將這個指針傳遞進dvmDefineClass()函數(shù),而這個函數(shù)最終調(diào)用了findClassNoInit()函數(shù):

findClassNoInit()方法是重頭戲,里面先判斷是否已經(jīng)加載,如果沒有加載會繼續(xù)進行加載,通過dexFindClass()方法,返回一個DexClassDef數(shù)據(jù)結構,這個數(shù)據(jù)結構是為了方便快速定位類在Dex中的位置,然后最終通過loadClassFromDex()方法給ClassObject指針賦值:

而loadClassFromDex()方法的源碼看起來比較乏味,直接總結一下:
- 為ClassObject申請內(nèi)存
- 設置字段信息
- 為超類建立索引
- 加載類接口
- 加載類字段
- 加載類方法
自此,我們僅僅完成了類加載的加載階段。類加載實際上還有很多步驟。后面會對ClassObject進行進一步的加工,后面緊接著調(diào)用了dvmLinkClass()方法進行Prepare and resolve,主要將符號引用轉換成為直接引用,在其中會進一步調(diào)用dvmResolveClass()方法,而這個方法實際上是在解析當前被加載類的父類以及接口:

這里只截取了解析父類的部分,接口部分類似??梢钥吹?,加載完了之后,就會將這個符號引用轉換為直接引用,并對GC可見。而這個dvmResolveClass()方法就是unexpected DEX異常觸發(fā)的地方,先來看兩段dvmResolveClass()方法的注釋:
* Because the DexTypeId is associated with the referring class' DEX file,
* we may have to resolve the same class more than once if it's referred
* to from classes in multiple DEX files. This is a necessary property for
* DEX files associated with different class loaders.
大意就是被引用的類可能是別的Dex文件里的,所以我們可能會因為某個類被另一個Dex文件中的類給引用而導致重復解析這個類。表明Dalvik會使用一種機制來避免這種現(xiàn)象,后面會提到。
* "fromUnverifiedConstant" should only be set if this call is the direct
* result of executing a "const-class" or "instance-of" instruction, which
* use class constants not resolved by the bytecode verifier.
大意就是如果我們通過"const-class"(通過類型索引獲取一個類的引用賦值給寄存器,比如直接引用XX.class) 或 "instance-of"(判斷寄存器中對象的引用是否是指定類型)(均為兩者Dalvik虛擬機的指令)指令去引用一個類,那么fromUnverifiedConstant變量會被set為True。表明這個變量在后續(xù)代碼中很重要。繼續(xù)下去:

很好,終于看到了
dvmThrowIllegalAccessError(
"Class ref in pre-verified class resolved to unexpected "
"implementation");
并且很清晰的看到了觸發(fā)unexpectedDEX異常的四個條件:
- fromUnverifiedConstant為False
- 被解析的類,設置了CLASS_ISPREVERIFIED
- referrer->pDvmDex != resClassCheck->pDvmDex
- 被引用類的類加載器不是Null
我們重點看前三個條件:
| fromUnverifiedConstant | 如果變量是被"const-class" or "instance-of"指令加載進來就是True否則為False |
|---|---|
| CLASS_ISPREVERIFIED | 在Dex優(yōu)化過程中引用其他Dex文件的類,被加載類不會設置該狀態(tài),否則會設置該狀態(tài) |
| pDvmDex | 如果被解析類和被引用類不在同一個Dex文件中就會觸發(fā)異常 |
我們重點看一下CLASS_ISPREVERIFIED被設置的代碼。前文有提到Dex文件的優(yōu)化是在dalvik/dexopt/OptMain.cpp的extractAndProcessZip()作為入口開始的。通過我一步步的跟蹤,最終在dalvik\vm\analysis\DexPrepare.cpp中的verifyAndOptimizeClasses()函數(shù)找到了設置的邏輯:

而這個函數(shù)的目的就是驗證并優(yōu)化Dex文件中的所有類,也正是在這里的驗證過程,可能會給類打上CLASS_ISPREVERIFIED的Flag。
針對以上分析的三個點,我們可以在熱修復中做出不同的技術方案:
- 我的前東家手Q的QFix:通過native修改fromUnverifiedConstant變量
- QQ空間:通過給每一個類引入一個單獨Dex中的類來避免CLASS_ISPREVERIFIED被設置
- 微信Tinker:通過全量Dex替換來避免pDvmDex不同
結束語
最近剛離職回學校,買了好幾本新書準備啃一啃,充實一下自己的技術棧。近幾天在研究Dalvik的源碼和機制,發(fā)現(xiàn)很多以前看似虛無縹緲的東西,其實都在源碼可以一探究竟,這種感覺真是太棒了。