本篇文章继续上一篇,主要分析一下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
参考:
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android Classloader热修复技术之百家齐放
- 安卓App热补丁动态修复技术介绍
- QFix探索之路—手Q热补丁轻量级方案
目前本人在公司负责热修复相关的工作,主要是基于robust的热修复相关工作。感兴趣的同学欢迎进群交流。