Android Classloader热修复技术之百家齐放

大概在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, &regTable))
        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中的方法。

权衡利弊,本文的方式更适合用于热修复,不过百家争鸣,百花齐放,存在即合理,不能随随便便对一种方式进行否定,就像当今的插件化技术,各有各的一套实现,也各自有各自的优缺点。

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值