您的位置:首页 > 移动开发 > Android开发

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

2016-12-15 11:19 603 查看

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

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

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



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

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

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

一、补丁包生成原理

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



里面是.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源码,这个比较简单

2、从Nuwa Gradle插件地址中下载插件源码,NuwaGradle-master\src\main\groovy中就是gradle插件源码,

下面介绍如何一步步把这个个插件源码运行到你的项目中

a、创建Gradle Module

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



(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
}
}
}
}


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

源码下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息