热修复

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值