Instant Run与热修复

  • 简介
    对Instant Run还不了解的可以阅读我上一篇文章 Instant Run浅析 。Instant Run与目前很多主流的热修复方案都使用了mutidex技术,andfix除外。其中微信tinker的方案也借鉴了Instant Run。更多热修复的介绍可参考HotFix原理介绍及使用总结。总的来说大致可以分成两类,如andfix的native hook方案,qq空间、微信、手q等的分dex方案。本文不涉及各方案的具体实现原理或孰优孰劣的话题,仅分析Instant Run是如何解决热修复遇到的问题的。
  • unexpected DEX异常
    上面提到的集中分dex方案都要解决的一个问题就是出现在5.0以前的unexpected DEX异常,被打上了preveryfied标识的类引用了别的dex的类时会抛出。假设我们app的dex为dex1,有两个类ClassA和ClassB,其中ClassA引用了ClassB。原app的所有Class都只引用到了本身dex所在的类,这样这些Class留都会被打上preverified标识。这时ClassB有bug,我们修复后生成了dex2,有ClassB’(为了区分,其实两个类名是一样的)并应用到了已发布的app上。当应用补丁时,ClassB被替换成了ClassB’。即来自dex1的ClassA引用了来自dex2的ClassB’,于是dalvik就会抛出异常。以下是dalvik vm的Resolve#dvmResolveClass相关代码:

    ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx,
    bool fromUnverifiedConstant)
    {
    resClass = dvmDexGetResolvedClass(pDvmDex, classIdx);
    if (resClass != NULL)
        return resClass;
    
        /*
         * If the referrer was pre-verified, the resolved class must come
         * from the same DEX or from a bootstrap class.  The pre-verifier
         * makes assumptions that could be invalidated by a wacky class
         * loader.  (See the notes at the top of oo/Class.c.)
         *
         * The verifier does *not* fail a class for using a const-class
         * or instance-of instruction referring to an unresolveable class,
         * because the result of the instruction is simply a Class object
         * or boolean -- there's no need to resolve the class object during
         * verification.  Instance field and virtual method accesses can
         * break dangerously if we get the wrong class, but const-class and
         * instance-of are only interesting at execution time.  So, if we
         * we got here as part of executing one of the "unverified class"
         * instructions, we skip the additional check.
         *
         * Ditto for class references from annotations and exception
         * handler lists.
         */
        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)
            {
                ALOGW("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);
                ALOGW("(%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;
            }
        }
        LOGVV("##### +ResolveClass(%s): referrer=%s dex=%p ldr=%p ref=%d",
            resClass->descriptor, referrer->descriptor, referrer->pDvmDex,
            referrer->classLoader, classIdx);
    
        /*
         * Add what we found to the list so we can skip the class search
         * next time through.
         *
         * TODO: should we be doing this when fromUnverifiedConstant==true?
         * (see comments at top of oo/Class.c)
         */
        dvmDexSetResolvedClass(pDvmDex, classIdx, resClass);
    }

    突破这个问题的关键点在于不要执行到这个if条件里去。qq空间为了解决这个问题用了插桩,即生成一个新dex,往所有类都插入一段代码来引用这个新dex的类,这样就可以防止类被打上preveryfied标识。手q原来用的也是这套方案,但现在转而对fromUnverifiedConstant变量下手了。在引用到补丁类之前先调用dvmResolveClass并将fromUnverifiedConstant设置为false,这样class就已经被缓存起来,当再次执行到dvmResolveClass时就可以直接取缓存返回了,走不到if条件里面。tinker则借鉴了instant run的模式,对整个dex文件进行替换,这样就不存在说被引用类所在的dex发生变化了。

  • Instant Run如何解决unexpected DEX问题
    刚才已经提到微信借鉴instant run的地方,dex替换,很好理解,但这只是instant run其中cold swap的部分。hot swap是又生成了一个新的dex而且没有替换旧dex的。

    回顾一下hot swap是如何实现的:一开始就在所有类都添加了IncrementalChange类型的变量change(在5.0之前IncrementalChange类和其他代码都在相同的一个dex里),随后生成$override结尾并实现了Incremental接口的补丁类,应用补丁时通过反射将补丁类实例化并赋值给change变量。这里有两个关键点,首先是通过ClassLoader来loadClass,然后是调用Class的newInstance方法来创建实例。从始至终都没有出现过补丁类,而是使用其接口IncrementalChange来代替。尝试结合使用qq空间和instant run这种创建实例的方式,发现是完全可行的。

    从loadClass方法跟进源码来看,整个过程都没用调用到dvmResolveClass来处理补丁类。最接近的也只是调用它来处理父类和接口。newInstance方法也是如此。其中loadClass的调用过程大致如下:
    BaseDexClassLoader#findClass
    -> DexPathList#findClass
    -> DexFile#loadClassBinaryName
    -> dalvik_system_DexFile#Dalvik_dalvik_system_DexFile_defineClassNative
    -> Class#dvmDefineClass
    -> Class#findClassNoInit
    -> Class#dvmLinkClass(resolve父类、接口)
    而dvmResolveClass方法最终也会调用上面出现的Class#findClassNoInit方法来加载类。所以可以判断通过loadClass来加载类是不会调用dvmResolveClass方法的。newInstance也一样,整个调用路径没出现dvmResolveClass。

    从dalvik字节码来看,会触发调用dvmResolveClass的字节码分别是:OP_CONST_CLASS、OP_CHECK_CAST、OP_INSTANCE_OF、OP_NEW_INSTANCE、OP_NEW_ARRAY、GOTO_TARGET。其中后面五种很快就能排除掉,不可能会出现这几种字节码操作补丁类。有疑惑的是OP_CONST_CLASS,它有两个操作数,一个是具体类的id,另一个是寄存器。作用是将一个类对象实例以到某个寄存器。第一感觉这个字节码应该会用到才是的。但从结合空间和instant run的实验结果来看,只有两种可能:fromUnverifiedConstant为true的方式先调用了dvmResolveClass,dvmResolveClass方法从来没被调用。而从我们刚才的分析来看,第一种是不太可能出现的。通过ClassLoader来加载类,它应该是不知道这个Class具体的类型的,所以估计用到OP_CONST_CLASS字节码时,类id并不是具体的补丁类,而是父类Class。这仅是个人的推测,如有错误欢迎指出。

    后来再通过instant run方式实例化补丁类的代码后面再通过new方式来实例化补丁类,还是会抛出unexpected DEX异常。第一种可能被排除,说明dvmResolveClass从未被调用。侧面验证了我的推测。

  • 总结
    Instant Run的cold swap采用dex替换来解决unexpected DEX异常,hot swap通过不直接使用补丁类,而是使用其在相同dex的接口的方式来解决此问题。

你可能感兴趣的:(Android)