安卓代码修复框架Patch
只支持程序重启修复,不支持热修复

上一篇文章自定义了gradle插件,但是这并没有啥用,因为我们要做代码修复,然而我们的插件没有做任何东西
当然我们必须知道我们的插件要做代码修复,具体要明确我们修复的目的,那我们开始列出我们的几个目标,然后一一实现它
插件目标
- 1.怎么在插件里面打印日志
- 2.要知道哪些类需要修复,那些类不需要修复
- 3.我们要在哪个任务进行代码修复
- 4.创建我们的补丁任务
- 5.我们怎么区别上个版本代码和我们修复之后的代码类,我们需要用什么工具进行代码注入
- 6.我们要处理的都是一些什么文件
- 7.找到修复的代码类之后怎么把它打包到patch.jar包里面
实现目标
-
1.怎么在插件里面打印日志
- 其实要在插件里面打印日志很简单,我们上篇文章就提到过
println ("我是插件")
,这是这个代码,其实调用的是java
的System.out.println("我是插件");
的简写groovy
默认提供System.out
的引入 - 但是我们都知道我们打印日志的时候,最好有一个
TAG
,而我们这里希望TAG
是我们构建变体的名称Variant
,这里我们新建一个类BLogger
代码实现如下
- 其实要在插件里面打印日志很简单,我们上篇文章就提到过
class BLogger{
private static String TAG
static void init(BaseVariant variant){
//构建变体的名称
TAG=">PatchPlugin-${variant.name.capitalize()}"
}
static void i(String log){
println("${TAG}:${log}")
}
}
这里我们传递了一个BaseVariant
类型参数进来,这样每个构建变体构建时都能区分开来
- 然后我们看看怎么用
@Override
void apply(Project project) {
//afterEvaluate是一般比较常见的一个配置参数的回调方式,只要project配置成功均会调用,
project.afterEvaluate {
project.android.applicationVariants.each {
variant ->
//初始化日志打印器
BLogger.init(variant)
BLogger.i("Hello World 中国")
}
}
}
我们在project.afterEvaluate
项目配置之后,可以通过project.android.applicationVariants
遍历获取到所有的构建变体,然后我们把我们开始配置我们的日志类,然后打印一下日志
我们点击上传一下这个插件 ,然后点击app的assemble任务看看构建日志

我们可以看到,他打印了两个变体的日志,因为我们现在没有自定义
productFlavors { xiaomi{} }
,所以并没有出现小米的变体,但是还是会有默认的Debug
和Release
这两个构建变体
- 2.要知道哪些类需要修复,那些类不需要修复
我们分析一下这个问题,第三库和安卓提供的代码库我们都不需要管,我们只想修复我们需要修复的代码,另外还有一个问题是,我们有一些业务比较稳定,也没有出现bug的可能性了,我们是不是应该区分一下,就不需要做代码注入了
基于上面的分析,我们先做一个准备,就是我们必须让开发者可以自己配置哪些业务代码是不需要被注入代码的,同时哪些类是需要注入的,这些都需要动态配置,需要怎么做呢?
我们可以参考android插件的配置,我们自定义一个扩展。
下面我们来实现一下 ,然后我们验证一下有没有用
@Override
void apply(Project project) {
//第一步创建扩展
project.extensions.create("bigman", BigmanExtension, project)
//afterEvaluate是一般比较常见的一个配置参数的回调方式,只要project配置成功均会调用,
project.afterEvaluate {
//通过名字找到配置里面的bigman扩展
extension = project.extensions.findByName("bigman") as BigmanExtension
project.android.applicationVariants.each {
variant ->
BLogger.init(variant)
extension.excludeClass.each {
name ->
BLogger.i("排除类名称:${name}")
}
}
}
- 第一步.在这里我们创建了一个名字为
bigman
的插件,然后我们在上面代码的基础上加入上面的代码
然后我们点击上传,上传成功之后,我们在我们的app模块的build.gralde
加以下代码
apply plugin: 'com.android.application'
apply plugin: 'com.bigman.tech'
bigman{
includePackage = ['com/example/administrator/patchdemo']
excludeClass = ['com/example/administrator/patchdemo/patchlib/SignaChecker','me/wcy/cfix/sample/Test']
}
- 第二步这里我们声明了两个数组一个
includePackage
我们用来指定需要插入代码的包路径,另一个excludeClass
使我们不希望插入代码的类路径,这里的分割符号要注意一下。
然后我们对比一下上面的代码
extension.excludeClass.each {
name ->
BLogger.i("排除类名称:${name}")
}
-
第三步 我们这里循环打印我们这个数组里面的值,我们点击gradle里的任务我这里选择
assembleDebug
image.png
我们可以看到即使我选择了这个任务,另一个在android扩展里配置了的构建变体同样会输出我们的排除数组。
我们这里已经顺利的打印出我们需要配置的数组,另一个数组大家可以通过同样的方法获取这是我们通过自定义扩展实现自定义配置,但是这两个东西具体有啥用,我们讲到代码注入的时候会说到
这里如果我们配置了
includePackage
,那其实已经可以很好地排除第三方库,但是假如我们这个位置不做任何配置,那我们还需要做额外的事情,比如android/support/
,R
,BuildConfig
这些不应该修改的类文件,怎么做,这个就比较简单我们这里会提供一个方法,这里后面会用到,代码先放出来
private static boolean shouldProcessClass(File classFile,BigmanExtension extension){
if (!classFile.exists()||!classFile.name.endsWith(SdkConstants.DOT_CLASS)){
return false
}
FileInputStream inputStream=new FileInputStream(classFile)
ClassReader cr=new ClassReader(inputStream)
String className=cr.className
inputStream.close()
return !className.startsWith("com/bigman/tech/lib")&&
!className.contains("android/support/")&&
!className.contains("/R\$")&&
!className.endsWith("/R")&&
!className.endsWith("/BuildConfig")&&
PatchSetUtils.isIncluded(className,extension.includePackage)&&
!PatchSetUtils.isExcluded(className,extension.excludeClass)
}
其实上面的代码就能解决上面说的问题,好了目标2已经完成
-
3.我们要在哪个任务进行代码修复
这个其实我们之前提到过,就是在dex任务之前进行,这个我们可以通过以下代码进行判别
//Gradle版本为1.5-2.x
Task dexTaskLower = project.tasks.findByName("transformClassesWithDexFor${variant.name.capitalize()}")
//Gradle版本为3.0+
Task dexTaskHigner = project.tasks.findByName("transformClassesWithDexBuilderFor${variant.name.capitalize()}")
//获取生成dex的任务
Task dexTask
if (dexTaskLower != null) {
dexTask = dexTaskLower
} else if (dexTaskHigner != null) {
dexTask = dexTaskHigner
} else {
BLogger.i("Gradle 版本暂不支持")
return
}
这里任务的名称其实是固定的 我们兼容了1.5-3.0+版本,通过project.tasks.findByName
就能获取到任务名称
这里其实我们可以抽出来写成一个方法,看起来简洁一点
我们说要在dex任务之前进行,但是具体要怎么做,大家应该是懵逼的
//补丁任务将在dex任务获取完依赖之后再进行
patchJarBeforeDexTask.dependsOn
dexTask.taskDependencies.getDependencies(dexTask)
//dex任务将在补丁任务执行完之后再执行
dexTask.dependsOn patchJarBeforeDexTask
patchJarBeforeDexTask
这里是我们将要进行代码注入的任务,这个任务我们等一下会讲到,大家知道它是一个任务就行
上面两句代码的意思就是我们这个代码注入任务patchJarBeforeDexTask
是在dexTask
任务获取完所有的依赖文件之后执行,这样能保证class文件的完整性,然后dexTask
任务必须在我们代码注入任务patchJarBeforeDexTask
完成之后执行
我们在这里改变了打包任务的执行次序,而且创建了一个patchJarBeforeDexTask
的任务,当然这个任务使我们的核心
-
4.创建我们的补丁任务
创建一个任务其实很简单,首先任务需要一个名字,我们需要和构建变体绑定在一起,我们看一下代码实现
String patchJarBeforeDex = "patchJarBeforeDex${variant.name.capitalize()}"
project.task(patchJarBeforeDex) << {
Set<File> inputFiles = dexTask.inputs.files.files
inputFiles.each {
file ->
//打印dex任务返回的输入文件
BLogger.i("transformClassesTask input:${file.absolutePath}")
}
}
可以看到我们通过project.task(patchJarBeforeDex) << {}
创建了一个任务,这个任务就是我们的核心任务,我们可以看到我们在任务里面获取到了dexTask
的一些输入文件,其实它们就是我们的class文件,我们上传一下,然后看看效果,操作和上面的一样。

可以看到已经获取到了所有的class文件和jar文件,这些文件我将对它们进行过滤和代码插入
我们可以简要看看哪些类应该过滤,比如上图的
R.class
,R$**.class
,android\support\
这几个安卓内置的类咱们都不需要去植入代码,那么大家就能理解我们上面写的那几句代码的意思了
!className.contains("android/support/")&&
!className.contains("/R\$")&&
!className.endsWith("/R")&&
这里就是过滤安卓内部代码的语句
-
5.我们怎么区别上个版本代码和我们修复之后的代码类,我们需要用什么工具进行代码注入
其实很简单,因为class文件生成hash码是唯一的,所以我们只要对比两次生成的hash码是否相等,就能判断是不是需要修复的代码类。
我们具体来实现以下:
* 第一步 我们需要为我们需要插入代码的类,生成hash码,然后保存到一个hash.txt
的文件File hashFile = new File(outputDir, "hash.txt") String patchJarBeforeDex = "patchJarBeforeDex${variant.name.capitalize()}" project.task(patchJarBeforeDex) << { Set<File> inputFiles = dexTask.inputs.files.files inputFiles.each { file -> //打印dex任务返回的输入文件 BLogger.i("transformClassesTask input:${file.absolutePath}") } //取到所有的类文件 Set<File> files=[] for (File file :inputFiles){ if (file.directory) { file.eachFileRecurse { f -> if (f.file) { files.add(f) } } }else{ files.add(file) } } //---核心逻辑---循环进行代码注入 files.each { file -> //如果是jar文件需要解压后再处理 if (file.name.endsWith(SdkConstants.DOT_JAR)) { PatchProcessor.processJar(file, hashFile, hashMap, patchDir, extension) } else if (file.name.endsWith(SdkConstants.DOT_CLASS)) { PatchProcessor.processClass(file, hashFile, hashMap, patchDir, extension) } } }
其他代码看源码 不说了
网友评论