精华内容
下载资源
问答
  • 热修复

    2021-03-21 15:23:52
    插件化Android热修复常用热修复解决方案实现原理什么是热修复?怎么进行热修复热修复需要解决的问题插桩式热修复落地怎么执行热修复(使用补丁包)?字节码插桩 Android热修复 常用热修复解决方案实现原理 什么是...

    Android热修复

    常用热修复解决方案实现原理

    什么是热修复?

    定义:在我们应用上线后出现bug需要及时修复时,不用再发新的安装包,只需要发布补丁包,在客户无感知下修复掉bug。

    怎么进行热修复?

    开发端:生成补丁包 -Gradle插件patch
    服务端:补丁包管理
    用户端:执行热修复

    为什么类替换方案
    —不能及时生效?

    热修复需要解决的问题

    • 开发端:

      • 补丁包是什么?
      • 如何生成补丁包?
      • 开启混淆后呢?
      • 对比改动自动生成补丁包(gradle)?
    • 用户端

      • 什么时候执行热修复?
        • 越早越好要在有bug类之前执行,application 里面;
        • 如果已经加载过,就修复不了了,因为ClassLoader有缓存了;
      • 怎么执行热修复(使用补丁包)?
      • Android版本兼容问题?

      插桩式热修复落地

      热修复方案很多,比较出名的下面四个要记住腾讯Tinker,阿里的AndFix,美团的Robust,以及Qzone
      比较出名的热修复方案

    • AndFix native动态替换java层方法,native层hook java层代码;
      从补丁包加载类,Test.class,通过反射拿到test方法和注解,JNI层替换到补丁包里面方法的属性,

    public class Test{
        @MethodReplace(class= "xxx.xxx.Test",method= "test")
        public void test(){
        //...
        }
    }
    
    • Robus 热修复解决方案:
      • 同样提供Gradle插件
      • 对每个产品代码在每个函数都在编译打包阶段自动插入一段代码;
      //State.java的getIndex函数
      public long getIndex() {
            return 100;
      }
      public static ChangeQuickRedirect changeQuickRedirect;
      public long getIndex() {
            //相当于插桩了开关,当补丁包通过类加载反射实例化,给这个静态changeQuickRedirect 赋值,就走if语句,否则开关关闭;
            if(changeQuickRedirect != null) {
            //PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数
                  if(PatchProxy.isSupport(new Object[0], this,   changeQuickRedirect, false)) {
                      return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
                  }
            }
            return 100L;
      }
      
      

    可 以看到Robust为每个class增加了个类型为ChangeQuickRedirect的静态成员,而在每个方法前都插入了使用changeQuickRedirect相关的逻辑,当 changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的。

    • Tinker
      bsdiff 作用:增量更新(今日头条/抖音都有存在)
      用法:
      1. bsdiff 1.txt 2.txt patch //得到patch差分包
      2. bspatch 1.txt 2.txt patch //下载patch和原包合并
      DexDifff

    Tinker 通过计算对比指定的Base APK中的dex和修改之后的APK中的Dex区别,补丁包中的内容就是两者差分的描述。运行时将Base APK与补丁包进行合并重启后合成新的dex文件。

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

    怎么执行热修复(使用补丁包)?

    • 获取到当前应用的PathClassloader;
    • 反射获取到DexPathList属性对象pathList;
    • 反射修改pathList的dexElements
      • 把补丁包patch.dex转化为Element[] (patch)
      • 获得pathList的dexElements属性(old)
      • patch+dexElements合并,并反射赋值给pathList的dexElements

    制作补丁包流程:
    1、把Bug修复掉后,先生成类的class文件。
    2、执行命令:dx --dex --output=patch.jar com/enjoy/enjoyfix/Utils.class

    应用补丁包: patchElment(补丁包生成的) + oldElement(APK原有的) 赋值给oldElement

     DexPathList.java
     //splitDexPath  多个dex的时候,a.dex:b.dex 拆分
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                               suppressedExceptions, definingContext, isTrusted);
    

    1、获取程序的PathClassLoader对象
    2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象
    3、反射获取pathList的dexElements对象 (oldElement)
    4、把补丁包变成Element数组:patchElement(反射执行makePathElements)
    5、合并patchElement+oldElement = newElement (Array.newInstance)
    6、反射把oldElement赋值成newElement

       /**
         * 1、获取程序的PathClassLoader对象
         * 2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象
         * 3、反射获取pathList的dexElements对象 (oldElement)
         * 4、把补丁包变成Element数组:patchElement(反射执行makePathElements)
         * 5、合并patchElement+oldElement = newElement (Array.newInstance)
         * 6、反射把oldElement赋值成newElement
         *
         * @param application
         * @param patch
         */
        public static void installPatch(Application application, File patch) {
            File hackDex = initHack(application);
            List<File> patchs = new ArrayList<>();
            patchs.add(hackDex);
            if (patch.exists()) {
                patchs.add(patch);
            }
    
            //1、获取程序的PathClassLoader对象
            ClassLoader classLoader = application.getClassLoader();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                try {
                    ClassLoaderInjector.inject(application, classLoader, patchs);
                } catch (Throwable throwable) {
                }
                return;
            }
            //2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象
            try {
                Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
                Object pathList = pathListField.get(classLoader);
                //3、反射获取pathList的dexElements对象 (oldElement)
                Field dexElementsField = ShareReflectUtil.findField(pathList, "dexElements");
                Object[] oldElements = (Object[]) dexElementsField.get(pathList);
                //4、把补丁包变成Element数组:patchElement(反射执行makePathElements)
                Object[] patchElements = null;
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    Method makePathElements = ShareReflectUtil.findMethod(pathList, "makePathElements",
                            List.class, File.class,
                            List.class);
                    ArrayList<IOException> ioExceptions = new ArrayList<>();
                    patchElements = (Object[])
                            makePathElements.invoke(pathList, patchs, application.getCacheDir(), ioExceptions);
    
                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    Method makePathElements = ShareReflectUtil.findMethod(pathList, "makeDexElements",
                            ArrayList.class, File.class, ArrayList.class);
                    ArrayList<IOException> ioExceptions = new ArrayList<>();
                    patchElements = (Object[])
                            makePathElements.invoke(pathList, patchs, application.getCacheDir(), ioExceptions);
                }
    
    
                //5、合并patchElement+oldElement = newElement (Array.newInstance)
                //创建一个新数组,大小 oldElements+patchElements
    //                int[].class.getComponentType() ==int.class
                Object[] newElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(),
                        oldElements.length + patchElements.length);
    
                System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
                System.arraycopy(oldElements, 0, newElements, patchElements.length, oldElements.length);
                //6、反射把oldElement赋值成newElement
                dexElementsField.set(pathList, newElements);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    makePathElements参数:
    1、补丁包:List[new File("/sdcard/patch.jar")]
    2、optimizedDirectory 传一个私有目录就行比如:context.getCacheDir()
    3、ArrayList suppressedExceptions = new ArrayList();

    有两个兼容问题:

    1. 在不同的版本中可能不是makePathElement,或者 参数会不同,需要适配!

    2. Android N 混合编译问题:热点代码记录后空闲编译,启动后就会插到ClassLoader;

    ART 是在 Android KitKat(Android 4.0)引入并在 Lollipop(Android 5.0)中设为默认运行环境,可以看作Dalvik2.0。 ART模式在Android N(7.0)之前安装APK时会采用AOT(Ahead of time:提前编译、静态编译)预编译为机器码。 而在Android N使用混合模式的运行时。应用在安装时不做编译,而是运行时解释字节码,同时在JIT编译了一 些代码后将这些代码信息记录至Profile文件,等到设备空闲的时候使用AOT(All-Of-the-Time compilation:全 时段编译)编译生成称为app_image的base.art(类对象映像)文件,这个art文件会在apk启动时自动加载(相当 于缓存)。根据类加载原理,类被加载了无法被替换,即无法修复。

    混合编译热修复解决方案
    运行时替换PathClassLoader方案 :
    上面所说app_image文件中的class是插入到PathClassloader中的ClassTable中。假设我们完全废弃掉PathClassloader,而 采用一个新建Classloader来加载后续的所有类,即可达到将cache无用化的效果。

    
    //总共替换三处:LoadedApk.class  Resource.class DrawableInflater.class   属性 ClassLoader classloader
    
     private static void doInject(Application app, ClassLoader classLoader) throws Throwable {
            Thread.currentThread().setContextClassLoader(classLoader);
    
            Context baseContext = (Context) ShareReflectUtil.findField(app, "mBase").get(app);
            if (Build.VERSION.SDK_INT >= 26) {
                ShareReflectUtil.findField(baseContext, "mClassLoader").set(baseContext, classLoader);
            }
    
            Object basePackageInfo = ShareReflectUtil.findField(baseContext, "mPackageInfo").get(baseContext);
            ShareReflectUtil.findField(basePackageInfo, "mClassLoader").set(basePackageInfo, classLoader);
    
            if (Build.VERSION.SDK_INT < 27) {
                Resources res = app.getResources();
                try {
                    ShareReflectUtil.findField(res, "mClassLoader").set(res, classLoader);
    
                    final Object drawableInflater = ShareReflectUtil.findField(res, "mDrawableInflater").get(res);
                    if (drawableInflater != null) {
                        ShareReflectUtil.findField(drawableInflater, "mClassLoader").set(drawableInflater, classLoader);
                    }
                } catch (Throwable ignored) {
                    // Ignored.
                }
            }
        }
    
    1. 假如MainActivity 引用到Uitls都在同一个dex中的话,那么这个MainActivity就会被打上CLASS_ISPREVERIFIED:标明MainActivity不需要跨dex调用,如果此时修复Utils里的bug,编辑单独的dex文件,MainActivity就需要跨dex调用,产生冲突
    if (ClassVerifier.PREVENT_VERIFY) {
        //AntilazyLoad类会被打包成单独的hack.dex;
        //classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。
        System.out.println(AntilazyLoad.class);
    }
    

    这里用到了字节码插桩技术

    字节码插桩

    插件实现

    • Gradle 本身就是java程序;帮我们完成一系列javac、dx、aapt、等命令行;
    • AfterEvaluater{} 注册一个监听,等Gradle解析完执行;
      字节码插桩要在此时执行,也是编译出class文件,执行dx之前;
    • ASM:操作Java 字节码的框架,ClassReader读取字节码,ClassVistor解析,ClassWriter生成字节码;
    static byte[] referHackWhenInit(InputStream inputStream) throws IOException {
        // class的解析器
        ClassReader cr = new ClassReader(inputStream)
        // class的输出器
        ClassWriter cw = new ClassWriter(cr, 0)
        // class访问者,相当于回调,解析器解析的结果,回调给访问者
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
    
            //要在构造方法里插桩 init
            @Override
            public MethodVisitor visitMethod(int access, final String name, String desc,
                                             String signature, String[] exceptions) {
    
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                mv = new MethodVisitor(Opcodes.ASM5, mv) {
                    @Override
                    void visitInsn(int opcode) {
                        //在构造方法中插入AntilazyLoad引用
                        if ("<init>".equals(name) && opcode == Opcodes.RETURN) {
                            //引用类型
                            //基本数据类型 : I J Z
                            super.visitLdcInsn(Type.getType("Lcom/enjoy/patch/hack/AntilazyLoad;"));
                        }
                        super.visitInsn(opcode);
                    }
                };
                return mv;
            }
    
        };
        //启动分析
        cr.accept(cv, 0);
        return cw.toByteArray();
    }
    
    

    自动化补丁方案

    gradle 插件是启动一个JVM来执行;
    apply plugin:‘com.android.application’
    apply plugin:‘com.android.library’

    方式 说明
    Build script脚本 把插件写在 build.gradle 文件中,一般用于简单的逻辑,只在该 build.gradle 文件可见
    buildSrc目录 将插件源代码放在 buildSrc/src/main/groovy/ 中,只对该项目中可 见
    独立项目 一个独立的 Java 项目/模块,可以将文件包发布到仓库(Jcenter), 使其他项目方便引入

    那些类需要打包进补丁包?如何筛选?
    .class->MD5
    如何用代码打包进补丁包;
    Runtime.get Runtime

    混淆怎么办?
    保障这次混淆和上次混淆出相同的类名

    展开全文
  • Android热修复原理(一)热修复框架对比和代码修复

    万次阅读 多人点赞 2018-03-12 01:20:29
    如果只是会这些热修复框架的使用那意义并不大,我们还需要了解它们的原理,这样不管热修复框架如何变化,只要基本原理不变,我们就可以很快的掌握它们。这一个系列不会对某些热修复框架源码进行解析,...

    title: “Android热修复原理(一)热修复框架对比和代码修复”
    date: 2018-03-12 00:18
    photos:

    • https://s2.ax1x.com/2019/05/29/VnQrOx.png
      tag:
    • Android热修复原理
      categories:
    • Android应用层

    本文首发于微信公众号「后场村刘皇叔」

    关联系列
    解析ClassLoader系列

    前言

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

    1.热修复的产生概述

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

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

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

    2.热修复框架的对比

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

    类别 成员
    阿里系 AndFix、Dexposed、阿里百川、Sophix
    腾讯系 微信的Tinker、QQ空间的超级补丁、手机QQ的QFix
    知名公司 美团的Robust、饿了么的Amigo、美丽说蘑菇街的Aceso
    其他 RocooFix、Nuwa、AnoleFix

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

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

    特性 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原理,现在我们就来学习代码修复。

    3.代码修复

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

    3.1 类加载方案

    类加载方案基于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(二)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的双亲委托模式就不会被加载,这就是类加载方案,如下图所示。
    VnQD61.png

    类加载方案需要重启App后让ClassLoader重新加载新的类,为什么需要重启呢?这是因为类是无法被卸载的,因此要想重新加载新的类就需要重启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等等。

    3.2 底层替换方案

    与类加载方案不同的是,底层替换方案不会再次加载新类,而是直接在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。

    3.3 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$overrideaccess$dispatch方法,accessdispatch方法中会根据参数&quot;onCreate.(Landroid/os/Bundle;)V&quot;执行‘MainActivitydispatch方法中会根据参数&quot;onCreate.(Landroid/os/Bundle;)V&quot;执行`MainActivitydispatch"onCreate.(Landroid/os/Bundle;)V"MainActivityoverride`的onCreate方法,从而实现了onCreate方法的修改。
    借鉴Instant Run的原理的热修复框架有Robust和Aceso。


    这里不仅分享大前端、Android、Java等技术,还有程序员成长类文章。
    ZmD7sH.jpg
    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 11,165
精华内容 4,466
关键字:

热修复