Gradle Plugin

本质就是把独立的和业务代码无关的,各个项目、子项目通用的逻辑抽离出来进行封装。今天我们从零开始,从最简单的Demo逐渐深入,来演示下gradle的插件如何编写。

入门Demo:最简单的plugin之打印Hello World

首先我们写一个最简单的Android gradle plugin,就在我们APP module,主module的gradle文件里写一个打印helloWorld的插件,非常简单,如下所示:

class PluginDemo implements Plugin<Project> {
    @Override
    void apply(Project target) {
        println "Hello plugin"
    }
}

apply plugin: PluginDemo

实现了Plugin接口,实现apply方法即可,然后我们make下项目,就可以看到Hello plugin的打印了。在print里面我们是写死的字符串,改成变量的写法如下:

class PluginDemo implements Plugin<Project> {
    @Override
    void apply(Project target) {
        def extension = target.extensions.create('buder', ExtensionDemo)
        target.afterEvaluate {
            println "watch ${extension.name}"
        }
    }
}

class ExtensionDemo {
    def name = '庆余年'
}

apply plugin: PluginDemo

但是这么写是无法动态配置的,例如我们gradle配置的Android版本之类的信息是可以手动填写,可以自己动态配置的:

android {
    compileSdkVersion 28
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.example.plugin"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

升级demo:可以像上面versionCode "1"、versionName "1.0"这样可配置的插件写法

步骤一:在项目中创建一个新的module,可以选择一个java module,这个module的名字必须是buildSrc;

步骤二:然后在main目录下面创建一个resources的文件夹,里面再创建一个META-INF的文件夹,里面再创建一个gradle-plugins的文件夹,最后创建一个xxx.properties的文件(xxx文件名可以随意取);再新建groovy目录用于编写插件逻辑,最终如下图:

Android 插件化开发教程 安卓插件怎么写_bc

上面module以及resources目录的创建就是我们编写插件的基础,其实做法很简单,就是需要创建一个符合Android studio可以为我们编译为插件的一个目录结构,只是这个目录结构需要我们自己创建罢了,要是哪天AS可以支持创建项目或者创建module那样一键创建的话,这两个步骤就不需要我们自己手动创建了。下面我们要做的事就是把刚刚那个入门demo的代码,拆分到不同文件中,而非直接写在主工程的build.gradle文件内。

步骤三:拆分到升级demo的PluginDemo.groovy 以及 ExtensionDemo.groovy中

1.在代码目录下com.example.plugindemo,新建一个PluginDemo.groovy的文件,

class PluginDemo implements Plugin<Project>{

    @Override
    void apply(Project project) {
        def extension = project.extensions.create('plugindemo', ExtensionDemo)
        project.afterEvaluate {
            println "watch ${extension.name}"
        }
    }
}

这里注意:第一个参数是xxx.properties文件的文件名称

2.在代码目录下com.example.plugindemo,新建一个ExtensionDemo.groovy的文件,

class ExtensionDemo {
    def name = "kkkk"
}

3.在resources/META-INF/gradle-plugins目录下,新建一个plugindemo.properties的文件,

implementation-class=com.example.plugindemo.PluginDemo

 4.在主工程就是app工程的build.gradle文件中使用,

apply plugin: 'plugindemo'

plugindemo {
    name '《新世界》'
}

这里注意:这里的plugindemo就是之前的xxx.properties的文件名,这里起的是PluginDemo,这个名字可以随意起,但是一定要和xxx.properties的文件名保持一致。

上面的1--3步就是将原来入门demo的那几行代码拆分到Android studio所能识别到的文件中去,步骤4就是使用,达到和我们使用versionName "1.0" 一样的效果,make项目后就可以打印出:watch 《新世界》。

上述过程的工程目录:

Android 插件化开发教程 安卓插件怎么写_android_02

需要注意的几点:

1.plugindemo.properties里面的值就是PluginDemo.groovy的类的全限定名,
2.plugindemo.properties这个文件的文件名是可以随便起的,比如您起为abcd.properties
3.resources/META-INF/gradle-plugins/*.properties 中的 * 是插件的名称,例如*.properties 是 abcd.properties,最终在应用插件时的代码就是 apply plugin: 'abcd',在使用的时候,下面的就是您的这个properties的文件名必须保持一致
apply plugin: 'abcd'

abcd {
    name '新世界'
}
而且注意,PluginDemo.groovy中的extension创建函数里的第一个参数也是abcd,
def extension = project.extensions.create('plugindemo', ExtensionDemo)
4.关于 buildSrc ⽬目录
• 这是 gradle 的⼀一个特殊⽬目录,这个⽬目录的 build.gradle 会⾃自动被执⾏行行,即使不不配配置进settings.gradle
• buildSrc 的执⾏行行早于任何⼀一个 project,也早于 settings.gradle。它是⼀一个独⽴立的存在
• buildSrc 所配置出来的 Plugin 会被⾃自动添加到编译过程中的每⼀一个 project 的 classpath,因此它们才可以直接使⽤用 apply plugin: 'xxx' 的⽅方式来便便捷应⽤用这些 plugin
• settings.gradle 中如果配置了了 ':buildSrc' ,buildSrc ⽬目录就会被当做是⼦子 Project ,因此会被执⾏行行两遍。所以在 settings.gradle ⾥里里⾯面应该删掉 ':buildSrc' 的配置

添加Transform的Demo:

transform是什么:Transform是由 Android 提供的,在项⽬目构建过程中把编译后的⽂文件(jar ⽂文件和 class ⽂文件)添加自定义的中间处理过程的工具

步骤一:因为需要编写gradle代码,所以在buildSrc项目的gradle文件中需要加入依赖,才能使用gradle相关的API

repositories {
    google()
    jcenter()
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.tools.build:gradle:3.5.3'
}

步骤二:将之前的自定义的apply plugin放到后面,因为你添加transform,它的初始化太靠前了,否则会报错:

android {
    compileSdkVersion 28
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.example.plugin"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

apply plugin: 'plugindemo'

plugindemo {
    name '新世界'
}

dependencies {

步骤三:编写TransformDemo.groovy文件:

package com.example.plugindemo

import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager;

public class TransformDemo extends Transform{

    /**
     * 所添加的task名称
     * @return
     */
    @Override
    String getName() {
        return "buderTransform"
    }

    /**
     * 可以改class文件、jar包、资源文件等
     * 这里我们要是需要修改字节码,就为 TransformManager.CONTENT_CLASS
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 修改的范围
     * 这里为整个项目范围
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {

        //这里就是我们的class以及jar文件的输入和输出,输出的话就是我们做了手脚后的class文件
        def inputs = transformInvocation.inputs
        def outputProvider = transformInvocation.outputProvider

        inputs.each {
            it.jarInputs.each {
                File dest = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.JAR)
                FileUtils.copyFile(it.file, dest)
            }

            it.directoryInputs.each {
                File dest = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(it.file, dest)
            }
        }
    }

}

步骤四:在PluginDemo.groovy中使用:

import com.android.build.gradle.BaseExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class PluginDemo implements Plugin<Project>{

    @Override
    void apply(Project project) {
        def extension = project.extensions.create('plugindemo', ExtensionDemo)
        project.afterEvaluate {
            println "watch ${extension.name}"
        }

        def transform = new TransformDemo()
        def baseExtension = project.extensions.getByType(BaseExtension)
//        println "bootClassPath = ${baseExtension.bootClasspath} "
        baseExtension.registerTransform(transform)
    }
}

此项目地址:https://github.com/buder-cp/DesignPattern/tree/master/buder_coder_plugin