本篇文章繼續(xù)上一篇瓢捉,主要分析一下classloader方案在dalvik虛擬機中的pre-verify問題舶治。關(guān)于classloader方案的原理可以參考上一篇文章android熱修復(fù)相關(guān)之Multidex解析進行了解请祖。自從Multidex出現(xiàn)之后,QQ空間的一篇文章引發(fā)了classloader熱修復(fù)方案的浪潮因痛,包括開源的Nuwa,HotFix,Tinker等饮醇,很有價值的一篇文章安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹。這篇文章比較清晰的闡述了pre-verify問題以及解決方案浪漠,但是我看完后還是有些疑惑的陕习,比如為什么當類A直接引用了類B后,就可以不被打上CLASS_VERIFIED標記址愿?這篇文章采用實踐加源碼的方式该镣,因為篇幅原因,具體的實踐操作過程可能不會特別詳細响谓,但是力求講清楚整個pre-verify的出現(xiàn)及解決過程损合,順便也可以了解到dalvik虛擬機的dexopt的大致流程。
盜個QQ空間的原理圖娘纷,原理很簡單,假設(shè)classes.dex中的Qzone.class有bug赖晶,我們通過動態(tài)加載patch.dex,并將patch.dex插入到Elements數(shù)組中,保證在classes.dex的前面捂贿。這樣一來,當出發(fā)Qzone.class的加載時厂僧,很明顯會加載到patch.class中的Qzone.class,而classes.dex中的Qzone.class是永遠加載不到的,從而達到熱修復(fù)的效果颜屠。從原理上分析沒有任何問題白魂,實踐一下看看:
首先,我們新建一個FixTest工程福荸,添加一個名為patch的module,核心代碼如下:
public static void inject(Context context,String dexPath){
try {
Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
Object originPathList=ReflectionUtils.getField(cl,DexUtils.class.getClassLoader(),"pathList");
Object originElements=ReflectionUtils.getField(originPathList.getClass(),originPathList,"dexElements");
String dexOpt=context.getDir("odex",0).getAbsolutePath();
DexClassLoader dexClassLoader=new DexClassLoader(dexPath,dexOpt,dexOpt,DexUtils.class.getClassLoader());
Object pathList=ReflectionUtils.getField(cl,dexClassLoader,"pathList");
Object elements=ReflectionUtils.getField(pathList.getClass(),pathList,"dexElements");
Object combineElements=combineArray(elements,originElements);
ReflectionUtils.setFeild(originPathList.getClass(),originPathList,"dexElements",combineElements);
Object object= ReflectionUtils.getField(originPathList.getClass(),originPathList,"dexElements");
Log.i("ljj", "inject->length: "+Array.getLength(object));
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
代碼就是實現(xiàn)了上圖中的原理背传,將dexPath對應(yīng)的patch包插入到了PathClassLoader的Elements的前面呆瞻。我們在app的module中,引入patch径玖,進行測試,首先在app的Application中加入
String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "patch.dex";
HotFix.inject(this, dexPath);
我們新建一個Test類痴脾,在MainActivity中用一個TextView顯示showText的結(jié)果,直接運行程序梳星,textView顯示“I am an error”赞赖。
public class Test {
public String showText(){
return "I am an error";
}
}
假設(shè)現(xiàn)在發(fā)現(xiàn)顯示錯了,我們要顯示的是“I am a patch”冤灾,按照上面所說的前域,我們可以修改Test類,然后打包命名為patch.dex進行下發(fā)即可韵吨。至于patch.dex的生成匿垄,有很多種方式,我們直接修改showText方法后归粉,執(zhí)行g(shù)radle build椿疗,然后將/app/build/intermediates/classes/debug/包名/Test.class文件隨便拷貝到一個目錄,在目錄中建立包級文件夾糠悼,假設(shè)頂層文件夾為dex届榄,里層文件夾為com/ljj/fixtest/Test.class,調(diào)用dx命令
dx --dex --output=patch.dex dex
將生成的patch.dex放到SDcard的根目錄。好的绢掰,至此一切準備工作都已經(jīng)完成痒蓬,我們在android5.0童擎,6.0滴劲,7.0上都能正常運行,但在android4.2的手機上當我們重啟時,報了這樣的異常:
01-02 00:56:37.674 11264-11264/com.ljj.fixtest E/AndroidRuntime: FATAL EXCEPTION: main
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
at com.ljj.fixtest.MainActivity$1.onClick(MainActivity.java:20)
at android.view.View.performClick(View.java:4299)
at android.view.View$PerformClick.run(View.java:17576)
at android.os.Handler.handleCallback(Handler.java:725)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:153)
at android.app.ActivityThread.main(ActivityThread.java:5356)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at
直譯就是一個被標為pre-verify的class引用了一個ref類顾复,這個ref類被發(fā)現(xiàn)不是期待的實現(xiàn)方式班挖,也就是被換掉了,去看一下異常拋出的位置以及如何調(diào)用到這個位置的芯砸。
在本例中萧芙,MainActivity中引用到了Test類的showText方法,執(zhí)行MainActivity的onCreate方法時會嘗試解析Test類假丧。MainActivity的onCreate方法很簡單双揪。
public class MainActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView textView=(TextView) findViewById(R.id.mytv);
textView.setText(new Test().showText());
}
}
我們看一下反編譯后onCreate的smali代碼渔期。
而解釋器執(zhí)行到new-instance時拘哨,會觸發(fā)倦青,最終會調(diào)用到dvmResolvedClass方法
HANDLE_OPCODE(OP_NEW_INSTANCE /*vAA, class@BBBB*/)
{
ClassObject* clazz;
Object* newObj;
EXPORT_PC();
vdst = INST_AA(inst);
ref = FETCH(1);
ILOGV("|new-instance v%d,class@0x%04x", vdst, ref);
clazz = dvmDexGetResolvedClass(methodClassDex, ref);
if (clazz == NULL) {
clazz = dvmResolveClass(curMethod->clazz, ref, false);
if (clazz == NULL)
GOTO_exceptionThrown();
}
dvmResolvedClass方法位于/dalvik/vm/oo/Resolve.cpp中产镐。
ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx,
bool fromUnverifiedConstant)
{
DvmDex* pDvmDex = referrer->pDvmDex;
ClassObject* resClass;
const char* className;
resClass = dvmDexGetResolvedClass(pDvmDex, classIdx);
if (resClass != NULL)
return resClass;
className = dexStringByTypeIdx(pDvmDex->pDexFile, classIdx);
if (className[0] != '\0' && className[1] == '\0') {
/* primitive type */
resClass = dvmFindPrimitiveClass(className[0]);
} else {
resClass = dvmFindClassNoInit(className, referrer->classLoader);
}
if (resClass != NULL) {
if (!fromUnverifiedConstant &&
IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED))
{
ClassObject* resClassCheck = resClass;
if (dvmIsArrayClass(resClassCheck))
resClassCheck = resClassCheck->elementClass;
if (referrer->pDvmDex != resClassCheck->pDvmDex &&
resClassCheck->classLoader != NULL)
{
LOGW("Class resolved by unexpected DEX:"
" %s(%p):%p ref [%s] %s(%p):%p",
referrer->descriptor, referrer->classLoader,
referrer->pDvmDex,
resClass->descriptor, resClassCheck->descriptor,
resClassCheck->classLoader, resClassCheck->pDvmDex);
LOGW("(%s had used a different %s during pre-verification)",
referrer->descriptor, resClass->descriptor);
dvmThrowIllegalAccessError(
"Class ref in pre-verified class resolved to unexpected "
"implementation");
return NULL;
}
}
.........
return resClass;
}
referrer是curMethod->clazz , 首先在dvmDexGetResolvedClass方法中判斷是否解析過該類,很明顯逃糟,該類是首次加載蓬豁,所以返回結(jié)果為空地粪,然后調(diào)用dvmFindClassNoInit方法用classloader去查找類蟆技,因為patch.dex已經(jīng)在之前反射注入到了elements中,所以此時resClass不為空旺聚,此時檢查MainActivity是否被打上了CLASS_ISPREVERIFIED砰粹,此時先給出結(jié)果碱璃,肯定是打上了的饭入,進而轉(zhuǎn)入到
if (referrer->pDvmDex != resClassCheck->pDvmDex && resClassCheck->classLoader != NULL)
這行代碼翻譯過來就是MainActivity所在的dex和Test所在的dex不是同一個且Test的類加載器不為空的情況下谐丢,就會拋出異常 "Class ref in pre-verified class resolved to unexpected implementation",現(xiàn)在大家都應(yīng)該清楚了這個異常具體的來源凭疮。
出現(xiàn)問題了执解,看看如何解決衰腌?盜用騰訊bugly的一張圖
當三個條件均滿足時右蕊,會拋出異常饶囚,解決方案大致上有以下四種萝风。
-
修改fromUnverfiedConstant=true
需要通過 native hook 攔截系統(tǒng)方法紫岩,更改方法的入口參數(shù)泉蝌,將 fromUnverifiedConstant 統(tǒng)一改為 true贪磺,風(fēng)險大粥鞋,幾乎無人采用瞄崇。 -
禁止dexopt過程打上CLASS_ISPREVERIFIED標記
Q-zone方案突破了此限制等浊,但是損失了性能摹蘑。 -
補丁類與引用類放在同一個dex中
Tinker等全量合成方案突破了此限制。 -
使dvmDexGetResolvedClass返回不為null过咬,直接返回
QFix的方案制妄,可參考這篇文章QFix探索之路—手Q熱補丁輕量級方案
各個方案都有各自的優(yōu)缺點耕捞,我們從學(xué)習(xí)的角度看,學(xué)習(xí)一下Q-zone方案的實現(xiàn)敞映。Q-zone方案的原理是在每個類的構(gòu)造方法中加入一行代碼,保證Hack.class在單獨的dex中振愿,選擇在構(gòu)造函數(shù)中進行可以不增加方法數(shù)埃疫。如下:
public class Test {
public Test() {
System.out.println(Hack.class);
}
}
我們從源碼的角度看一下栓霜,為什么加入了這行代碼胳蛮,每個插入的類中都不會打上CLASS_ISPREVERIFIED了仅炊。
dexopt的過程是分為verify+optimize兩個步驟進行的抚垄,對于每個類的verify+optimize方法是在verifyAndOptimizeClass方法中進行的呆馁,源碼位置在:
/dalvik/vm/analysis/DexPrepare.cpp
static void verifyAndOptimizeClass(DexFile* pDexFile, ClassObject* clazz,
const DexClassDef* pClassDef, bool doVerify, bool doOpt)
{
....
if (doVerify) {
if (dvmVerifyClass(clazz)) {
/*
* Set the "is preverified" flag in the DexClassDef. We
* do it here, rather than in the ClassObject structure,
* because the DexClassDef is part of the odex file.
*/
assert((clazz->accessFlags & JAVA_FLAGS_MASK) ==
pClassDef->accessFlags);
((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISPREVERIFIED;
verified = true;
} else {
// TODO: log when in verbose mode
LOGV("DexOpt: '%s' failed verification", classDescriptor);
}
}
if (doOpt) {
bool needVerify = (gDvm.dexOptMode == OPTIMIZE_MODE_VERIFIED ||
gDvm.dexOptMode == OPTIMIZE_MODE_FULL);
if (!verified && needVerify) {
LOGV("DexOpt: not optimizing '%s': not verified",
classDescriptor);
} else {
dvmOptimizeClass(clazz, false);
/* set the flag whether or not we actually changed anything */
((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISOPTIMIZED;
}
}
}
很清晰,dvmVerifyClass如果校驗通過了纺腊,該clazz就會被打上CLASS_ISPREVERIFIED標記誓沸。接下來我們主要看dvmVerifyClass方法都干了什么壹粟。源碼位置:/dalvik/vm/analysis/DexVerify.cpp
bool dvmVerifyClass(ClassObject* clazz)
{
int i;
if (dvmIsClassVerified(clazz)) {
LOGD("Ignoring duplicate verify attempt on %s", clazz->descriptor);
return true;
}
for (i = 0; i < clazz->directMethodCount; i++) {
if (!verifyMethod(&clazz->directMethods[i])) {
LOG_VFY("Verifier rejected class %s", clazz->descriptor);
return false;
}
}
for (i = 0; i < clazz->virtualMethodCount; i++) {
if (!verifyMethod(&clazz->virtualMethods[i])) {
LOG_VFY("Verifier rejected class %s", clazz->descriptor);
return false;
}
}
return true;
}
在verifyMethod中會對Method的各個字段進行驗證煮寡,篇幅原因薇组,不進行逐層源碼追蹤了坐儿,在verifyMethod方法中貌矿,會調(diào)用dvmVerifyCodeFlow方法逛漫,接著調(diào)用doCodeVerification酌毡,會具體分析每一條指令枷踏,執(zhí)行必要的解析及驗證旭蠕。對于每一條指令掏熬,是調(diào)用verifyInstruction方法來驗證的。verifyInstruction方法的源碼位置:/dalvik/vm/CodeVerify.cpp讶坯。在verifyInstruction中辆琅,注意這段代碼。
case OP_CONST_CLASS:
case OP_CONST_CLASS_JUMBO:
assert(gDvm.classJavaLangClass != NULL);
/* make sure we can resolve the class; access check is important */
resClass = dvmOptResolveClass(meth->clazz, decInsn.vB, &failure);
if (resClass == NULL) {
const char* badClassDesc = dexStringByTypeIdx(pDexFile, decInsn.vB);
dvmLogUnableToResolveClass(badClassDesc, meth);
LOG_VFY("VFY: unable to resolve const-class %d (%s) in %s",
decInsn.vB, badClassDesc, meth->clazz->descriptor);
assert(failure != VERIFY_ERROR_GENERIC);
} else {
setRegisterType(workLine, decInsn.vA,
regTypeFromClass(gDvm.classJavaLangClass));
}
break;
為什么要關(guān)注OP_CONST_CLASS,因為我們插入的System.out.println(Hack.class);會生成const-class的dalvik指令这刷,可以通過dexdump或者反編譯apk來查看婉烟,此時會觸發(fā)dvmOptResolveClass的調(diào)用。dvmOptResolveClass函數(shù)會去查找Hack.class,由于我們的dex沒有Hack.class,肯定查不到暇屋,拋異常返回似袁,此時這個類的dvmVerifyClass過程會返回false,這個類也就沒有打上CLASS_ISPREVERIFIED咐刨,而verified為false昙衅,導(dǎo)致也不會進行optimize過程。
值得說明的是如果類沒有打上CLASS_ISPREVERIFIED定鸟,那么verify+optimize都會在類第一次加載時dvmInitClass中進行而涉,正常情況下每個類的verify+optimize只會在安裝時dexopt中進行一次啼县,verify過程非常重,會對類的所有方法的所有指令都進行校驗,如果短時間內(nèi),大量的類進行verify,耗時是比較嚴重的,尤其在應(yīng)用剛啟動的時候送挑,有可能造成白屏司澎。
至于我們?nèi)绾尾迦隨ystem.out.println(Hack.class),我們可以采用transformAPI+javaassist進行實現(xiàn)蛤铜。實現(xiàn)過程注意兩點:
- Application不要插入Hack.class,因為application的構(gòu)造函數(shù)執(zhí)行時,我們還沒有注入hack.apk
- 在注入patch.dex前注入hack.apk,否則會找不到類
pre-verify方案驗證demo腐芍,很簡單,直接運行app,然后將patch.dex放到sdcard的根目錄下即可。
Demo地址:https://github.com/jjlan/FixTest
參考:
- Android熱補丁動態(tài)修復(fù)技術(shù)(一):從Dex分包原理到熱補丁
- Android Classloader熱修復(fù)技術(shù)之百家齊放
- 安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹
- QFix探索之路—手Q熱補丁輕量級方案
目前本人在公司負責熱修復(fù)相關(guān)的工作脑沿,主要是基于robust的熱修復(fù)相關(guān)工作韭邓。感興趣的同學(xué)歡迎進群交流诗力。