zoukankan      html  css  js  c++  java
  • 热修复原理

    参考

    https://liuwangshu.blog.csdn.net/article/details/79522200

    前言

    在Android应用开发中,热修复技术被越来越多的开发者所使用,也出现了很多热修复框架,比如:AndFix、Tinker、Dexposed和Nuwa等等。如果只是会这些热修复框架的使用那意义并不大,我们还需要了解它们的原理,这样不管热修复框架如何变化,只要基本原理不变,我们就可以很快的掌握它们。这一个系列不会对某些热修复框架源码进行解析,而是讲解热修复框架的通用原理。

    1.热修复的产生概述

    在开发中我们会遇到如下的情况:

    • 刚发布的版本出现了严重的bug,这就需要去解决bug、测试并打渠道包在各个应用市场上重新发布,这会耗费大量的人力物力,代价会比较大。
    • 已经改正了此前发布版本的bug,如果下一个版本是一个大版本,那么两个版本的间隔时间会很长,这样要等到下个大版本发布再修复bug,这样此前版本的bug会长期的影响用户。
    • 版本升级率不高,并且需要很长时间来完成版本覆盖,此前版本的bug就会一直影响不升级版本的用户。
    • 有一个小而重要的功能,需要短时间内完成版本覆盖,比如节日活动。

    为了解决上面的问题,热修复框架就产生了。对于Bug的处理,开发人员不要过于依赖热修复框架,在开发的过程中还是要按照标准的流程做好自测、配合测试人员完成测试流程。

    2.热修复框架的对比

    热修复框架的种类繁多,按照公司团队划分主要有以下几种:

    类别

    成员

    阿里系

    AndFixDexposed、阿里百川、Sophix

    腾讯系

    微信的TinkerQQ空间的超级补丁、手机QQQFix

    知名公司

    美团的Robust、饿了么的Amigo、美丽说蘑菇街的Aceso

    其他

    RocooFixNuwaAnoleFix

    虽然热修复框架很多,但热修复框架的核心技术主要有三类,分别是代码修复、资源修复 和 动态链接库修复,其中每个核心技术又有很多不同的技术方案,每个技术方案又有不同的实现,另外这些热修复框架仍在不断的更新迭代中,可见热修复框架的技术实现是繁多可变的。作为开发者需要了解这些技术方案的基本原理,这样就可以以不变应万变。

    部分热修复框架的对比如下表所示。

    特性

    AndFix

    Tinker/Amigo

    QQ空间

    Robust/Aceso

    即时生效

    方法替换

    类替换

    类结构修改

    资源替换

    so替换

    支持gradle

    支持ART

    支持Android7.0

    我们可以根据上表和具体业务来选择合适的热修复框架,当然上表的信息很难做到完全准确,因为部分的热修复框架还在不断更新迭代。
    从表中也可以发现Tinker和Amigo拥有的特性最多,是不是就选它们呢?也不尽然,拥有的特性多也意味着框架的代码量庞大,我们需要根据业务来选择最合适的,假设我们只是要用到方法替换,那么使用Tinker和Amigo显然是大材小用了。另外如果项目需要即时生效,那么使用Tinker和Amigo是无法满足需求的。对于即时生效,AndFix、Robust和Aceso都满足这一点,这是因为AndFix的代码修复采用了底层替换方案,而Robust和Aceso的代码修复借鉴了Instant Run原理。

     

    资源热修复

    很多热修复的框架的资源修复参考了Instant Run的资源修复的原理,因此我们首先要了解Instant Run是什么。

     

    Instant Run概述

    Instant Run是Android Studio2.0以后新增的一个运行机制,能够显著减少开发人员第二次及以后的构建和部署时间。

    在没有使用Instant Run前,我们编译部署应用程序的流程如图13-1所示。

    从图13-1可以看出,传统的编译部署需要重新安装App和重启App,这显然会很耗时,Instant Run会避免这一情况,如圈13-2所示。

     

    从图13-2可以看出Instant Run的构建和部署都是基于更改的部分的。

    Instant Run部署有三种方式,Instant Run会根据代码的情况来决定采用哪种部署方式,无论哪种方式都不需要重新安装App,这一点就已经提高了不少的效率。

    • Hot swap:从名称也可以看出Hot Swap是效率最高的部署方式,代码的增量改变不需要重启App,甚至不需要重启当前的Activity。修改一个现有方法中的代码时会采用Hot Swap。
    • Warm Swap: App不需重启,但是Activity需要重启。修改或删除一个现有的资源文件时会采用Warm Swap。
    • Cold Swap: App需要重启,但是不需要重新安装。采用Cold Swap的情况很多,比如添加、删除或修改一个字段和方法、添加一个类等。

     

    Instant Run的资源修复

    既然很多热修复的框架资源修复参考了Instant Run的资源修复原理,那么我们了解Instant Run的资源修复原理就可以了。

    Instant Run并不是Android的源码,需要通过反编译获取,可以以参考相关书籍。

     

    Instant Run资源修复的核心逻辑在MonkeyPatcher的monkeyPatchExistingResources方法中,如下所示:

    com/android/tools/fd/runtime/MonkeyPatcher,java

    public static void monkeyPatchExistingResources(Context context, 
        String externalResourceFile, Collection<Activity> activities)  {
        
        if (externalResourceFile==null) {
            return;
        }
    
        try {
            //创建一个新的AssetManager
            AssetManager newAssetManager = (AssetManager)AssetManager.class.getConstructor(new Class[0]).newInstance(new Object[0]);//1
            Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", new Class[]{String.class})//2
            mAddAssetPath.setAccessible(true);
            //通过反射调闸addAssetPath方法加载外部的资源(SD卡)
            if(((Integer)mAddAssetPath.invoke(newAssetManager, new Object[]{externalResourceFile})).intValue()==0) {//3
                throw new IllegalStateException("Coulrl not create new aosetManacjer"),
            }
            Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks",  new Class[0]);
            mEnsureStringBlocks.setAccessible(true);
            mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
    
    
            if(activities != null) {
                for(Activity activity : activities) {
                    Resources resources = activity.getResources();//4
                    try{
                        //反射得到Resources的AssetManager类型的mAssets字段
                        Field mAssets = Resources.class.getDeclaredField("mAssets");//5
                        mAssets.setAccessible(true);
    
                        //将mAssets字段的引用替换为新创建的AssetManager
                        mAssets.set(resources, newAssetManager); //6
                    } catch (Throwable ignore){
                        ...
                    }
    
                    //得到Activity的Resources.Theme
                    Resources.Theme theme = activity.getTheme();
                    try{
                        try{
                            //反射得到Resources.Theme的mAssets字段
                            Field ma = Resources.Theme.class.getDeclaredField("mAssets");
                            ma.setAccessible(true);
    
                            //将Resources.Theme的mAssets替换为newAssetManager
                            ma.set(theme, newAssetManager);//7
                        } catch(NoSuchFieldException ignore) {
                            ...
                        }
                        ...
                    } catch(Throwable e) {
                        Log.e("InstantRun","Failed to update existing theme for activity activity, e);
                    }
                    pruneResourceCaches(resources);
                }
            } // if(activities != null)结束
    
    
            //根据 SDK版本的不同, 用不同的方式得到 Resources 的弱引用集合
            Collection<WeakReferenee<Resources> references;
            if(Build.VERSION.SDK_INT >= 19) {
                Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
                Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance", new Class[0]);
                mGetInstance.setAccessible(true);
                Object resourcesManager = mGetInstance.invoke(null, new Object[0]);
                try {
                    Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources" ) ,
                    fMActiveResources.setAccessible(true);
                    ArrayMap<?, WeakReference<Resources>> arrayMap = (ArrayMap)fMActiveResources.get(resourcesManager);
                    references = arrayMap.values();
                } catch(NoSuchFieldException ignore) {
                    Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
                    mResourceReferences.setAccessible(true);
                    references = (Collection)mResourceReferences.get(resourcesManager);
                }
            } else {
                Class<?> activityThread = Class.forName("android.app.ActivityThread");
                Field fMActiveResources = activityThread.getDeclaredField("mActiveResources") ;
                fMActiveResources.setAccessible(true);
                Object thread = getActivityThread(context, activityThread);
                HashMap<?, WeakReference<Resources>> map = (HashMap)fMActiveResources.get(thread);
                references = map.values ();
            }
    
            //遍历并得到弱引用集合中的 Resources, 将 Resources的 mAssets 字段引用替换成新的 AssetManager
            for(WeakReference<Resources> wr : references) {
                Resources resources = (Resources)wr.get();
                if (resources != null) {
                    try {
                        Field mAssets = Resources.class.getDeclaredField("mAssets");
                        mAssets.setAccessible(true);
                        mAssets.set(resources, newAssetManager);
                    } catch (Throwable ignore) {
                        ...
                    }
                    resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrices());
                }
            }
        } catch (Throwable e) {
            throw new IllegalStateException(e);
        }
    }
    • 在注释1处创建一个新的AssetManager,在注释2和注释3处通过反射调用addAssetPath方法加载外部(SD卡)的资源。
    • 在注释4处遍历Activity列表,得到每个Activity的Resources,在注释5处通过反射得到Resources的AssetManager类型的mAssets字段,并在注释6处改写mAssets字段的引用为新的AssetManager。
    • 采用同样的方式,在注释7处将Resources.Theme的mAssets字段的引用替换为新创建的AssetManager。
    • 紧接着根据SDK版本的不同,用不同的方式得到Resources的弱引用集合,再遍历这个弱引用集合,将弱引用集合中的Resources的mAssets字段引用都替换成新创建的AssetManager。

    可以看出Instant Run中的资源热修复可以简单地总结为两个步骤:

    (1)创建新的AssetManager,通过反射调用addAssetPath方法加载外部的资源,这样新创建的AssetManager就含有了外部资源。

    (2)将AssetManager类型的mAssets字段的引用全部替换为新创建的AssetManager。

    代码修复

    代码修复主要有三个方案,分别是底层替换方案、类加载方案和Instant Run方案。

    类加载方案

    类加载方案基于Dex分包方案,什么是Dex分包方案呢?这个得先从65536限制和LinearAlloc限制说起。

    65536限制

    随着应用功能越来越复杂,代码量不断地增大,引入的库也越来越多,可能会在编译时提示如下异常:

    com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536

    这说明应用中引用的方法数超过了最大数65536个。

    产生这一问题的原因就是系统的65536限制,65536限制的主要原因是DVM Bytecode的限制,DVM指令集的方法调用指令invoke-kind索引为16bits,最多能引用 65535个方法。

    LinearAlloc限制

    在安装时可能会提示INSTALL_FAILED_DEXOPT。产生的原因就是LinearAlloc限制,DVM中的LinearAlloc是一个固定的缓存区,当方法数过多超出了缓存区的大小时会报错。

    为了解决65536限制和LinearAlloc限制,从而产生了Dex分包方案。

    Dex分包方案主要做的是在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其他代码放到次Dex中。

    当应用启动时先加载主Dex,等到应用启动后再动态的加载次Dex,从而缓解了主Dex的65536限制和LinearAlloc限制。

    类加载方案

    Dex分包方案主要有两种,分别是Google官方方案、Dex自动拆包和动态加载方案。因为Dex分包方案不是本章的重点,这里就不再过多的介绍,我们接着来学习类加载方案。

    在Android中的ClassLoader中讲到了ClassLoader的加载过程,其中一个环节就是调用DexPathList的findClass的方法,如下所示。

    libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {//1
            Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
            if (clazz != null) {
                return clazz;
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

    Element内部封装了DexFile,DexFile用于加载dex文件,因此每个dex文件对应一个Element

    多个Element组成了有序的Element数组dexElements。

    当要查找类时,

    • 会在注释1处遍历Element数组dexElements(相当于遍历dex文件数组),
    • 注释2处调用Element的findClass方法,其方法内部会调用DexFile的loadClassBinaryName方法查找类。

    如果在Element中(dex文件)找到了该类就返回,如果没有找到就接着在下一个Element中进行查找。

    根据上面的查找流程,我们将有bug的类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar,放在Element数组dexElements的第一个元素,这样会首先找到Patch.dex中的Key.class去替换之前存在bug的Key.class,排在数组后面的dex文件中的存在bug的Key.class根据ClassLoader的双亲委托模式就不会被加载,这就是类加载方案,如下图所示。

    类加载方案需要重启App后让ClassLoader重新加载新的类,为什么需要重启呢?

    是因为类在app运行时是无法被卸载的,因此要想重新加载新的类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的

    虽然很多热修复框架采用了类加载方案,但具体的实现细节和步骤还是有一些区别的,比如

    • QQ空间的超级补丁和Nuwa是按照上面说得将补丁包放在Element数组的第一个元素得到优先加载。
    • 微信Tinker将新旧apk做了diff,得到patch.dex,然后将patch.dex与手机中apk的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex放在Element数组的第一个元素。
    • 饿了么的Amigo则是将补丁包中每个dex 对应的Element取出来,之后组成新的Element数组,在运行时通过反射用新的Element数组替换掉现有的Element 数组。

    采用类加载方案的主要是以腾讯系为主,包括微信的Tinker、QQ空间的超级补丁、手机QQ的QFix、饿了么的Amigo和Nuwa等等。

    底层替换方案

    与类加载方案不同的是,底层替换方案不会再次加载新类,而是直接在Native层修改原有类,由于是在原有类进行修改限制会比较多,不能够增减原有类的方法和字段,如果我们增加了方法数,那么方法索引数也会增加,这样访问方法时会无法通过索引找到正确的方法,同样的字段也是类似的情况。

    底层替换方案和反射的原理有些关联,就拿方法替换来说,方法反射我们可以调用java.lang.Class.getDeclaredMethod,假设我们要反射Key的show方法,会调用如下所示。

    Key.class.getDeclaredMethod("show").invoke(Key.class.newInstance());

    Android 8.0的invoke方法,如下所示。

    libcore/ojluni/src/main/java/java/lang/reflect/Method.java

        @FastNative
        public native Object invoke(Object obj, Object... args)
                throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;

    invoke方法是个native方法,对应Jni层的代码为:

    art/runtime/native/java_lang_reflect_Method.cc

    static jobject Method_invoke(JNIEnv* env, jobject javaMethod, jobject javaReceiver,
                                 jobject javaArgs) {
      ScopedFastNativeObjectAccess soa(env);
      return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);

    Method_invoke函数中又调用了InvokeMethod函数:

    art/runtime/reflection.cc

    jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
                         jobject javaReceiver, jobject javaArgs, size_t num_frames) {
    ...
      ObjPtr<mirror::Executable> executable = soa.Decode<mirror::Executable>(javaMethod);
      const bool accessible = executable->IsAccessible();
      ArtMethod* m = executable->GetArtMethod();//1
    ...
    }

    注释1处获取传入的javaMethod(Key的show方法)在ART虚拟机中对应的一个ArtMethod指针,ArtMethod结构体中包含了Java方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等等,

    ArtMethod结构如下所示。

    art/runtime/art_method.h

    class ArtMethod FINAL {
        ...
        protected:
        GcRoot<mirror::Class> declaring_class_;
        std::atomic<std::uint32_t> access_flags_;
        uint32_t dex_code_item_offset_;
        uint32_t dex_method_index_;
        uint16_t method_index_;
        uint16_t hotness_count_;
        struct PtrSizedFields {
            ArtMethod** dex_cache_resolved_methods_;//1
            void* data_;
            void* entry_point_from_quick_compiled_code_;//2
        } ptr_sized_fields_;
    }

    ArtMethod结构中比较重要的字段是注释1处的dex_cache_resolved_methods_和注释2处的entry_point_from_quick_compiled_code_,它们是方法的执行入口,当我们调用某一个方法时(比如Key的show方法),就会取得show方法的执行入口,通过执行入口就可以跳过去执行show方法。

    替换ArtMethod结构体中的字段或者替换整个ArtMethod结构体,这就是底层替换方案。

    AndFix采用的是替换ArtMethod结构体中的字段,这样会有兼容问题,因为厂商可能会修改ArtMethod结构体,导致方法替换失败。Sophix采用的是替换整个ArtMethod结构体,这样不会存在兼容问题。

    底层替换方案直接替换了方法,可以立即生效不需要重启。采用底层替换方案主要是阿里系为主,包括AndFix、Dexposed、阿里百川、Sophix。

    Instant Run方案

     除了资源修复,代码修复同样也可以借鉴Instant Run的原理, 可以说Instant Run的出现推动了热修复框架的发展。

    Instant Run在第一次构建apk时,使用ASM在每一个方法中注入了类似如下的代码:

    IncrementalChange localIncrementalChange = $change;//1
    if (localIncrementalChange != null) {//2
        localIncrementalChange.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[] { this, paramBundle });
        return;
    }

    其中注释1处是一个成员变量localIncrementalChange ,它的值为$change,$change实现了IncrementalChange这个抽象接口。

    当我们点击InstantRun时,如果方法没有变化则$change为null,就调用return,不做任何处理。如果方法有变化,就生成替换类,

    这里我们假设MainActivity的onCreate方法做了修改,就会生成替换类MainActivity$override,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的$change设置为MainActivity$override,因此满足了注释2的条件,会执行MainActivity$override的access$dispatch方法,accessdispatch方法中会根据参数"onCreate.(Landroid/os/Bundle;)V"执行'MainActivityoverride`的onCreate方法,从而实现了onCreate方法的修改。

    借鉴Instant Run的原理的热修复框架有Robust和Aceso。

    什么是ASM?

    ASM是一个Java字节码操控框架,它能够动态生成类或者增强现有类的功能。ASM可以直接产生class文件,也可以再类被加载到虚拟机之前动态改变类的行为。

    动态链接库的修复

    Android平台的动态链接库主要指的是so库,所以简称动态链接库为so。

    热修复的so修复主要是更新so,换句话说就是重新加载so,因此so的修复的基础原理就是加载so。

    so修复主要有两个方案:

    1. 将so补丁插入到NativeLibraryElement数组的前部,让so补丁的路径先被返回和加载。

      这和13.3.1节讲到的DexPathList的findClass方法类似,在NativcLibraryElement数组中的每一个NativeLibraryElernent对应一个so库,在注释1处调用NativeLibraryElement的findNativeLiorary方法就可以返叫so的路径。

      上面的代码结合13.3.1节的类加载方案,就可以得到so的修复的一种方案,就是将so补丁插入到NativeLibraryElement数组的前部,让so补丁的路径先被返回,并调用Runtime的cloLoad方法进行加载,在doLoad方法中会

    2. 调用System的load方法来接管so的加载入口。

  • 相关阅读:
    误加all_load引起的程序报错
    ConstraintLayout 约束布局
    前端判断是否APP客户端打开触屏,实现跳转APP原生组件交互之遐想
    TP5 多条件whereOr查询
    json手动解析详解
    Centos python 2.6 升级到 2.7
    js中click重复执行
    使用 Python 实现实用小工具
    使用 Python 编写密码爆破工具
    使用Python进行无线网络攻击
  • 原文地址:https://www.cnblogs.com/muouren/p/11741364.html
Copyright © 2011-2022 走看看