自定义gradle插件

前言:还记得前期做过一个android热修复的东西,其中有一个很重要的步骤就是通过javassist对jar进行字节码修改。当初修改字节码使用的是一个jar包。今天将为修改字节码这一步骤定义成一个gradle插件。

一、gradle的工作流

自定义gradle插件_第1张图片

1.1初始化阶段

在该阶段主要是做一些编译的前期准备工作,可以通俗的理解为解析settings.gradle
ps:
1.1.1gradle中的project
Gradle 中,每一个待编译的工程都叫一个 Project。每一个 Project 在构建的时候都包含一系列的 Task。每一个project都对应一个build.gradle,该project的task都定义build.gradle引用的插件里。
自定义gradle插件_第2张图片
如上图所示,总共有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'

1.2配置阶段

Configration阶段的目标是解析每个project中的build.gradle。比如multi-project build 例子中,解析每个子目录中的 build.gradle,每个 Project 都会被解析,其内部的任务也会被添加到一个有向 图里,用于解决执行过程中的依赖关系

1.3 执行阶段

你在 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任务链)

二、自定义插件(groovy)

扯了半天和主题无关的,言归正传。

2.1建立插件工程目录结构

2.1.1在电脑磁盘上新建一个文件夹
自定义gradle插件_第3张图片
2.1.2紧接着在里面新建一个文件build.gradle(可以不需要任何内容,只需要一个空壳文件,到这里其实我们已经建立好一个gradle project)
2.1.3 通过AS工具打开刚才新建的project
在这里你可能会问,为啥不直接使用AS创建一个project,要知道as默认创建的是一个android project
2.1.4 建立项目工程结构
自定义gradle插件_第4张图片
如图所示创建3个文件夹,请注意文件夹名字必须如上所示。

2.2 自定义插件编写

2.2.1 添加goovy依赖
我们先给这个Project添加一下依赖,因为我们最开始是通过新建文件夹的形式,然后在AS中导入这个项目,所以它还没有把groovy相关的包依赖进来。我们在项目名字上右键,选择Open Module Settings,然后添加Dependencies,如下图所示(网络盗图):
自定义gradle插件_第5张图片

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
自定义gradle插件_第6张图片
这里有三个参数:
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)
            }
        }

三、自定义插件发布

3.1 给插件命名

在发布插件时,得先命名好我们的插件,以便外部引用
自定义gradle插件_第7张图片

在META-INF.gradle-plugins下新建一个xxx.properties(图中的xxx是com.inject.ndh),xxx就是我们编写的插件的名字(上图表示插件的名字叫com.inject.ndh)。这里有一点需要注意,xxx一定不能同外部扩展属性同名(具体原因我没有搞明白),在xxx.properties里面配置好该插件的实现类:

// 等号 (=)左边是固定写法,右边是插件的实现类的全类名 
implementation-class = com.inject.ndh.InjectPlugin

3.2 插件发布

编写前期定义的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。发布完成后在仓库中可以看到:

自定义gradle插件_第8张图片

上图看到了1.0.0~1.0.3 四个文件夹,这表示我已经发布了4个版本的插件。点开1.0.3文件夹可以看到一系列的文件(别去删了哦),
反编译看看第一个inject-1.0.3.jar是啥:
自定义gradle插件_第9张图片
可以看到就是我们写的代码。 换句话说,groovy写的插件最终编译成了jar包,gradle引用的插件其实也是一个jar包。
3.3 外部引用该插件
3.3.1 根目录中的配置
在工程根目录的build.gradle里做如下配置
自定义gradle插件_第10张图片
第一处 在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

你可能感兴趣的:(android开发)