Android Nuwa 热修复原理和的gradle插件详解并怎么修改gradle插件

Android Nuwa 热修复原理和的gradle插件详解并怎么修改gradle插件

最近项目要用到热修复(Hotfix),就调研了一些热修复方案,最后选择了Nuwa,但是因为Nuwa不在维护了,就去把Nuwa源码下载下来详细看了下,以备后期自定义使用。

下面给大家看下之前跟同事分享用的图

nuwa热修复

如图:Nuwa主要分为两部分:

1、 Nuwa应用层实现(主要是java实现的原理是ClassLoader,把补丁包中的类加载进来,这不是这个文章主要讲的,网上也有很多这样的文章)

2、Nuwa gradle插件实现(本文重点说的,分为两点,一:补丁包生成原理,二:Nuwa生成补丁包源码详解与怎么在gradle修改Nuwa的gradle插件源码并使用)

一、补丁包生成原理

研究补丁包生成原理之前,先看下他生成的补丁包patch.jar,我用zip解压缩是这样的
Nuwa的补丁包

里面是.dex文件,我们都知道android只识别.dex或者.odex。然后我再反编译该文件发现里面是我修改bug过程中修改的类。然后我就想如果没有混淆的情况下,我把修改的几个类单独导成jar包生成dex文件是不是也可以当补丁包来用。

下面我就验证了下,把修改的类导成jar包(eclipse导出jar包比较容易,studio就比较困难,android studio怎么到处某几个类的jar包?

导出jar包后使用adt 自带的转dex工具 \sdk\build-tools\android-4.4W\dx.bat 转成dex文件
dx – dex –output=savewithdexfile.jar target_java.jar
dx 就是把前面的dx.bat拖进命令行 + – dex – output= +输出的名字 target_java.jar这个就是把目标jar拖进命令行就行

转成dex文件后当补丁包使用果然有效,因此nuwa的补丁包就是把修改的类导成dex包就可以了。看到我就想那我们还为什么还要用nuwa的gradle插件来生成补丁包,我们手动生成也行啊,其实如果没有混淆的情况下是可以的,但是如果有混淆就会比较麻烦了 。下面让我们把nuwa插件运行到我们的项目中,也方便我们修改该插件。

怎么把Nuwa的gradle插件源码运行到你的项目中

Nuwa地址:https://github.com/jasonross/Nuwa
Nuwa Gradle插件地址:https://github.com/jasonross/NuwaGradle

1、Nuwa地址中下载java源码:Nuwa-master\nuwa\src\main\java里的源码拷入你的项目中如下图:
Nuwa java源码
这样你就有了nuwa的java源码,这个比较简单

2、从Nuwa Gradle插件地址中下载插件源码,NuwaGradle-master\src\main\groovy中就是gradle插件源码,
下面介绍如何一步步把这个个插件源码运行到你的项目中

a、创建Gradle Module
(1)首先在你的项目中新建一个Module我选Android Librarty类型,《新建的Module名称必须为BuildSrc》如下图
创建本项目的gradle插件

(2) 将Module里面的内容删除,只保留build.gradle文件和src/main目录。由于gradle是基于groovy,因此,我们开发的gradle插件相当于一个groovy项目。所以需要在main目录下新建groovy目录
然后把Nuwa插件NuwaGradle-master\src\main\groovy 下的文件拷入该文件

(3)其中,build.gradle内容为:

apply plugin: 'groovy'

dependencies {
    compile gradleApi()//gradle sdk
    compile localGroovy()//groovy sdk
}

repositories {
    jcenter()
}

dependencies {
    compile gradleApi()
    compile "commons-io:commons-io:1.4"
    compile 'commons-codec:commons-codec:1.6'

}

(4)到现在插件源码基本完成了,那么你的项目如何使用呢?
在app这个Module中如何使用呢?直接在app的build.gradle下加入代码

apply plugin: com.mile.nuwa.buildsrc.NuwaPlugin

为什么是com.mile.nuwa.buildsrc.NuwaPlugin呢 因为我nuwa插件的路径是这个如图
这里写图片描述

如果你是用nuwa的可能是cn.jiajixin.nuwa.NuwaPlugin

b、运行中遇到的bug
好了到这一步我们该运行了 bulid apk 发现报出如下错误

Error:Execution failed for task ':app:nuwaJarBeforeDexOtherRelease'.
> org/objectweb/asm/ClassReader

发现找不到ClassReader
就把自己studio中的asm-all-5.0.3.jar jar 包放入插件的module的libs下如下图
这里写图片描述

并在插件的build.gradle 添加compile files(‘libs/asm-all-5.0.3.jar’) 总体代码如下

apply plugin: 'groovy'

dependencies {
    compile gradleApi()//gradle sdk
    compile localGroovy()//groovy sdk
}

repositories {
    jcenter()
}

dependencies {
    compile gradleApi()
    compile "commons-io:commons-io:1.4"
    compile 'commons-codec:commons-codec:1.6'
    compile files('libs/asm-all-5.0.3.jar')

}

这样基本上就可以运行了 ~~~~下面再带大家看看这个gradle插件都做了哪些事情

c、我们来看看插件做了哪些事情

下面是我加了注释的代码。上代码主要是这个类NuwaPlugin.groovy

package com.mile.nuwa.buildsrc

import com.mile.nuwa.buildsrc.util.*
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Plugin
import org.gradle.api.Project


class NuwaPlugin implements Plugin<Project> {
    HashSet<String> includePackage
    HashSet<String> excludeClass
    def debugOn             //是否生成debug版本的
    def patchList = []      //补丁包文件夹集合
    def beforeDexTasks = []
    private static final String NUWA_DIR = "NuwaDir"    //第一次生成的nuwa文件夹需要考出来,这个就是拷出来的位置
    private static final String NUWA_PATCHES = "nuwaPatches"

    private static final String MAPPING_TXT = "mapping.txt"//混淆前后关系表
    private static final String HASH_TXT = "hash.txt"//记录每个类的hash值 作为下次对比

    private static final String DEBUG = "debug"


    @Override
    void apply(Project project) {

        println("**********************************************start");

        //创建了一个名字为nuwa的扩展容器 容器就是NuwaExtension
        project.extensions.create("nuwa", NuwaExtension, project)

        //项目评估完走这一步
        project.afterEvaluate {
            //获取nvwa这个扩展容器
            def extension = project.extensions.findByName("nuwa") as NuwaExtension
            includePackage = extension.includePackage//Hashset<String>
            excludeClass = extension.excludeClass//Hashset<String>
            debugOn = extension.debugOn

            //遍历android中的build的不同版本(如debug 与 release)
            project.android.applicationVariants.each { variant ->
                //判断是否编译debug版的
                if (!variant.name.contains(DEBUG) || (variant.name.contains(DEBUG) && debugOn)) {

                    Map hashMap     //hash值对应的map
                    File nuwaDir    //nuwa的输出产物目录
                    File patchDir   //需要打patch的classes文件目录,会对比hash值,如果hash值不一样,会拷到这个目录
                    /**
                     *找到preDex,dex,proguard这三个task
                     */
                    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()}")//混淆

                    //找到manifest文件
                    def processManifestTask = project.tasks.findByName("process${variant.name.capitalize()}Manifest")
                    def manifestFile = processManifestTask.outputs.files.files[0]

                    //这个属性是从控制台输入的,代表之前release版本生成的混淆文件和hash文件目录,这两个文件发版时需要保持
                    //gradlew clean nuwaQihooDebugPatch -P NuwaDir=/Users/jason/Documents/nuwa
                    def oldNuwaDir = NuwaFileUtils.getFileFromProperty(project, NUWA_DIR)
                    if (oldNuwaDir) {
                        //如果文件夹存在的话混淆的时候应用mapping
                        def mappingFile = NuwaFileUtils.getVariantFile(oldNuwaDir, variant, MAPPING_TXT)
                        NuwaAndroidUtils.applymapping(proguardTask, mappingFile)//
                    }
                    if (oldNuwaDir) {
                        //如果文件夹存在的话获得各个class的hash
                        def hashFile = NuwaFileUtils.getVariantFile(oldNuwaDir, variant, HASH_TXT)
                        //将文件中的有bug的hash存入这个map
                        hashMap = NuwaMapUtils.parseMap(hashFile)
                    }

                    def dirName = variant.dirName// (debug 与 release)
                    nuwaDir = new File("${project.buildDir}/outputs/nuwa")///build/outputs/nuwa/
                    def outputDir = new File("${nuwaDir}/${dirName}")//不同build版本的文件夹不同

                    /**
                     * hash文件
                     * /build/outputs/nuwa/qihoo/debug/hash.txt
                     */
                    def hashFile = new File(outputDir, "hash.txt")

                    /**
                     * 创建相关文件的闭包
                     */
                    Closure nuwaPrepareClosure = {
                        //获得application名字
                        println("***********manifestFile:"+manifestFile.toString())

                        def applicationName = NuwaAndroidUtils.getApplication(manifestFile)
                        println("***********getApplication name:"+applicationName)
                        if (applicationName != null) {
                            excludeClass.add(applicationName)
                        }
                        //创建相关文件夹
                        outputDir.mkdirs()
                        if (!hashFile.exists()) {
                            hashFile.createNewFile()
                        }

                        if (oldNuwaDir) {
                            patchDir = new File("${nuwaDir}/${dirName}/patch")
                            patchDir.mkdirs()
                            patchList.add(patchDir)
                        }
                    }

                    def nuwaPatch = "nuwa${variant.name.capitalize()}Patch"
                    //创建了一个nuwaPatch的task并加入把闭包中的操作加入该task的action队列中的最后一个
                    project.task(nuwaPatch) << {
                        if (patchDir) {
                            NuwaAndroidUtils.dex(project, patchDir)//把修改的转成java文件转成dex文件
                        }
                    }
                    def nuwaPatchTask = project.tasks[nuwaPatch]

                    Closure copyMappingClosure = {
                        //将构建产生的mapping文件拷贝至目标nuwa目录
                        if (proguardTask) {
                            def mapFile = new File("${project.buildDir}/outputs/mapping/${variant.dirName}/mapping.txt")
                            def newMapFile = new File("${nuwaDir}/${variant.dirName}/mapping.txt");
                            FileUtils.copyFile(mapFile, newMapFile)
                        }
                    }

                    /**
                     * 了解到preDex会在dex任务之前把所有的库工程和第三方jar包提前打成dex,
                     * 下次运行只需重新dex被修改的库,以此节省时间。
                     * dex任务会把preDex生成的dex文件和主工程中的class文件一起生成class.dex
                     */
                    if (preDexTask) {
                        println("非混淆的******************")
                        //这个Task存在的情况,即没有开启Multidex
                        def nuwaJarBeforePreDex = "nuwaJarBeforePreDex${variant.name.capitalize()}"
                        //创建一个nuwaJarBeforePreDex名字的task 并且加入一个action(闭包里的操作)到task的action队里的最后
                        project.task(nuwaJarBeforePreDex) << {
                            Set<File> inputFiles = preDexTask.inputs.files.files
                            inputFiles.each { inputFile ->
                                def path = inputFile.absolutePath
                                println("nuwaJarBeforePreDex:"+path)
                                if (NuwaProcessor.shouldProcessPreDexJar(path)) {
                                    println("==============:"+includePackage.toString()+"***************:"+excludeClass.toString())
                                    NuwaProcessor.processJar(hashFile, inputFile, patchDir, hashMap, includePackage, excludeClass)
                                }else{
                                    /*println("******************classes.jar is null")*/
                                }
                            }
                        }
                        def nuwaJarBeforePreDexTask = project.tasks[nuwaJarBeforePreDex]
                        nuwaJarBeforePreDexTask.dependsOn preDexTask.taskDependencies.getDependencies(preDexTask)
                        preDexTask.dependsOn nuwaJarBeforePreDexTask

                        nuwaJarBeforePreDexTask.doFirst(nuwaPrepareClosure)

                        def nuwaClassBeforeDex = "nuwaClassBeforeDex${variant.name.capitalize()}"
                        project.task(nuwaClassBeforeDex) << {
                            Set<File> inputFiles = dexTask.inputs.files.files
                            inputFiles.each { inputFile ->
                                def path = inputFile.absolutePath
//                                println("******************nuwaClassBeforeDex:"+path)
                                if (path.endsWith(".class") && !path.contains("/R\$") && !path.endsWith("/R.class") && !path.endsWith("/BuildConfig.class") &&!path.contains("R\$")
                                        && !path.endsWith("R.class") && !path.endsWith("BuildConfig.class")) {
                                    println("==============:"+includePackage.toString()+"***************:"+excludeClass.toString())
                                    if (NuwaSetUtils.isIncluded(path, includePackage)) {
                                        if (!NuwaSetUtils.isExcluded(path, excludeClass)) {
                                            def bytes = NuwaProcessor.processClass(inputFile)
                                            println("come in path:"+path)
                                            println("come in dirName:"+dirName)
                                            String mSplit = "${dirName}"
                                            path = path.split(mSplit)[1]
                                            path = path.substring(1,path.size())
                                            println("path.split:"+path)
                                            def hash = DigestUtils.shaHex(bytes)
                                            hashFile.append(NuwaMapUtils.format(path, hash))
                                            println("******************isIncluded:"+path)
                                            if (NuwaMapUtils.notSame(hashMap, path, hash)) {
                                                NuwaFileUtils.copyBytesToFile(inputFile.bytes, NuwaFileUtils.touchFile(patchDir, path))
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        def nuwaClassBeforeDexTask = project.tasks[nuwaClassBeforeDex]
                        nuwaClassBeforeDexTask.dependsOn dexTask.taskDependencies.getDependencies(dexTask)
                        dexTask.dependsOn nuwaClassBeforeDexTask

                        nuwaClassBeforeDexTask.doLast(copyMappingClosure)

                        nuwaPatchTask.dependsOn nuwaClassBeforeDexTask
                        beforeDexTasks.add(nuwaClassBeforeDexTask)
                    } else {
                        println("混淆的******************")
                        /**
                         * 如果preDexTask这个task不存在,即开启了Multidex
                         * dex任务之前会生成一个jar文件,包含了所有的class,即使做了混淆也是一个jar
                         * 这种情况下只对jar进行处理即可
                         */
                        def nuwaJarBeforeDex = "nuwaJarBeforeDex${variant.name.capitalize()}"
                        //创建一个nuwaJarBeforeDex的task并添加下面闭包的任务到action队里的最后一个
                        project.task(nuwaJarBeforeDex) << {
                            println("nuwaJarBeforeDex:inputFile")
                            Set<File> inputFiles = dexTask.inputs.files.files//获取build生成的classes.jar
                            println("inputFiles:"+inputFiles.toString());
                            //遍历所有文件
                            for(File inputFile:inputFiles){
                                def path = inputFile.absolutePath
                                println("inputFiles each"+path);
                                //如果是以jar结尾,则对jar进行字节码注入处理
                                if (path.endsWith(".jar")) {
                                    println("==============:"+includePackage.toString()+"***************:"+excludeClass.toString())
                                    println("hashFile:"+hashFile+"::inputFile:"+inputFile+"::patchDir"+patchDir+"::includePackage:"+includePackage+"::excludeClass:"+excludeClass)
                                    NuwaProcessor.processJar(hashFile, inputFile, patchDir, hashMap, includePackage, excludeClass)
                                }
                            }
                            /*inputFiles.each { inputFile ->
                                def path = inputFile.absolutePath
                                println("inputFiles each"+path);
                                //如果是以jar结尾,则对jar进行字节码注入处理
                                if (path.endsWith(".jar")) {
                                    println("==============:"+includePackage.toString()+"***************:"+excludeClass.toString())
                                    NuwaProcessor.processJar(hashFile, inputFile, patchDir, hashMap, includePackage, excludeClass)
                                }
                            }*/
                        }
                        println("*************end")
                        //找到nuwaJarBeforeDex这个Task
                        def nuwaJarBeforeDexTask = project.tasks[nuwaJarBeforeDex]
                        nuwaJarBeforeDexTask.dependsOn dexTask.taskDependencies.getDependencies(dexTask)//nuwaJarBeforeDexTask指定依赖dexTask
                        dexTask.dependsOn nuwaJarBeforeDexTask

                        nuwaJarBeforeDexTask.doFirst(nuwaPrepareClosure)//把nuwaPrepareClosure这个闭包放到task的action队列第一位
                        nuwaJarBeforeDexTask.doLast(copyMappingClosure)//把拷贝混淆文件的mapping的壁报放到task的action队列最后一位

                        nuwaPatchTask.dependsOn nuwaJarBeforeDexTask//nuwaPatchTask指定依赖nuwaJarBeforeDexTask
                        beforeDexTasks.add(nuwaJarBeforeDexTask)
                    }

                }
            }

            //创建一个task来,并把patchDir 中需要生产补丁包的class转成dex文件
            project.task(NUWA_PATCHES) << {
                patchList.each { patchDir ->
                    NuwaAndroidUtils.dex(project, patchDir)
                }
            }
            //遍历beforeDexTasks中的task并且让NUWA_PATCHES都每个都依赖他
            beforeDexTasks.each {
                project.tasks[NUWA_PATCHES].dependsOn it
            }
        }
    }
}


如果看的困难?稍后把我运行源码上传上来

源码下载

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值