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中的方法。

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

时间: 2024-08-06 16:06:42

Android Classloader热修复技术之百家齐放的相关文章

Android热修复技术选型

2015年以来,Android开发领域里对热修复技术的讨论和分享越来越多,同时也出现了一些不同的解决方案,如QQ空间补丁方案.阿里AndFix以及微信Tinker,它们在原理各有不同,适用场景各异,到底采用哪种方案,是开发者比较头疼的问题.本文希望通过介绍QQ空间补丁.Tinker以及基于AndFix的阿里百川HotFix技术的原理分析和横向比较,帮助开发者更深入了解热修复方案. 技术背景 一.正常开发流程 从流程来看,传统的开发流程存在很多弊端: 重新发布版本代价太大 用户下载安装成本太高 B

Android热修复技术选型——三大流派解析

2015年以来,Android开发领域里对热修复技术的讨论和分享越来越多,同时也出现了一些不同的解决方案,如QQ空间补丁方案.阿里AndFix以及微信Tinker,它们在原理各有不同,适用场景各异,到底采用哪种方案,是开发者比较头疼的问题.本文希望通过介绍QQ空间补丁.Tinker以及基于AndFix的阿里百川HotFix技术的原理分析和横向比较,帮助开发者更深入了解热修复方案. 技术背景 -----------------------------------------------------

Android 热修复技术(1)---原理

热修复技术分为几部分: 原理介绍 Android HotFix源码分析 自定义框架 1.Android分包MultiDex原理 首先Dex是什么东西? Dex就是Window里面的exe文件 也就是可执行问题. Android没有用传统的Java虚拟机,而是使用dalvik虚拟机.当APK安装到手机后,dalvik会先把de文件转化位ODEX文件, 优化结构. 在早期的android系统中,为了优化dex,所有的method会存放在一张表里面,表的大小位short,也就是65535(65K) B

全面了解Android热修复技术

WeTest 导读 本文探讨了Android热修复技术的发展脉络,现状及其未来. 热修复技术概述 热修复技术在近年来飞速发展,尤其是在InstantRun方案推出之后,各种热修复技术竞相涌现.国内大部分成熟的主流APP都拥有自己的热修复技术,像手淘.支付宝.QQ.饿了么.美团等等. 目前能搜集到的资料,大多简单罗列每个方案的特点并进行横向比较,而其中技术发展的脉络往往被掩盖了.热修复技术从何而来,又将往何处去?在这些资料中都找不到答案. 我认为,走马观花地看一遍各家的热修复方案并不能找到答案,所

5.9 热修复技术

热修复是在应用的App包发布到市场之后,出现了Bug,无需替换包来进行在线更新的一种技术,对用户是无感知的.目前广义上有两种方案可以实现代码的替换,一种是类的替换,基于Classloader:另一种是方法的替换,而这两种方式各有优缺点. 方法的替换:只能替换方法的内容,所以不能够对要patch的类进行方法的新增和删除:但同时,方法的替换可以在应用不重启的情况下实现.它包小.快速.功能单一.比较轻量,这种方案是热修复. 类的替换:可以修改类结构,功能更加的强大:但是必须要重启一次才会有效,因为已经

Android的热修复-微信Tinker

作者:邓浩宸 11/9/2016 1:13:49 PM Android的热修复 前言: 随着时代的发展,由于公司的项目需要去求变化平凡计划总赶不上变化,H5的高灵活性,开发周期短,更新速度快H5以及一些混合开发越来越被看好,然而主要原因之一:这种混合开发的方式容错率大,更新和修复BUG快.不用发布版本就可以让用户不觉的情况下就更新对应的内容或者BUG,我们不能否认混合开发的快捷,正在此前提下热修复和热更新技术也得到了非常大的发展,不管热修复还是热更新,都是对app的内容或者逻辑变化做出像web页

Android RocooFix 热修复框架

这里我要讲述下android热补丁前世今生 Under the Hood: Rebuilding Facebook for Android 发布者:Frank Qixing DU · 发布时间:2012年12月14日上午 3:01 Over the last year, we've been retooling our mobile apps to make them faster, more reliable, and easier to use. Several months ago, we

Android中热修复框架Robust原理解析+并将框架代码从&quot;闭源&quot;变成&quot;开源&quot;(下篇)

一.回顾框架原理 本篇继续来看热修复框架Robust原理,在之前的一篇文章中已经详细讲解了:Robust框架原理,因为这个框架不是开源的,所以通过官方给出的原理介绍,咋们自己模拟了案例和框架逻辑的简单实践.最后在通过反编译美团app进行验证咋们的逻辑实现是否大致不差.最终确定实践的逻辑大同小异.但是在上一篇文章末尾多次强调了,这个框架吸引我研究的不是他热修复技术,而是他有一个技术点,就是如何在编译期给每个类每个方法都加上修复功能代码,对于上层开发代码是透明的.因为从之前案例可以看到,如果方法没有

Android热修复技术专题:来自微信、淘宝、支付宝、QQ空间的热修复方案

最近好多人都讨论关于热更新的话题,所以查询了一些资料看看 当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App.测试.向各个应用市场和渠道换包.提示用户升级.用户下载.覆盖安装.有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布. 这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?答案当然是有的,那就是最近涌现出来得热补丁方案,主要包括淘宝的Dexpo