手把手教你写热修复(HOTFIX)

前提

写这篇文章的目的呢,也是理一下本身的思路吧,同时把最近看到的一些热修复知识献给读者们。不知道同窗们最近是否是听到了不少关于热修复的事情,各大厂商,各界大佬们都有属于本身的热修复框架,最近阿里不也推出了个爆炸消息,堪称最牛逼的修复框架Sophix,同时还推出了对应的一本pdf(叫什么深刻理解Android热修复技术原理),不知道多少同窗看过,深刻看应该是能够看到个原理,可是我感受看了我也写不出这样的代码,毕竟大厂大佬。这篇文章呢,就简单的教你们如何写一个属于本身公司或者本身的热修复框架java

友情提醒

1.这篇文章的重点在于.class文件的打桩,可能会偏重于groovy语言,与java相通没事,相信我你绝对能看懂。android

2.若是没有看过我以前的那篇文章可能会有些懵哦,以前的那篇文章讲的是原理,经过DexClassLoader如何热修复。git

3.由于热修复关键点仍是在于打桩生成差别文件的dex,而不是在于把这个dex文件插入到已安装的app中(两个相辅相成(打桩和插入dex)),由于google的multidex里面已经把这个操做作的很好了,咱们只须要修修改改就能够完成这个插入操做,还有就是以前那篇文章还留了一个坑。github

4.若是没有接触过热修复的同窗可能会对下面的一些词汇比较闷(打桩修改字节码文件(.class)打桩目的为了解决CLASS_ISPREVERIFIED预约义,不明白的可参考上一篇文章)api

5.附文章传送门及这篇文章的项目源码app

DexClassLoader热修复的入门到放弃框架

AutoFix欢迎star,fork,issueide

小节

  • 如何正确的打桩避免Gradle1.4以上Transform API致使的没法打包(解决以前文章的坑)
  • 如何在编译成dex文件前进行打桩
  • 如何打桩
  • 如何区分差别文件及正确的打包出patch.jar(只对修改后的文件进行打包)

解决Gradle1.4以上的Transform问题

由于google的gradle升级了嘛,主要是他引入了transform API(官网解释The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1),致使咱们的plugin找不到以前咱们写好的task任务名。post

下面我给你们讲一下经过plugin进行打桩操做,接下来我会给你们看一下nuwa热修复项目中的部分代码。gradle

def preDexTask = project.tasks.findByName("preDex${variant.name.capitalize()}")
def dexTask = project.tasks.findByName("dex${variant.name.capitalize()}")
def proguardTask = project.tasks.findByName("proguard${variant.name.capitalize()}")复制代码

这是nuwa热修复的源码他事先定义好了这些任务,这些任务就是把字节码文件打包成dex文件的任务,上面的代码意思就是获取这些任务的名字。(就是apply plugin: 'com.android.application'里面的任务)。从上面的代码能够看到,咱们定义的任务名称分别是(preDex${variant.name.capitalize()})(dex${variant.name.capitalize()})(proguard${variant.name.capitalize()})($这个符号就是拼接字符串的意思和kotlin同样,variant.name.capitalize()这个就是获取的字符串是debug 仍是release)。而后上面咱们也说了,gradle1.4以后google更名字了,咱们固然找不到这些任务名了,固然报错了哦。如今呢咱们只须要作一些简单的if判断操做不就能够了吗?根据不一样的gradle版本号修改一下名字不就得了,下面贴出RocooFix的代码块如何解决的

static String getProGuardTaskName(Project project, BaseVariant variant) {
        if (isUseTransformAPI(project)) {
            return "transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}"
        } else {
            return "proguard${variant.name.capitalize()}"
        }
    }

    static String getPreDexTaskName(Project project, BaseVariant variant) {
        if (isUseTransformAPI(project)) {
            return ""
        } else {
            return "preDex${variant.name.capitalize()}"
        }
    }

    static String getDexTaskName(Project project, BaseVariant variant) {
        if (isUseTransformAPI(project)) {
            return "transformClassesWithDexFor${variant.name.capitalize()}"
        } else {
            return "dex${variant.name.capitalize()}"
        }
    }复制代码

看到了吗?就是判断一下当前的项目gradle版本号,而后修改一下名称返回给你。简单吧。几行代码解决了兼容性问题。

什么时候打桩

以前那篇文章也说了,apk编译的生命周期。因此这边顾名思义固然是在被打成dex文件以前对class文件的时候操做啊。

接下来问题来了,如何在被打成dex文件前操做呢,刚刚上面说的获取那些task名称还记得吗?这就是关键。由于在groovy中有这么一个语法,任务之间能够经过dependsOn来添加依赖。

那么好如今举个例子。

task A{}
task B{}

A dependsOn B复制代码

很明显吗?就是执行A前必须B执行完了才行。知道了这个吗?咱们下面继续看项目源码如何设置在咱们打桩完成以后再执行dex操做

def autoJarBeforeDexTask = project.tasks[autoJarBeforeDex]

 autoJarBeforeDexTask.dependsOn dexTask.taskDependencies.getDependencies(dexTask)
 autoJarBeforeDexTask.doFirst(prepareClosure)
 autoPatchTask.dependsOn autoJarBeforeDexTask
 dexTask.dependsOn autoPatchTask复制代码

一下来这么多代码 并且这些代码还不认识可能会有点蒙哦,不要着急,一行一句的讲解给你听他们的依赖关系(简单讲一下上面代码的意思
autoJarBeforeDexTask这个任务就是进行打桩的任务,dexTask这个任务就是打包成dex的任务,prepareClosure初始化操做的,autoPatchTask打包成补丁文件的任务

第一行呢 获取项目中的task 名字是autoJarBeforeDex

第二行呢 先说一下这句话的意思(dexTask.taskDependencies.getDependencies(dexTask) 这句话就是拿到dexTask的依赖任务),而后咱们的autoJarBeforeDexTask这个任务对dexTask的依赖任务 进行依赖

第三行呢 autoJarBeforeDexTask 点doFirst(prepareClosure)意思就是说在执行autoJarBeforeDexTask任务前先执行这个prepareClosure

第四行和第五行不用说了吧

最后逻辑如prepareClosure -> autoJarBeforeDexTask -> autoPatchTask -> dexTask 依次执行

这样操做就解决了在dex文件生成前进行字节码文件的插入

如何打桩

首先呢,打桩你的先获取字节码文件吧。就是这些file。如何获取这些文件呢,由于咱们编译项目的时候会在项目中生成一个build目录,里面有项目相关的全部文件,咱们能够经过下面的方式获取这些文件,咱们打桩只须要获取jar文件和intermediates/class下面对于的class文件,代码以下

static Set
  
  
  

 
  
  getDexTaskInputFiles(Project project, BaseVariant variant, Task dexTask) { if (dexTask == null) { dexTask = project.tasks.findByName(getDexTaskName(project, variant)); } if (isUseTransformAPI(project)) { def extensions = [SdkConstants.EXT_JAR] as String[] Set 
 
  
    files = Sets.newHashSet(); dexTask.inputs.files.files.each { if (it.exists()) { if (it.isDirectory()) { Collection 
   
     jars = FileUtils.listFiles(it, extensions, true); files.addAll(jars) //intermediates/class下面对应的class文件 if (it.absolutePath.toLowerCase().endsWith("intermediates${File.separator}classes${File.separator}${variant.dirName}".toLowerCase())) { files.add(it) } //jar包 } else if (it.name.endsWith(SdkConstants.DOT_JAR)) { files.add(it) } } } return files } else { return dexTask.inputs.files.files; } } 
    
   

 复制代码

文件这时候咱们已经拿到了。而后咱们要遍历这些文件依次给他们打桩,同时要过滤掉jar包中不须要打桩的文件否则会耗时

//打桩等一些工做
  def autoJarBeforeDex = "autoJarBeforeDex${variant.name.capitalize()}"
                project.task(autoJarBeforeDex) << {
                    //获取build/intermediates/下的文件
                    Set
  
  
  

 
  
  inputFiles = AutoUtils.getDexTaskInputFiles(project, variant, dexTask) inputFiles.each { inputFile -> def path = inputFile.absolutePath if (path.endsWith(SdkConstants.DOT_JAR)) { //对jar包进行打桩 NuwaProcessor.processJar(hashFile,hashMap,inputFile, patchDir, includePackage, excludeClass) } else if (inputFile.isDirectory()) { //intermediates/classes/debug 目录下面须要打桩的class def extensions = [SdkConstants.EXT_CLASS] as String[] //过滤不须要打桩的文件class def inputClasses = FileUtils.listFiles(inputFile, extensions, true); inputClasses.each { inputClassFile -> def classPath = inputClassFile.absolutePath //过滤R文件和config文件 if (classPath.endsWith(".class") && !classPath.contains("/R\$") && !classPath.endsWith("/R.class") && !classPath.endsWith("/BuildConfig.class")) { //引用nuwa而来的 if (NuwaSetUtils.isIncluded(classPath, includePackage)) { if (!NuwaSetUtils.isExcluded(classPath, excludeClass)) { def bytes = NuwaProcessor.processClass(inputClassFile) if ("\\".equals(File.separator)) { classPath = classPath.split("${dirName}\\\\")[1] } else { classPath = classPath.split("${dirName}/")[1] } def hash = DigestUtils.shaHex(bytes) hashFile.append(AutoUtils.format(classPath, hash)) //根据hash值来判断当前文件是否为差别文件须要作成patch吗? if (AutoUtils.notSame(hashMap,classPath, hash)) { def file = new File("${patchDir}${File.separator}${classPath}") file.getParentFile().mkdirs() if (!file.exists()) { file.createNewFile() } FileUtils.writeByteArrayToFile(file, bytes) } } } } } } } } 

 复制代码

好吧代码有点长,可是每个关键点都有相应的注释,
上面的代码的意思简单的说就是 先判断文件是jar包仍是路径 若是是jar包,进行jar包的打桩方式,若是是路径的话 找到class文件判断这个class是否要打桩(如R文件就不须要)。而后根据文件的hash值来来判断这个类是否修改过,若是修改过吧吧这些类放在一个文件夹中,最后统一打包成补丁。

打桩代码有两处

//对jar包进行打桩 
 NuwaProcessor.processJar(hashFile,hashMap,inputFile, patchDir, includePackage, excludeClass)复制代码
//对文件进行打桩
def bytes = NuwaProcessor.processClass(inputClassFile)复制代码

此次介绍的打桩用的是asm这个库,具体代码都在项目中能够去看看,这里就不详细说了。

如何区分差别文件打包成patch.jar

这个关键点在于项目要在gradle中配置一些信息 有兴趣的同窗能够看一下项目里面有集成过程
AutoFix

auto_fix {
    lastVersion = '1'//须要打补丁的状况下打开此处
}复制代码

若是细心的同窗会发现咱们的项目中建立了hashFile这个文件。这个文件是用来记录每一个版本的打桩了的字节码文件的hash值。

上面的代码也能够看出须要配置上一次的版本号,你出现bug了确定要修改你的versionCode 而后把以前的填写到lastVersion上,他会根据上次的hashFile来和此次生成的hashFile进行对比,若是不相同说明这个类被修改过,而后吧这个文件copy已发到patch目录下,打桩完成以后咱们,咱们到对于的patch目录下会找到这些文件而后把它们打包成patch.jar生成相应的补丁文件。有这么一段代码

//根据hash值来判断当前文件是否为差别文件须要作成patch吗?
    if (AutoUtils.notSame(hashMap,classPath, hash)) {
        def file = new File("${patchDir}${File.separator}${classPath}")
        file.getParentFile().mkdirs()
        if (!file.exists()) {
            file.createNewFile()
        }
        FileUtils.writeByteArrayToFile(file, bytes)
    }复制代码

这就是上面说的意思根据hashMap 和当前的hash值来作判断。最终生成差别文件。打包成补丁文件(问题又来了,如何生成补丁文件呢。还记得以前说的执行任务的流程吗?prepareClosure -> autoJarBeforeDexTask -> autoPatchTask -> dexTask 依次执行)autoPatchTask这个任务就是打补丁任务能够看一下源码

//制做patch补丁包
                def autoPatchTaskName = "applyAuto${variant.name.capitalize()}Patch"
                project.task(autoPatchTaskName) << {
                    if (patchDir) {
                        AutoUtils.makeDex(project, patchDir)
                    }
                }
                def autoPatchTask = project.tasks[autoPatchTaskName]复制代码

没错就是他,仍是上一篇文章讲到的打包操做只不过代码话了 具体代码能够去项目中看,这里就不详解了。

总结

说了这么多,发现我咋还不会写呢,怎么办,这篇文章说的都是tmd什么打桩,对的你没听错,由于热修复就是插入dex 而后就是打桩 就这两件事以前的文章讲的是插入dex这篇文章讲的是打桩,若是在不会能够去看看项目源码。

ending

有什么疑问和不解能够留言哦,但愿此次分享带给你们带来的不是时间的浪费,而是能力的提高。谢谢

源码奉上:AutoFix欢迎star,fork,issue

相关文章
相关标签/搜索