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/NuwaNuwa 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 } } } }
如果看的困难?稍后把我运行源码上传上来
源码下载
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件