大概在2015年10月底,QQ空间发了一篇叫《安卓App热补丁动态修复技术介绍》的文章,文章中提到为了能让Class进行热修复,其中一个条件就是防止类被打上CLASS_ISPREVERIFIED标记,具体的做法便是让一个Dex引用另一个Dex(hack.apk)中的空类(为了让业务无感知,需要在编译时动态注入字节码),并且在应用程序Application类起来的时候要加载这个hack.apk。也就是说最多需要进行两次反射,即加载hack.apk的时候需要进行一次反射操作,将hack.apk加入到DexElements中去,当有patch下发的时候,还要进行一次反射操作,将patch.apk加入到DexElements中去。虽说现在的手机已经很高级,但在应用起来的时候做两次反射,对性能要求高的有时候还是无法接受。
而在不久前,在一个插件化的微信群里,一位大神说,可以不使用hack.apk就可以做到同样的效果,他们至今是单Dex模式,并且他们的方案在QQ空间文章发出来之前便已经实现了。那么具体的实现是如何呢?
他们的做法很简单,注入字节码依旧是少不了的,只不过注入的字节码的内容发生了变化,从原来的引用另一个Dex中的Hack.class空类,修改成了引用系统的一个类。
原来注入的字节码内容如下:
if (Boolean.FALSE.booleanValue()){
System.out.println(com.to.package.Hack.class);
}
而现在注入的字节码内容修改成了如下
if (Boolean.FALSE.booleanValue()){
System.out.println(com.android.internal.util.Predicate.class);
}
可以看到,在这段永远不可能执行到的if语句中,唯一的区别就是打印的那个class发生了变化,由Hack.class修改成了com.android.internal.util.Predicate系统类,那么这个类是干嘛用的呢,为什么选择这个类呢?
先来看看这个类的内容:
package com.android.internal.util;
/**
* A Predicate can determine a true or false value for any input of its
* parameterized type. For example, a {@code RegexPredicate} might implement
* {@code Predicate<String>}, and return true for any String that matches its
* given regular expression.
* <p/>
* <p/>
* Implementors of Predicate which may cause side effects upon evaluation are
* strongly encouraged to state this fact clearly in their API documentation.
*/
public interface Predicate<T> {
boolean apply(T t);
}
很简单的一个泛型类,从注释中可以看到,这个类可以用于断言一些内容,比如我需要判断一个字符串是否满足某个正则,如果满足的话就在apply中返回true。并且这个类十分简单,这也是选择这个类的原因之一,因为这个类小,不复杂。还有一个原因就是这个类在API 8开始就一直存在,并且一直延续到最新版的Android系统,该类也没有被删除,还是com.android.internal.util包下唯一对上层开发者可见的一个类。
这样就完事了,就可以打patch了?当然不是,如果这样就完事了,岂不是和直接引用系统的类没有区别了,还需要在项目中定义一个同样的类,并且这个类不需要注入这段字节码。这样,就存在两个这样的类,一个是我们自己app定义的,另一个是系统中存在的。
于是,在application中我们再也不需要反射加载hack.apk了,直接加载patch.apk即可进行热修复,节省了一次反射插入Dex到DexElements中的时间。
那么这之中的原理是什么呢?为什么这么做可以达到热修复的目的呢。
其实,本质还是一样的,这么做也可以防止Class被打上CLASS_ISPREVERIFIED标记,让我们一起扒一扒源码。
在App安装的时候,apk中的classes.dex会被虚拟机(dexopt)优化成odex文件,然后才会拿去执行。其中会执行到C/C++层的一个rewriteDex函数,该函数关键内容如下:
static bool rewriteDex(u1* addr, int len, u4* pHeaderFlags,
DexClassLookup** ppClassLookup)
{
//省略n行代码...
if (!loadAllClasses(pDvmDex))
goto bail;
verifyAndOptimizeClasses(pDvmDex->pDexFile, doVerify, doOpt);
//省略n行代码
bail:
//省略n行代码
return result;
}
会先调用loadAllClasses函数加载所有class到内存中,该函数中会对加载的类遍历进行判断是否重复定义了,即app中是否定义了一个和系统一样的类,函数内容如下:
static bool loadAllClasses(DvmDex* pDvmDex)
{
//省略n行代码
for (idx = 0; idx < count; idx++) {
//省略n行代码
newClass = dvmFindSystemClassNoInit(classDescriptor);
if (newClass == NULL) {
//省略n行代码
} else if (newClass->pDvmDex != pDvmDex) {
//在这里进行了重复定义的校验,即app中的Predicate类和系统中的Predicate类重复定义了,会被标记成CLASS_MULTIPLE_DEFS
/*
* We don‘t load the new one, and we tag the first one found
* with the "multiple def" flag so the resolver doesn‘t try
* to make it available.
*/
LOGD("DexOpt: ‘%s‘ has an earlier definition; blocking out\n",
classDescriptor);
SET_CLASS_FLAG(newClass, CLASS_MULTIPLE_DEFS);
} else {
//省略n行代码
}
}
//省略n行代码
return true;
}
上面这个函数中,会进行了重复定义的校验,即app中的Predicate类和系统中的Predicate类重复定义了,于是app中的Predicate类就会被标记成CLASS_MULTIPLE_DEFS。
接着会执行到verifyAndOptimizeClass函数,该函数内容如下
static void verifyAndOptimizeClass(DexFile* pDexFile, ClassObject* clazz,
const DexClassDef* pClassDef, bool doVerify, bool doOpt)
{
const char* classDescriptor;
bool verified = false;
//这里会进行一次判断,如果重复定义,则输出log
if (clazz->pDvmDex->pDexFile != pDexFile) {
/*
* The current DEX file defined a class that is also present in the
* bootstrap class path. The class loader favored the bootstrap
* version, which means that we have a pointer to a class that is
* (a) not the one we want to examine, and (b) mapped read-only,
* so we will seg fault if we try to rewrite instructions inside it.
*/
ALOGD("DexOpt: not verifying/optimizing ‘%s‘: multiple definitions",
clazz->descriptor);
return;
}
classDescriptor = dexStringByTypeIdx(pDexFile, pClassDef->classIdx);
/*
* First, try to verify it.
*/
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
ALOGV("DexOpt: ‘%s‘ failed verification", classDescriptor);
}
}
//opt操作
if (doOpt) {
bool needVerify = (gDvm.dexOptMode == OPTIMIZE_MODE_VERIFIED ||
gDvm.dexOptMode == OPTIMIZE_MODE_FULL);
if (!verified && needVerify) {
ALOGV("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;
}
}
}
函数刚开始同样会进行一次校验,如果发现clazz->pDvmDex->pDexFile != pDexFile,就说明当前校验的类存在重复定义,输出了一行log,log内容为DexOpt: not verifying/optimizing Lcom/android/internal/util/Predicate: multiple definitions ;并且对当前类停止校验和优化。
Predicate类会被强制return停止校验,那么其他类呢?如果虚拟机启动的时候设置了doVerify为true,那么就会去执行dvmVerifyClass函数。该函数内容如下:
bool dvmVerifyClass(ClassObject* clazz)
{
int i;
if (dvmIsClassVerified(clazz)) {
ALOGD("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函数进行校验,即directMethods和virtualMethods方法,而这个函数的内部会进行一次code-flow analysis,简单来说就是对每个方法的字节码进行一次分析,如下
static bool verifyMethod(Method* meth)
{
bool result = false;
//此处省略n行代码
/*
* Do code-flow analysis.
*
* We could probably skip this for a method with no registers, but
* that‘s so rare that there‘s little point in checking.
*/
if (!dvmVerifyCodeFlow(&vdata)) {
//ALOGD("+++ %s failed code flow", meth->name);
goto bail;
}
success:
result = true;
bail:
//此处省略n行代码
return result;
}
继续调用到dvmVerifyCodeFlow函数中去:
/*
* Entry point for the detailed code-flow analysis of a single method.
*/
bool dvmVerifyCodeFlow(VerifierData* vdata)
{
bool result = false;
//此处省略n行代码
/*
* Run the verifier.
*/
if (!doCodeVerification(vdata, ®Table))
goto bail;
//此处省略n行代码
/*
* Success.
*/
result = true;
bail:
//此处省略n行代码
return result;
}
继续跟踪到doCodeVerification函数:
static bool doCodeVerification(VerifierData* vdata, RegisterTable* regTable)
{
//此处省略n行代码
/*
* Continue until no instructions are marked "changed".
*/
while (true) {
//此处省略n行代码
if (!verifyInstruction(meth, insnFlags, regTable, insnIdx,
uninitMap, &startGuess))
{
//ALOGD("+++ %s bailing at %d", meth->name, insnIdx);
goto bail;
}
//此处省略n行代码
}
//此处省略n行代码
result = true;
bail:
return result;
}
一直跟踪到verifyInstruction函数,verifyInstruction函数中有一个switc分支,当校验到我们注入的那段字节码的时候,会进入到 case OP_CONST_CLASS:分支。
那么什么时候会进入这个分支呢,简单的说就是遇到了const_class字节码操作的时候,这个字节码在什么时候会触发呢,使用apktool反编译一下我们注入字节码的类可以发现,System.out.println打印的内容里面,就有一段const_class字节码
# direct methods
.method static constructor <clinit>()V
.locals 2
sget-object v0, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;
invoke-virtual {v0}, Ljava/lang/Boolean;->booleanValue()Z
move-result v0
if-eqz v0, :cond_0
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
//这个字节码会触发该Switch语句OP_CONST_CLASS分支
const-class v1, Lcom/android/internal/util/Predicate;
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/Object;)V
:cond_0
return-void
.end method
翻一下文档可以发现OP_CONST_CLASS操作符的作用就是
Move a reference to the class specified by the given index into the specified register. In the case where the indicated type is primitive, this will store a reference to the primitive type’s degenerate class.
简单的这么理解
根据指定的索引将一个指向class的引用保存在一个特定的寄存器,如果是基本数据类型,会指向它的包装类型。
verifyInstruction函数的内容如下:
static bool verifyInstruction(const Method* meth, InsnFlags* insnFlags,
RegisterTable* regTable, int insnIdx, UninitInstanceMap* uninitMap,
int* pStartGuess)
{
//此处省略n行代码
switch (decInsn.opcode) {
//此处省略n行代码
case OP_CONST_CLASS:
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;
//此处省略n行代码
}
//此处省略n行代码
}
最终调用到了dvmOptResolveClass函数中去拿到一个ClassObject对象
ClassObject* dvmOptResolveClass(ClassObject* referrer, u4 classIdx,
VerifyError* pFailure)
{
DvmDex* pDvmDex = referrer->pDvmDex;
ClassObject* resClass;
//此处省略n行代码
/*
* Check the table first. If not there, do the lookup by name.
*/
resClass = dvmDexGetResolvedClass(pDvmDex, classIdx);
/* multiple definitions? */
if (IS_CLASS_FLAG_SET(resClass, CLASS_MULTIPLE_DEFS)) {
ALOGI("DexOpt: not resolving ambiguous class ‘%s‘",
resClass->descriptor);
if (pFailure != NULL)
*pFailure = VERIFY_ERROR_NO_CLASS;
return NULL;
}
//此处省略n行代码
return resClass;
}
该函数中通过dvmDexGetResolvedClass函数拿到了class,这个class就是最开始的app中被终止校验和优化的Predicate类,并且这个类由于被标记成了重复定义,执行到这里的时候,就会被认为是一个模糊不清的概念,因为app中有一个,系统中有一个,不知道使用哪一个,这时候就会直接终止校验,返回VERIFY_ERROR_NO_CLASS,一直会返回到最开始调用的verifyAndOptimizeClass函数中去,并且会输出log,内容为DexOpt: ‘{classDescriptor}’ failed verification, 这时候verified变量会被标记成false,并且由于校验失败,opt操作也可能被终止,会输出log,内容为DexOpt: ‘{classDescriptor}’ failed verification。最终所有注入了字节码的类都没有打上CLASS_ISPREVERIFIED标记,也就达到了QQ空间文章中的条件,即防止类被打上标记。
最后说下一些额外的东西,如果刚刚的const-class操作符指向的class不存在,就会扔出一个ClassNotFoundException异常。
总结一下这么做的好处及坏处:
- 好处就是节约一次反射时间,毕竟是在app启动的时候,能节约多少时间就节约多少时间,如果你的app没有使用multidex,那么app就是单dex,这种情况下无需重复引入hack.apk这个dex来额外达到热修复的目的。
- 坏处就是com.android.internal.util.Predicate这个类如果在高版本中删除了或者被国内的rom定制厂商删除了,那么就坑爹了。不过一般rom不会把这个类去除,因为一旦去除,google的CTS测试就过不了,这是一个公开的sdk中的方法。
权衡利弊,本文的方式更适合用于热修复,不过百家争鸣,百花齐放,存在即合理,不能随随便便对一种方式进行否定,就像当今的插件化技术,各有各的一套实现,也各自有各自的优缺点。