Dalvik類加載機制以及unexpectedDEX異常的避免

未經(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_1.png

Dalvik由于是采用JIT及時編譯,因此App在第一次被打開的時候會進行dexopt操作而導致啟動較慢。

函數(shù)執(zhí)行流程

PackageManagerService是用來管理應用安裝,卸載,優(yōu)化等工作的系統(tǒng)服務。和PMS的各種操作最終會通過Java層的Installer—>InstallerConnection—>Socket通信到native的installd.c服務(這個套路在黑域中也有用到)。

InstallerConnection.connect():

dalvik_2.png

InstallerConnection.dexopt():

dalvik_3.png

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

dalvik_4.png

Dex文件的解析

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

DexFile的結構體:

dalvik_5.png

? 需要注意的是這一步是在Dex文件優(yōu)化之后,所以從這里開始提到的Dex文件都是Odex文件。具體的解析過程不敘述。接下來的過程就是要從DexFile中加載指定的類,并將其裝入虛擬機的運行時環(huán)境中。

運行時數(shù)據(jù)裝載

? 到這里,我們需要抽象出另一個數(shù)據(jù)結構——ClassObject。我們到這里可以梳理一下流程:

dalvik_6.png

? 至于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ū)別的原因是什么呢?其實很簡單:

dalvik_7.png
dalvik_8.png

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

兩者其實就是一個空殼,真正的加載函數(shù)都是調(diào)用自BaseDexClassLoader的父類ClassLoader的loadClass函數(shù):

dalvik_9.png

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

dalvik_10.png

先判斷虛擬機是否已經(jīng)加載了這個class,如果加載了就會直接返回。這也是為什么dex插樁流派的熱修復必須要應用重啟才能修復,因為一旦虛擬機加載了某個類,就不會重新再加載。

再看到后面,先調(diào)用了parent的loadClass()函數(shù),如果為空才會調(diào)用自己的findClass()函數(shù)。對,沒錯!優(yōu)雅的雙親委派機制(責任鏈)以及模版方法的設計模式。Android建議我們不要重寫loadClass()方法,去重寫findClass()方法,就是為了遵循這個機制和生態(tài)。因此我們繼續(xù)跟蹤BaseDexClassLoader的findClass()方法:

dalvik_11.png

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

dalvik_12.png

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

dalvik_13.png

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

dalvik_14.png

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

dalvik_15.png

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

dalvik_16.png

而loadClassFromDex()方法的源碼看起來比較乏味,直接總結一下:

  • 為ClassObject申請內(nèi)存
  • 設置字段信息
  • 為超類建立索引
  • 加載類接口
  • 加載類字段
  • 加載類方法

自此,我們僅僅完成了類加載的加載階段。類加載實際上還有很多步驟。后面會對ClassObject進行進一步的加工,后面緊接著調(diào)用了dvmLinkClass()方法進行Prepare and resolve,主要將符號引用轉換成為直接引用,在其中會進一步調(diào)用dvmResolveClass()方法,而這個方法實際上是在解析當前被加載類的父類以及接口:

dalvik_17.png

這里只截取了解析父類的部分,接口部分類似??梢钥吹?,加載完了之后,就會將這個符號引用轉換為直接引用,并對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ù)下去:

dalvik_18.png

很好,終于看到了

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ù)找到了設置的邏輯:

dalvik_19.png

而這個函數(shù)的目的就是驗證并優(yōu)化Dex文件中的所有類,也正是在這里的驗證過程,可能會給類打上CLASS_ISPREVERIFIED的Flag。

針對以上分析的三個點,我們可以在熱修復中做出不同的技術方案:

  • 我的前東家手Q的QFix:通過native修改fromUnverifiedConstant變量
  • QQ空間:通過給每一個類引入一個單獨Dex中的類來避免CLASS_ISPREVERIFIED被設置
  • 微信Tinker:通過全量Dex替換來避免pDvmDex不同

結束語

最近剛離職回學校,買了好幾本新書準備啃一啃,充實一下自己的技術棧。近幾天在研究Dalvik的源碼和機制,發(fā)現(xiàn)很多以前看似虛無縹緲的東西,其實都在源碼可以一探究竟,這種感覺真是太棒了。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容