本篇文章繼續上一篇,主要分析一下classloader方案在dalvik虛擬機中的pre-verify問題。關于classloader方案的原理可以參考上一篇文章android熱修復相關之Multidex解析進行了解。自從Multidex出現之后,QQ空間的一篇文章引發了classloader熱修復方案的浪潮,包括開源的Nuwa,HotFix,Tinker等,很有價值的一篇文章安卓App熱補丁動態修復技術介紹。這篇文章比較清晰的闡述了pre-verify問題以及解決方案,但是我看完后還是有些疑惑的,比如為什么當類A直接引用了類B后,就可以不被打上CLASS_VERIFIED標記?這篇文章采用實踐加源碼的方式,因為篇幅原因,具體的實踐操作過程可能不會特別詳細,但是力求講清楚整個pre-verify的出現及解決過程,順便也可以了解到dalvik虛擬機的dexopt的大致流程。
盜個QQ空間的原理圖,原理很簡單,假設classes.dex中的Qzone.class有bug,我們通過動態加載patch.dex,并將patch.dex插入到Elements數組中,保證在classes.dex的前面。這樣一來,當出發Qzone.class的加載時,很明顯會加載到patch.class中的Qzone.class,而classes.dex中的Qzone.class是永遠加載不到的,從而達到熱修復的效果。從原理上分析沒有任何問題,實踐一下看看:
首先,我們新建一個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();
}
}
代碼就是實現了上圖中的原理,將dexPath對應的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的結果,直接運行程序,textView顯示“I am an error”。
public class Test {
public String showText(){
return "I am an error";
}
}
假設現在發現顯示錯了,我們要顯示的是“I am a patch”,按照上面所說的,我們可以修改Test類,然后打包命名為patch.dex進行下發即可。至于patch.dex的生成,有很多種方式,我們直接修改showText方法后,執行gradle build,然后將/app/build/intermediates/classes/debug/包名/Test.class文件隨便拷貝到一個目錄,在目錄中建立包級文件夾,假設頂層文件夾為dex,里層文件夾為com/ljj/fixtest/Test.class,調用dx命令
dx --dex --output=patch.dex dex
將生成的patch.dex放到SDcard的根目錄。好的,至此一切準備工作都已經完成,我們在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類被發現不是期待的實現方式,也就是被換掉了,去看一下異常拋出的位置以及如何調用到這個位置的。
在本例中,MainActivity中引用到了Test類的showText方法,執行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代碼。
而解釋器執行到new-instance時,會觸發,最終會調用到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方法中判斷是否解析過該類,很明顯,該類是首次加載,所以返回結果為空,然后調用dvmFindClassNoInit方法用classloader去查找類,因為patch.dex已經在之前反射注入到了elements中,所以此時resClass不為空,此時檢查MainActivity是否被打上了CLASS_ISPREVERIFIED,此時先給出結果,肯定是打上了的,進而轉入到
if (referrer->pDvmDex != resClassCheck->pDvmDex && resClassCheck->classLoader != NULL)
這行代碼翻譯過來就是MainActivity所在的dex和Test所在的dex不是同一個且Test的類加載器不為空的情況下,就會拋出異常 "Class ref in pre-verified class resolved to unexpected implementation",現在大家都應該清楚了這個異常具體的來源。
出現問題了,看看如何解決?盜用騰訊bugly的一張圖
當三個條件均滿足時,會拋出異常,解決方案大致上有以下四種。
-
修改fromUnverfiedConstant=true
需要通過 native hook 攔截系統方法,更改方法的入口參數,將 fromUnverifiedConstant 統一改為 true,風險大,幾乎無人采用。 -
禁止dexopt過程打上CLASS_ISPREVERIFIED標記
Q-zone方案突破了此限制,但是損失了性能。 -
補丁類與引用類放在同一個dex中
Tinker等全量合成方案突破了此限制。 -
使dvmDexGetResolvedClass返回不為null,直接返回
QFix的方案,可參考這篇文章QFix探索之路—手Q熱補丁輕量級方案
各個方案都有各自的優缺點,我們從學習的角度看,學習一下Q-zone方案的實現。Q-zone方案的原理是在每個類的構造方法中加入一行代碼,保證Hack.class在單獨的dex中,選擇在構造函數中進行可以不增加方法數。如下:
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方法中,會調用dvmVerifyCodeFlow方法,接著調用doCodeVerification,會具體分析每一條指令,執行必要的解析及驗證。對于每一條指令,是調用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;
為什么要關注OP_CONST_CLASS,因為我們插入的System.out.println(Hack.class);會生成const-class的dalvik指令,可以通過dexdump或者反編譯apk來查看,此時會觸發dvmOptResolveClass的調用。dvmOptResolveClass函數會去查找Hack.class,由于我們的dex沒有Hack.class,肯定查不到,拋異常返回,此時這個類的dvmVerifyClass過程會返回false,這個類也就沒有打上CLASS_ISPREVERIFIED,而verified為false,導致也不會進行optimize過程。
值得說明的是如果類沒有打上CLASS_ISPREVERIFIED,那么verify+optimize都會在類第一次加載時dvmInitClass中進行,正常情況下每個類的verify+optimize只會在安裝時dexopt中進行一次,verify過程非常重,會對類的所有方法的所有指令都進行校驗,如果短時間內,大量的類進行verify,耗時是比較嚴重的,尤其在應用剛啟動的時候,有可能造成白屏。
至于我們如何插入System.out.println(Hack.class),我們可以采用transformAPI+javaassist進行實現。實現過程注意兩點:
- Application不要插入Hack.class,因為application的構造函數執行時,我們還沒有注入hack.apk
- 在注入patch.dex前注入hack.apk,否則會找不到類
pre-verify方案驗證demo,很簡單,直接運行app,然后將patch.dex放到sdcard的根目錄下即可。
Demo地址:https://github.com/jjlan/FixTest
參考:
目前本人在公司負責熱修復相關的工作,主要是基于robust的熱修復相關工作。感興趣的同學歡迎進群交流。