一、什么是热修复
在应用上线后出现Bug需要及时修复时,不需要再发新的安装包,只要发布补丁包,在用户无感知的情况下修复Bug。
二、怎么进行热修复
从以下几个角度入手来描述热修复需要解决的问题:服务端、用户端、开发端。
1、服务端:补丁包管理:
2、用户端:进行热修复:什么时候执行热修复?怎么执行热修复(使用补丁包)?Android版本兼容问题;
3、开发端:生成补丁包:补丁包是什么?如何生成补丁包?开启混淆后怎么处理?对比改动自动生成补丁包(Gradle)?
三、常用热修复框架:
热修复方案很多,其中比较出名的有腾讯的Tinker、阿里的AndFix、美团的Robust以及QZone的超级补丁方案:
几种热修复方案的简单对比下面就从 类替换 及 即时生效 两个角度来对以上几种方案做个简单说明:
1、Tinker和QZone都采用了类Class的替换:根据类加载机制,所以就不能实现即时生效,而这两者的区别在于补丁包上,Tinker采用了差分对dex包做了diff,而Qzone则是全量的类替换;
2、AndFix和Robust可以理解为是对方法Method的替换:AndFix是在Native动态替换java层的方法,通过nativce层hook JAVA层代码,通过注解来找到替换的Method;而Robust是对每个函数在编译打包阶段自动的插入一段代码,类似于代理,将方法执行的代码重定向到其他方法中;
四、热修复所用到的知识点
类加载:BootClassLoader、PathClassLoader、DexClassLoader、双亲委派机制以及类查找流程(注:此知识点会有专门章节解析),基于此可以简单总结一下热修复的流程:
——获取当前应用的PathClassLoader
——反射获取到DexPathList属性对象pathList
——发射修改pathList的dexElements:获取path.dex补丁包的dexElements、获取pathList的dexElement、将两个dexElements按照补丁包dexElement在前原有dexElemtns灾后的顺序合并后反射复制给pathList的dexElement;
五、热修复兼容问题:
1、Elements数组获取问题:补丁包path.dex最后要获取其dexElements,针对不同版本获取接口makePathElement方法名称或者参数获取会有不同,需要兼容;
2、AndroidN混合编译问题:在使用混合模式运行时,应用在安装时不做编译,而是运行时解释字节码,同时在JIT编译了一些代码将这些代码信息记录至Profile文件,等到设备空闲的时候使用AOT编译生成app_image的base.art(类对象映射)文件,这个art文件会在apk启动是自动加载(相当于缓存),根据类加载原理,类被加载了无法被替代,即无法修复;
——解决方案:运行时替换PathClassLoader:app_image中的的class是插入到PathClassLoader的ClassTable中的,假设我们完全废弃掉PathClassLoader,而采用一个新建的ClassLoader来加载后续的类,即可达到cache无用化的作用:Thread.currentThread().setContextClassLoader(classLoade r);
3、CLASS_ISPREVERIFIED标志:在Dalvik虚拟机中,如果一个类所调用的Class全部在一个Dex文件中,则就会被打上CLASS_ISPREVERIFIED标志;比如MainActivity类中只引用了Utils类,当打包时,MainActivity和Utils都在classes.dex中,则加载时MainActivity类就会被标记为CLASS_ISPREVERIFIFD;此时在做热修复的时候,如果用补丁包的dex中的Utils类去替换有Bug的Utils类,则就会导致MainActivity引用的Utils类不在同一个dex中,所以会和之前的标志产生冲突,出现校验失败;
——解决方案:利用字节码插桩技术:可在每个类中添加一个特殊类的调用,这个类单独编译生成一个dex文件,此时在加载的时候就不会被打上CLASS_ISPREVERIFIED标志,进而就能校验通过,进行替换修复;
(注:字节码插桩和ASM会有专门章节解析)
六、Gradle插件相关:
1、插件开发的几种方式:
——Build Script脚本: 把插件写在buidler.gradle文件中,一般用于简单的逻辑,并且只对改builder.gradle可见;
——buildSrc目录:将插件源文件放在buildSrc/src/main/groovy中,仅对此项目可见;
——独立项目:一个独立的JAVA项目或者模块,可将文件包发布到仓库(jcenter),方便其他项目引入;
2、插件实现:
public class TestPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
...
}
}
继承 implements Plugin<Project>,实现 apply(Project project)方法;
3、插件扩展:
public class TestPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getExtensions().create("patch", PatchExtension.class);
project.afterEvaluate(new Action<Project>() {
@Override
public void execute(final Project project) {
...
});
}
});
}
}
通过create方法创建扩展,patch对应扩展名,即在builder.gradle中
对应patch,其中debugOn、applicationName则对应PatchExtension类中定义;
apply plugin: 'com.example.patch'
patch {
debugOn true
applicationName 'com.example.hotfix.MyApplication'
}
public class PatchExtension {
boolean debugOn;
String applicationName;
}
注一:插件的引入:
apply plugin: 'com.example.patch'
apply plugin: com.example.patch.plugin.TestPlugin
以上两种引入插件的方式都可以,唯一区别为一个有引号,一个没有,有引号引入的相当于插件的一个别名,如果要采用用引号引入的方式,则必须在buildSrc\build\resources\main\META-INF\gradle-plugins/com.example.patch.properties这样一个文件,别名对应于此文件的文件名,在编译时会自动填充此文件的内容:
implementation-class=com.example.patch.plugin.PatchPlugin
注二:afterEvaluate的使用:即builder.gradle也是一行一行执行的,所以一般是在脚本扫描完后的回调函数afterEvaluate中才能正确的获取patch中的配置值;
七、利用Gradle插件自动生成补丁包
1、Task的理解:即任务,Android工程引入的插件中,创建多个自定义任务,比如:
—编译JAVA任务:compileDebugJavaWithJavac
—混淆任务:transformClassesAndResourcesWithProguardFor
—打包dex任务:transformClassesWithDexBuilderFor
2、自动生成补丁包:
思路:在编译生成class文件时,可以将生成的class文件的名字和对应的md5值保存,当第二次再次编译的时候可以对比生成class的文件的md5值是否有变化,如有变化,则将次class文件打入到补丁包中;
实现:基于上述Task的理解,可以获取transformClassesWithDexBuilderFor任务的输入作为切入点,进行class文件及对应md5值的生成及校验;
3、防止混淆开启引起补丁包异常:因为开启混淆后,可能每次编译后对同一个类混淆的名字有改变,如果有改变,会影响补丁包生成的可靠性:
——解决方案:可以利用开启混淆后生成的mapping文件,此文件记载了混淆前后类及方法的变化,同时可以在proguard-rules.pro中配置-applymapping选项;即在每次编译混淆后都会参照前一次混淆的名字,这样就保证了生成补丁包校验时的可靠性;
总结一下:
以上是基于热修复的学习的基本内容,只是简单列举了一下流程、知识点及注意点,针对每个知识点的详解后续会有文章做支持,暂时先列举出来:
——类加载相关...
——Gradle相关...
——热修复的Demo...
网友评论