前言
上一章我们讲了一下如何通过加载补丁包dex的方式来进行热修复的原理,但是这个补丁包要如何获取呢?这一节我们就来讲讲如何来自动生成补丁包。这一章我们会接触两个新的知识点:插桩技术和gradle插件开发。
一、插桩技术
我们在讲解热修复过程中会提到了字节码插桩的概念,那么要如何实现字节码插桩呢?
(1)字节码插桩能干吗?
实现插桩之前我们先来了解下插桩能干吗,我们写的.java文件会被编译成.class,而这个.class文件一般我们是无法直接做修改的,除非修改.java文件,再重新编译。但是字节码插桩技术它能够帮助我们直接的修改class文件,当然这个过程中还需要借助class工具来实现,下面我们会讲到。
(2)字节码插桩如何修改class文件
class文件是一个被编译的用于JVM识别的二进制文件,并不是我们想怎么修改就怎么修改的,需要按照class文件的格式进行修改才有效,否则只会破坏文件的有效性。但是我们并不了解class文件的格式这时候就需要借助一些工具来帮助我们来完成这份工作,在Android里面我们可以借助asm或者javassist框架来操作我们的class文件。这样就有效避免了我们不了解class文件格式无从下手的尴尬局面。
我知道很多人都想问asm和javassist是啥?其实我们并不需要弄明白他们是啥,只需要知道有这样一个类似工具的东西能够帮助我们去操作class文件就可以。至于如何操作,百度是最好的方式!所以很多时候我们受限与自己的知识面无法弄清楚很多原理性的东西,但是如果有人点播一下,或者给个提示,要实现某一功能还是没有太大问题,不会就百度嘛。以前我也总觉得在别人面前百度挺羞愧的,就显得自己很菜鸡,但经过一年多来的学习,我意识到这真的不是一件羞耻的事情,反而是一个好习惯。不会就去找资料,这么好的学习习惯为什么会觉得羞耻呢?谁也不是一出生就会的,这一点没什么好羞愧的。
(3)为什么要字节码插桩
这个问题,其实在上一章节我们已经讲过了,在进行热修复过程中,会遇到一个类校验的问题,而解决这个问题的路径就是绕过类校验,就是要让我们app中所有的class类都引用一下第三方的hack.dex中的C类。这样app中所有的类在加载时就都不会被打上CLASS_ISPREVERIFIED标记了,避免热修复过程中出现的类加载校验失败的问题,但是我们又不能直接通过代码引用hack.dex中的C类,这就需要我们通过字节码插桩来实现无侵入式的引用,直接在编译过程中引用而不是写代码过程中去引用。
(4) 何时进行字节码插桩
class文件是在打包之前编译完成的,我们要做的就是在apk打包之前进行插桩干预。我们通常编译我们的apk的时候可能都没有注意到,gradle在编译的时候会顺序执行很多任务。如下图:
其中有一个transformClassesWithDexBuilderForDebug任务,这个任务就是将我们的class文件打包进dex文件中。而我们要做的就是在打包进dex之前进行插桩干预修改我们的class文件。
(5)编译过程中怎么获得所有要执行插桩的class文件并插桩
每一个Gradle任务都对应着一个Task,而每一个Task都为我们提供了执行前干预和执行后干预的入口,他们分别时doFirst和doLast方法。说到这里估计大家就懵了,没关系,我们只需要知道有这样的入口供我们去干预就行了,至于代码如何实现可参考demo,也可百度。相信难不倒大家,难的是思路!那么我们想要实现插桩就必须拦截transformClassesWithDexBuilderForXX任务,这里的XX表示是debug版本还是release版本。在任务执行前做修改class文件的操作。这里就只贴出插桩的核心代码,完整拦截任务并插桩的代码有兴趣的可以自行参考demo中的PatchPlugin类实现。demo中插桩采用的就是上面说的asm框架,这是Android提供给我们的框架:
/**
*
* @param inputStream class文件的输入流
* @return
* @throws IOException
*/
public static byte[] referHackWhenInit(InputStream inputStream) throws IOException {
ClassReader cr = new ClassReader(inputStream);//class的解析器
ClassWriter cw = new ClassWriter(cr, 0);//class的输出器
//class的访问者,相当于回调,解析器解析的结果,回调给访问者
ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
@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(api, mv) {
@Override
public void visitInsn(int opcode) {
//在构造方法中插入hack.dex中的AntilazyLoad类引用
if ("<init>".equals(name) && opcode == Opcodes.RETURN) {
super.visitLdcInsn(Type.getType("Lcom/single/patch/hack/AntilazyLoad;"));
}
super.visitInsn(opcode);
}
};
return mv;
}
};
//启动分析,启动后ClassReader 开始解析class数据,然后回调访问者的visitMethod
cr.accept(cv, 0);
return cw.toByteArray();
}
二、生成补丁包的插件
1、android studio 如何进行插件开发
android 的插件开发有三种方式,这里我们只介绍一种,直接在Android studio上进行的插件开发。步骤如下:
(1)、在工程app同级目录新建一个buildSrc目录,这是一个特殊的目录。给build.gradle使用的代码目录。创建好后编译
(2)、buildSrc根目录下创建build.gradle文件,复制如下代码
apply plugin:'java'
tasks.withType(JavaCompile) { options.encoding = "UTF-8" }
repositories {
google()
jcenter()
}
dependencies {
implementation 'com.android.tools.build:gradle:3.1.3'
implementation 'com.google.code.gson:gson:2.8.5'
implementation gradleApi()
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
(3)、实现Plugin接口,如我的demo中的PatchPlugin,继承Plugin<Project>并实现apply方法。而我们的插件所要执行的任务就都在apply方法内部实现,这里跟我们写Java方法没啥区别。区别就在于这里我们可能要熟悉一下gradle的API。
(4)、应用我们自定义的插件
相信大家对apply plugin: 'com.android.application'都不陌生吧。这就是应用Android官方写好的插件,而使用我们自己的插件同理。
(5)插件配置
android {
compileSdkVersion 29
buildToolsVersion "29.0.0"
defaultConfig {
applicationId "com.single.code.shotfix"
minSdkVersion 26
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
我们在使用Android插件的时候都要配置如上一段代码。这就是插件的配置,而我们自定的插件如果也想要自定义类似这样的配置该如何实现呢?比如tinker热修复插件在使用时就需要我们配置一堆的配置项。在apply方法中创建一个patch{}配置
project.getExtensions().create("patch", PatchExtension.class);
参数解析:
patch:配置名称,如同android。
class:配置参数类,PatchExtension里面就是我们自定义配置参数。写入需要配置的参数,并实现set/get方法。
(6)如何获取插件的配置
//gradle执行会解析build.gradle文件,afterEvaluate表示在解析完成之后再执行我们的代码
project.afterEvaluate(new Action<Project>() {
@Override
public void execute(final Project project) {
final PatchExtension patchExtension =
project.getExtensions().findByType(PatchExtension.class);
//获得用户的配置,在debug模式下是否开启热修复
final boolean debugOn = patchExtension.debugOn;
//得到android的配置
AppExtension android = project.getExtensions().getByType(AppExtension.class);
}
});
(7)插件如何判断哪些代码文件有改动
- 应用在每次版本发布的时候,会生成一份缓存文件,里面记录了所有class文件的md5,我们的框架对应的md5值文件就是hex.txt,还有一份mapping混淆列表文件。
- 在后续的版本中编译过程中执行transformClassesAndResourcesWithProguardForXX混淆任务时应用上一个版本的mapping文件,确保补丁包中的class混淆与上一版本的相同class的混淆保持一致,然后将混淆完成后的class文件的md5和上一版本的class文件的md5值进行比较,md5值不一样的class表示有改动,就需要被打到补丁包中。
思路有了剩下的就是敲代码了,请参考demo。如果您觉得对您又一点点帮助,高抬小手给个赞吧!