前言:还记得前期做过一个android热修复的东西,其中有一个很重要的步骤就是通过javassist对jar进行字节码修改。当初修改字节码使用的是一个jar包。今天将为修改字节码这一步骤定义成一个gradle插件。
在该阶段主要是做一些编译的前期准备工作,可以通俗的理解为解析settings.gradle
ps:
1.1.1gradle中的project
Gradle 中,每一个待编译的工程都叫一个 Project。每一个 Project 在构建的时候都包含一系列的 Task。每一个project都对应一个build.gradle,该project的task都定义build.gradle引用的插件里。
如上图所示,总共有4个project,可以这么简单理解,谁的目录下有build.gradle 谁就是一个project
1.1.2 multi-projects build
我们有过这种经历,一个工程中的app模块需要引用到一个lib模块
但是当我们执行 ./gradlew iD 来安装app应用时,其实app和lib两个模块都有编译。这种模式叫multi-projects build。
Gradle 的 Multi-Projects Build 很容易,需要:
在 posdevice 下也添加一个 build.gradle。这个 build.gradle 一般干得活是:配置 其他子 Project 的。比如为子 Project 添加一些属性。这个 build.gradle 有没有都无所 属。
在 posdevice 下添加一个名为 settings.gradle。这个文件很重要,名字必须是 settings.gradle。它里边用来告诉 Gradle,这个 multiprojects 包含多少个子 Project。
来看 settings.gradle 的内容,最关键的内容就是告诉 Gradle 这个 multiprojects 包含哪些 子 projects。
//include ':app', ':mylibrary', ':hotfix' 这种配置也可以分开写成以下方式
include ':mylibrary'
include ':app'
include ':hotfix'
Configration阶段的目标是解析每个project中的build.gradle。比如multi-project build 例子中,解析每个子目录中的 build.gradle,每个 Project 都会被解析,其内部的任务也会被添加到一个有向 图里,用于解决执行过程中的依赖关系
你在 gradle xxx 中指定什么任务,gradle 就会将这个 xxx 任务链上的所有任务全部按依赖顺序执行一遍!
ps:
task的任务链由dependsOn进行串联
task a(dependsOn: 'b'){}
task b(dependsOn: 'c'){}
task b(dependsOn: 'c'){}
当你执行 ./gradle a 时,其任务真正执行顺序是 c->b->a(这就是一条task任务链)
扯了半天和主题无关的,言归正传。
2.1.1在电脑磁盘上新建一个文件夹
2.1.2紧接着在里面新建一个文件build.gradle(可以不需要任何内容,只需要一个空壳文件,到这里其实我们已经建立好一个gradle project)
2.1.3 通过AS工具打开刚才新建的project
在这里你可能会问,为啥不直接使用AS创建一个project,要知道as默认创建的是一个android project
2.1.4 建立项目工程结构
如图所示创建3个文件夹,请注意文件夹名字必须如上所示。
2.2.1 添加goovy依赖
我们先给这个Project添加一下依赖,因为我们最开始是通过新建文件夹的形式,然后在AS中导入这个项目,所以它还没有把groovy相关的包依赖进来。我们在项目名字上右键,选择Open Module Settings,然后添加Dependencies,如下图所示(网络盗图):
2.2.2创建类InjectPlugin(通过new->file形式新建InjectPlugin.groovy)
2.2.3实现接口 plugin
class InjectPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
}
}
2.2.4定义扩展属性
说扩展属性可能叫陌生,但是当你看到下面的配置,你会恍然大悟:
android {
compileSdkVersion sdkVersion //这些东西就是扩展属性Extension
buildToolsVersion "23.0.2"
defaultConfig {
minSdkVersion 9
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
我们要修改jar包的字节码,因此需要从外部传入待修改jar包路径,以及操作完之后存放路径,另外还需要一个编译该jar包的android.jar路径。
创建外部属性也很简单 在ExtensionContainer有相关api
https://docs.gradle.org/current/javadoc/org/gradle/api/plugins/ExtensionContainer.html
这里有三个参数:
name - The name for the extension (该名字会在build.gradle中使用,后续会有介绍)
type - The type of the extension(这是一个类,用于接收外部出入的参数)
constructionArguments - The arguments to be used to construct the extension instance(可选参数)
在我们工程中实现起来非常方便:
//创建扩展属性 injectConfig,并将外部属性配置使用InjectPluginExtension进行管理
project.extensions.create("injectConfig", InjectPluginExtension)
//插件获取外部配置扩展属性
roject.afterEvaluate {
//这里获取扩展属性时的名字,同上面定义的名字保持一致
extension = project['injectConfig'];
jarpath = extension.jarDir
outputdir = extension.outputDir
androidhome = extension.androidHome
}
现在我们来建立InjectPluginExtension.groovy
package com.inject.ndh
class InjectPluginExtension {
String outputDir;//操作结束后jar存放路径
String jarDir;//待操作jar路径
String androidHome//编译该jar的android.jar路径
}
外部build.gradle配置扩展属性是这样子的:
//这里的injectConfig同创建扩展属性的name参数名字保持一致
injectConfig{
//注入成功后 jar包存放位置
outputDir "${basedir}/mylibrary/out"
//待注入的jar包
jarDir "${basedir}/mylibrary/build/libs/mylib.jar"
//当前编译环境下的android.jar路径
androidHome "/Applications/AS/dv/android-sdk/platforms/android-${sdkVersion}/android.jar"
}
2.2.5 创建task
InjectPlugin–>apply方法中定义:
project.task("inject") << {
createDir(project)
initJar()
reBuildJar(project)
}
以上就定义了一个名为inject的task,当外部build.gradle引用了该插件后,通过./gradlew inject 就可以执行这里的方法。
createDir(project)方法:
def createDir(Project project) {
project.exec {
commandLine "mkdir", "-p", "${outputdir}"
}
}
这里表示通过命令行创建一个文件夹。其中的exec你可能会好奇是什么东西,当你看看前面一片gradle入门介绍里的taskType简介(http://blog.csdn.net/killer991684069/article/details/51767222)之后相信你会豁然开朗。在project里面有所有gradle原生taskType相关的api可以调用。
injectJar()几乎是纯java风格的代码,这里不做解释,有兴趣请看实例代码,代码下载地址在文章最后。
reBuildJar()主要执行jar重编译(javassist操作完字节码只会输出其class文件,我们需要将class文件重新打包成jar),以及将编译后的jar拷贝到指定目录:
def reBuildJar(Project project) {
println("${desJarName},${outputdir}")
project.exec {
//执行jar命令 进行打包
commandLine "jar", "-cvfM", "${desJarName}", "-C", "${outputdir}", "."
}
project.exec {
//执行mv命令,将编译后的jar包 移动到指定的输出目录
commandLine "mv", "${desJarName}", "${outputdir}"
}
}
2.2.6 hook已存在的task
这里我想在别人使用我的插件的使用,只要执行完build task,我就让它立即进行jar的字节码修改:
//获取build任务
project.tasks.getByName("build") {
//build任务执行完后立即执行jar的注入 也即是:你通过命令执行./gradlew build 编译好的jar包会立即被修改字节码
doLast {
println("START INJECT1!!" + jarpath)
createDir(project)
initJar()
reBuildJar(project)
}
}
在META-INF.gradle-plugins下新建一个xxx.properties(图中的xxx是com.inject.ndh),xxx就是我们编写的插件的名字(上图表示插件的名字叫com.inject.ndh)。这里有一点需要注意,xxx一定不能同外部扩展属性同名(具体原因我没有搞明白),在xxx.properties里面配置好该插件的实现类:
// 等号 (=)左边是固定写法,右边是插件的实现类的全类名
implementation-class = com.inject.ndh.InjectPlugin
编写前期定义的build.gradle文件
apply plugin: 'groovy'
apply plugin: 'maven'
//定义我们插件的版本号
version = '1.0.3'
//为我们的插件分组
group = 'com.inject.ndh'
archivesBaseName = 'inject'
repositories {
mavenCentral()
}
dependencies {
compile gradleApi()
compile localGroovy()
// 插件工程中 使用到了javassist
compile 'org.javassist:javassist:3.18.+'
}
// 一定要记得使用交叉编译选项,因为我们可能用很高的JDK版本编译,为了让安装了低版本的同学能用上我们写的插件,必须设定source和target
compileGroovy {
sourceCompatibility = 1.7
targetCompatibility = 1.7
options.encoding = "UTF-8"
}
uploadArchives {
repositories.mavenDeployer {
// 如果你公司或者自己搭了nexus私服,那么可以将插件deploy到上面去
// repository(url: "http://10.XXX.XXX.XXX:8080/nexus/content/repositories/releases/") {
// authentication(userName: "admin", password: "admin")
// }
// 如果没有私服的话,发布到本地也是ok的
repository(url: 'file:/Users/ndh/Desktop/mm/mm')
}
}
通过 ./gradlew uploadArchives 将我们的插件发布到相应的仓库,这里我发布到了本地 /Users/ndh/Desktop/mm/mm。发布完成后在仓库中可以看到:
上图看到了1.0.0~1.0.3 四个文件夹,这表示我已经发布了4个版本的插件。点开1.0.3文件夹可以看到一系列的文件(别去删了哦),
反编译看看第一个inject-1.0.3.jar是啥:
可以看到就是我们写的代码。 换句话说,groovy写的插件最终编译成了jar包,gradle引用的插件其实也是一个jar包。
3.3 外部引用该插件
3.3.1 根目录中的配置
在工程根目录的build.gradle里做如下配置
第一处 在buildscript 里配置我们的插件仓库路径(文中是发布到本地仓库)
第二处 在dependencies 引用我们的插件:这个地址由三部分组成
1 group (图中的com.inject.ndh 定义在插件工程build.gradle里 group = ‘com.inject.ndh’)
2 archivesBaseName (图中的inject定义在插件工程build.gradle里 archivesBaseName = ‘inject’)
3 版本号 (图中的1.0.3定义在插件工程build.gradle里version = ‘1.0.3’)
第三处在allprojects下配置插件仓库路径
3.3.2 实际工程中的引用
mylibrary/build.gradle中的配置
//引用插件 插件名字就是xxx.properties文件名
apply plugin: 'com.inject.ndh'
//配置扩展属性
injectConfig{
//注入成功后 jar包存放位置
outputDir "${basedir}/mylibrary/out"
//待注入的jar包
jarDir "${basedir}/mylibrary/build/libs/mylib.jar"
//当前编译环境下的android.jar路径
androidHome "/Applications/AS/dv/android-sdk/platforms/android-${sdkVersion}/android.jar"
}
四、实例下载
自定义插件工程地址:https://github.com/killer8000/Inject_gradle_plugin
插件引用工程地址下载:https://github.com/killer8000/HotFix_SDK2