Gradle插件入门

462 阅读10分钟
   你的压力来源于,无法自律只是假装很努力,现状跟不上欲望,所以你焦虑又恐慌,诸君共勉!

问题

同样我们还是要抛出几个问题:

  • 什么是Gradle插件?
  • 我们引入插件有几种方式呢?
  • 插件该如何去写呢?

真实场景

其实Gradle是帮我们自动化构建的一套工具,Gradle插件我用一直在开发中使用,比如com.android.application,com.android.library,前者标识这是一个APP运行程序,后者标识当前module是一个依赖库,这个就是插件。插件可以给我们提供很多有用的功能,目前我们用插件,主要用来帮助我们进行自动化,比如字节码插桩,生成代码等,接下来我们就看看什么插件是怎么书写的,怎么运行的,我们不搞形式主义,就是告诉你怎么去书写自己的插件。

插件实现方式

  • build.gradle

    我们可以在build.gradle书写自己的函数(亦或者是一个Task),完成一个简单的功能,但是这个函数的可见性只有在这个文件可见,如果其他的gradle文件想用,只能引这个文件,比如通过apply方式。

  • 单独的module

    这个就比较方便了,这个module可以单独发布,其他任何仓库直接引入即可。

  • 工程buildSrc

    只有在当前工程可见可用

其中在build.gradle文件的方式,这里不展开讲解,很简单,估计大家也或多或少见过或者写过,接下来就详细讲解一下另外两种方式。

buildSrc方式构建插件

构建工程配置

image.png

image.png

image.png 首先创建名为”buildSrc“的文件夹(注意名字不能改变,只能是这个),然后可以本地Sync一下,然后创建一个resources的目录(和java目录同一个级别),然后在resources目录下创建META-INF目录,随后在META-INF目录下创建gradle-plugins目录,最后创建配置文件xxx.properties.(注意这个配置文件的名称也是有说法的,比如这里创建的com.judahao.buildSrc,这个名称是在apply插件的时候用到的,字符串名称是绑定的)

介绍build.gradle文件
// 你需要什么插件就引什么即可
apply plugin: 'java'
apply plugin: 'kotlin'

buildscript {
    ext.kotlin_version = '1.3.61'
    repositories {
        jcenter()
        mavenCentral()
    }
    dependencies {
        // 这个就是你用来的三方插件
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
    }
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

repositories {
    jcenter()
    google()
}

dependencies {
    // 注意,gradle插件开发需要引下面两个依赖,一个gradle开发的api,一个就是gradle的包
    implementation gradleApi()
    implementation 'com.android.tools.build:gradle:4.1.0'
    // ASM 相关,这个后面讲解ASM字节码插桩再说
    implementation 'org.ow2.asm:asm:9.2'
    implementation 'org.ow2.asm:asm-util:9.1'
    implementation 'org.ow2.asm:asm-commons:9.2'
    implementation 'androidx.room:room-compiler:2.3.0'
}

很简单,需要什么就依赖什么,注意依赖gradle的开发api和工具包即可。

创建Plugin
public class BuildSrcPlugin implements Plugin<Project> {

  public static final String BUILD_SRC_EXTENSION_NAME = "buildSrc";
  public static final String ANDROID_EXTENSION_NAME = "android";

  @Override
  public void apply(@Nonnull Project project) {
    // 注册transform,这里可以先不关注,后面讲ASM插桩会讲到
    Objects.requireNonNull(project.getExtensions().findByType(AppExtension.class))
        .registerTransform(new LogTransform());
    // 获取扩展的配置属性
    BuildSrcExtension buildSrcExtension =
        project.getExtensions().create(BUILD_SRC_EXTENSION_NAME, BuildSrcExtension.class);
    // 这个时候没有值,因为还没有读取完毕配置文件
    System.out.println("--- BuildSrc Extension versionCode: " + buildSrcExtension.versionCode);
    System.out.println("--- BuildSrc Extension versionName: " + buildSrcExtension.versionName);
    System.out.println(buildSrcExtension.versionCode);
    // gradle读取配置完毕,注册一个监听
    project.afterEvaluate(evaluatedProject -> {
      // 获取android打包的扩展配置信息
      AppExtension appExtension =
          (AppExtension) project.getExtensions().findByName(ANDROID_EXTENSION_NAME);
      if (appExtension != null) {
       // 获取变体(简单理解就是打出一种类型的包)
        DomainObjectSet<ApplicationVariant> applicationVariants =
            appExtension.getApplicationVariants();
        for (ApplicationVariant applicationVariant : applicationVariants) {
          // 如果当前是debug的包,给Debug添加Task
          if (applicationVariant.getBuildType().getName().equalsIgnoreCase("debug")) {
            // 创建一个自定义的任务
            BuildSrcTask packageTask =project.getTasks().create("BuildSrcTask" + applicationVariant.getName(), BuildSrcTask.class);
            // 初始化必要属性
            packageTask.initPackageTask(applicationVariant, project);
            // 投建依赖关系,让自定的Task依赖debug打包的Task,debug打包的Task依赖clean,
            // 保证每次运行都clean一下
            Task debugTask = applicationVariant.getAssembleProvider().get();
            debugTask.dependsOn(project.getTasks().findByName("clean"));
            packageTask.dependsOn(debugTask);
          }
        }
      }
    });
  }

}

TIP:什么是variant变体,其实打包有两种行为,buildType+flavorType,buildType就是我们常说的构建类型,比如debug,release(这两个是默认提供的,当然你是可以修改的,一般来说是不会动的),后者的flavor是干嘛的呢?比如说,我们做中台SDK,提供一个卖货的功能,我需要给ABCD四个APP使用,四个APP需要的卖货的功能基本一致,只有些微的查边,他们的UI主样式也是不一样的(比如有的主色点白的,有的蓝色,有的绿色),这个时候我们就可以用flavor了,四个flavor主要是有区别的代码,我们可以放在四个文件夹,打包的时候我们只需要配置好,flavorType,打包的时候会自己去找对应的目录去打包(和免费版付费版差不多,都可以用flavor)。所以,打包的类型一共有buildType*flavorType种,比如debugA,releaseD。

PS:不要问我为啥不用if-else,要用flavor,不同的颜色,我可以通过if-else(switch)区分啊,之前面试管就这么问我,我懒得掰扯,google提供这么好的工具你不用,你去用if-else。

分析一下代码(Transform先不用管,也是一种Task其实):

  • 首先获取扩展,啥是扩展呢?简单,就是属性,Task运行的时候可能会需要参数,这个扩展就是用来写参数的。
  • 注册监听,在gradle文件配置读取完毕,gradle之间的依赖关系弄清楚之后,回调,我们在回调中处理自己的逻辑。
  • 获取所有的变体,就是打包的类型,我们获取debug类型的打包Task,让这个任务依赖”clean“的task,保证每次打该debug包,都会执行clean,同时我们创建了自己的Task,让自定义的Task依赖debug的打包Task。这样我们每次运行自己的Task,都可以保证先执行clean然后执行打debug最后执行我们的自定义Task。
具体的Task
public class BuildSrcTask extends DefaultTask {

  private BaseVariant mVariant;

  private Project mProject;

  public void initPackageTask(@Nonnull BaseVariant variant, @Nonnull Project project) {
    setGroup("pluginStudy");
    this.mProject = project;
    this.mVariant = variant;
  }

  @TaskAction
  public void runPackageCompleteTask() {
  // 获取自定义的扩展
    AppExtension appExtension =
        (AppExtension) mProject.getExtensions().findByName(BuildSrcPlugin.ANDROID_EXTENSION_NAME);
    // 获取输出
    for (BaseVariantOutput output : mVariant.getOutputs()) {
      File outputFile = output.getOutputFile();
      if (outputFile != null) {
        // 打印APP信息
        System.out.println("# applicationId = " + mVariant.getMergedFlavor().getApplicationId());
        System.out.println("# fileName = " + outputFile.getAbsoluteFile());
        assert appExtension != null;
        System.out.println("# versionName = " + appExtension.getDefaultConfig().getVersionName());
        System.out.println("# versionCode = " + appExtension.getDefaultConfig().getVersionCode());
      }
    }
  }
}

TIP:setGroup方法,是可以让你的Task归类吧,方便寻找,如下方截图所示。 image.png

  • 同样,我们先获取自己的扩展,自己的配置属性对象Model。
  • 然后获取变体的输出,并打印,很简单,我们这个时候其实是可以拿到产物包了,比如你想上传,然后通过机器人告诉你包打好了,都行,随意老铁。
自定义扩展
public class BuildSrcExtension {

  public int versionCode;

  public String versionName;

  // public也不行,要get set方法

  public void setVersionCode(int versionCode) {
    this.versionCode = versionCode;
  }

  public void setVersionName(String versionName) {
    this.versionName = versionName;
  }
}
}

就是一个model对象,比如可以在里面配置versionCode和versionName,很灵活,这里就是做一个演示,你当然可以扩展里面放对象对吧,有兴趣可以自己了解下,主要就是让我们存放参数的。注意注意,一定要有set方法,我发现在Java里面,就算是public的也会运行报错,扩展在运行的时候会读取你在gradle文件的配置,给对象复制,只会调用set方法。

自定义插件使用

首先再次说明一下,buildSrc是工程可见的,所以不需要再根目录的build.gradle配置依赖。 image.png 我们只需要在想要使用的module引入即可,首先依赖plugin(这里是plugins,以前是通过apply的,gradle升级会有所变化,不用纠结),然后我们就可以使用扩展了,这个名称也是自己定义的,属性也是我们定义的,当然扩展可以包括一个扩展,映射到Extension,就是对象里面包括两另外一个对象,有兴趣自己了解下,很简单。很简单,这样buildSrc的插件就完成了。

单独module构建插件

构建工程配置

image.png

image.png 首先setting.gradle需要把这个module引进来(对了我看到网上说buildSrc也需要引入,但是我本地发现引入报错,不用引入,不知道是不是网上的是不是很久之前的了),其余的配置是和BuildSrc一致的。

介绍build.gradle文件
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'maven'
apply plugin: 'maven-publish'

buildscript {
    ext.kotlin_version = '1.3.61'
    repositories {
        jcenter()
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
    }
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

repositories {
    jcenter()
    google()
}

dependencies {
    implementation gradleApi()
    implementation 'com.android.tools.build:gradle:4.1.0'
}

// Maven,这个脚本用来发布插件的,这里模拟的是本地发布
// 如果你一来了本地module的插件,要测试,注意程序运行起来之前,要先发布本地插件不然跑不过,因为找不到
uploadArchives {
    repositories {
        mavenDeployer {
            //提交到远程服务器:
            //repository(url: "http://www.xxx.com/repos") {
            //authentication(userName: "admin", password: "admin")
            //}
            //本地的Maven地址设置为项目下的/localMaven的文件夹
            repository(url: uri('/Users/judahao/Downloads/'))
        }
    }
}

//uploadArchives {
//    repositories {
//        mavenDeployer {
//            pom.groupId = 'your.package.name'
//            pom.artifactId = "your_plugin_name"
//            pom.version = '1.0.0'
//            repository(url: uri('../repo'))
//        }
//    }
//}

// Maven-publish
publishing {
    publications {
        mavenJava(MavenPublication) {
            groupId 'com.judahao.module'
            artifactId 'plugin'
            version '1.0.0'
            from components.java
        }
    }
    repositories {
        maven {
            url uri('/Users/judahao/Downloads/')
        }
    }
}

很简单,需要什么就依赖什么,注意依赖gradle的开发api和工具包即可,但是注意一下,本地是要测试module的插件,本地依赖添加完毕之后,记得发布一下,不然你依赖有了,因为这个插件不像BuildSrc是工程可见的,插件远端找不到,不就会运行失败了吗。

创建Plugin
public class ModelPlugin implements Plugin<Project> {


  public static final String BUILD_SRC_EXTENSION_NAME = "module";
  public static final String ANDROID_EXTENSION_NAME = "android";

  @Override
  public void apply(@Nonnull Project project) {
    ModuleExtension buildSrcExtension =
        project.getExtensions().create(BUILD_SRC_EXTENSION_NAME, ModuleExtension.class);
    // 这个时候没有值
    System.out.println("--- Module Extension versionCode: " + buildSrcExtension.versionCode);
    System.out.println("--- Module Extension versionName: " + buildSrcExtension.versionName);
    System.out.println(buildSrcExtension.versionCode);
    // gradle读取配置完毕
    project.afterEvaluate(evaluatedProject -> {
      AppExtension appExtension =
          (AppExtension) project.getExtensions().findByName(ANDROID_EXTENSION_NAME);
      if (appExtension != null) {
//        appExtension.getApplicationVariants().all(applicationVariant -> {
//          applicationVariant.getOutputs().all(baseVariantOutput -> {
//            System.out.println(baseVariantOutput.getOutputFile());
//          });
//        });
        DomainObjectSet<ApplicationVariant> applicationVariants =
            appExtension.getApplicationVariants();
        for (ApplicationVariant applicationVariant : applicationVariants) {
          // 给Debug添加Task
          if (applicationVariant.getBuildType().getName().equalsIgnoreCase("debug")) {
            // 创建任务
            ModuleTask packageTask =
                project.getTasks()
                    .create("ModuleTask" + applicationVariant.getName(), ModuleTask.class);
            packageTask.initPackageTask(applicationVariant, project);
            // 投建依赖关系
            Task debugTask = applicationVariant.getAssembleProvider().get();
            debugTask.dependsOn(project.getTasks().findByName("clean"));
            packageTask.dependsOn(debugTask);
          }
        }
      }
    });
  }


}

不过多介绍,和BuildSrc的是一致的,前面有讲过。

具体的Task
public class ModuleTask extends DefaultTask {

  private BaseVariant mVariant;

  private Project mProject;

  public void initPackageTask(@Nonnull BaseVariant variant, @Nonnull Project project) {
    setGroup("pluginStudy");
    this.mProject = project;
    this.mVariant = variant;
  }

  @TaskAction
  public void runPackageCompleteTask() {
    AppExtension appExtension =
        (AppExtension) mProject.getExtensions().findByName(ModelPlugin.ANDROID_EXTENSION_NAME);
    // 获取输出
    for (BaseVariantOutput output : mVariant.getOutputs()) {
      File outputFile = output.getOutputFile();
      if (outputFile != null) {
        // 打印APP信息
        System.out.println("# applicationId = " + mVariant.getMergedFlavor().getApplicationId());
        System.out.println("# fileName = " + outputFile.getAbsoluteFile());
        assert appExtension != null;
        System.out.println("# versionName = " + appExtension.getDefaultConfig().getVersionName());
        System.out.println("# versionCode = " + appExtension.getDefaultConfig().getVersionCode());
      }
    }
  }
}

和buildSrc同理,一样的。

自定义扩展
public class ModuleExtension {

  public int versionCode;

  public String versionName;

  // public也不行,要get set方法

  public void setVersionCode(int versionCode) {
    this.versionCode = versionCode;
  }

  public void setVersionName(String versionName) {
    this.versionName = versionName;
  }
}

和buildSrc同理。

自定义插件使用

image.png 首先发布本地插件,保证程序运行远端可以找到。 image.png 添加插件的依赖,虽然是本地发布,但是模拟的是网络发布,没有什么区别。上面的插件,就是项目默认帮我们默认生成的插件依赖。 image.png 你需要使用的module直接配置插件依赖即可,齐活。

再次说一下,buildSrc是项目可见的,所以不需要再项目根目录的classPath配置东西,但是module是网上发布的,所以需要配置路径依赖。

小结

总体来说,插件的常用的两种方式已经叙述完毕,如果是项目用,我们在buildSrc写就可以了,但是追求复用,当时是单独的module去书写插件,语言无所谓,java,kotlin,groovy都是可以的看你习惯。

上面的问题估计也都解决了,大家需要知道插件的书写规范,怎么配置就可以了。

  • BuildSrc方式
    • 根目录创建BuildSrc目录
    • 创建main/resources/META-INT/gradle-plugins/xxx.properties文件,这个xxx配置文件名称是和apply ’xxx‘,名字保持一致的注意。
    • 开发自己的插件,实现自己的功能。
    • 最后在配置文件xxx.properties中书写’implementation-class=包名.Plugin‘,就是你目标插件的路径,把插件路径写进去。
  • 单独Module方式
    • 创建一个单独的Module,并在setting.gradle文件中引入
    • 创建main/resources/META-INT/gradle-plugins/xxx.properties文件,这个xxx配置文件名称是和apply ’xxx‘,名字保持一致的注意。
    • 开发自己的插件,实现自己的功能。
    • 配置文件xxx.properties中书写’implementation-class=包名.Plugin‘,就是你目标插件的路径,把插件路径写进去。
    • 完毕之后,发布你的目标插件到Maven中,你也可以到后台看看,你是不是成功发布。(账号密码是阿德细节,就别问我了bro)
    • 到项目的根目录的build.gradle,查找classPath,配置好你的发布插件依赖,包括groupId,artifactId以及版本号。
    • 然后在你需要的使用的module依赖,apply’xxx‘即可。