自定义gradle插件入门

Android Gradle

Android项目使用 Gradle 作为构建框架,Gradle 又是以Groovy为脚本语言。所以学习Gradle之前需要先熟悉Groovy脚本语言。

Groovy是基于Java语言的脚本语言,所以它的语法和Java非常相似,但是具有比java更好的灵活性。下面就列举一些和Java的主要区别。

Android Gradle 的 Project 和 Tasks

这个Gradle中最重要的两个概念。每次构建(build)至少由一个project构成,一个project 由一到多个task构成。项目结构中的每个build.gradle文件代表一个project,在这编译脚本文件中可以定义一系列的task;task 本质上又是由一组被顺序执行的Action`对象构成,Action其实是一段代码块,类似于Java中的方法。

Android Gradle 构建生命周期

每次构建的执行本质上执行一系列的Task。某些Task可能依赖其他Task。那些没有依赖的Task总会被最先执行,而且每个Task只会被执行一遍。每次构建的依赖关系是在构建的配置阶段确定的。每次构建分为3个阶段:

  • Initialization: 初始化阶段

这是创建Project阶段,构建工具根据每个build.gradle文件创建出一个Project实例。初始化阶段会执行项目根目录下的settings.gradle文件,来分析哪些项目参与构建。

所以这个文件里面的内容经常是:

include ':app'

include ':libraries'

这是告诉Gradle这些项目需要编译,所以我们引入一些开源的项目的时候,需要在这里填上对应的项目名称,来告诉Gradle这些项目需要参与构建。

  • Configuration:配置阶段

这个阶段,通过执行构建脚本来为每个project创建并配置Task。配置阶段会去加载所有参与构建的项目的build.gradle文件,会将每个build.gradle文件实例化为一个Gradle的project对象。然后分析project之间的依赖关系,下载依赖文件,分析project下的task之间的依赖关系。

  • Execution:执行阶段

这是Task真正被执行的阶段,Gradle会根据依赖关系决定哪些Task需要被执行,以及执行的先后顺序。 
task是Gradle中的最小执行单元,我们所有的构建,编译,打包,debug,test等都是执行了某一个task,一个project可以有多个task,task之间可以互相依赖。例如我有两个task,taskA和taskB,指定taskA依赖taskB,然后执行taskA,这时会先去执行taskB,taskB执行完毕后在执行taskA。

说到这可能会有疑问,我翻遍了build.gradle也没看见一个task长啥样,有一种被欺骗的赶脚!

其实不是,你点击AndroidStudio右侧的一个Gradle按钮,会打开一个面板,内容差不多是这样的:

这里写图片描述

里面的每一个条目都是一个task,那这些task是哪来的呢?

一个是根目录下的 build.gradle 中的

dependencies {

   classpath 'com.android.tools.build:gradle:3.2.1'

}

一个是 app 目录下的 build.gradle 中的

apply plugin: 'com.android.application'

这两段代码决定的。也就是说,Gradle提供了一个框架,这个框架有一些运行的机制可以让你完成编译,但是至于怎么编译是由插件决定的。还好Google已经给我们写好了Android对应的Gradle工具,我们使用就可以了。

根目录下的build.gradle中 dependencies { classpath ‘com.android.tools.build:gradle:3.2.1’ } 是Android Gradle编译插件的版本。

app目录下的build.gradle中的apply plugin: ‘com.android.application’ 是引入了Android的应用构建项目,还有com.android.library和com.android.test用来构建library和测试。

所有Android构建需要执行的task都封装在工具里,如果你有一些特殊需求的话,也可以自己写一些task。那么对于开发一个Android应用来说,最关键的部分就是如何来用Android Gradle的插件了。

认知Gradle Wrapper

Android Studio中默认会使用 Gradle Wrapper 而不是直接使用Gradle。命令也是使用gradlew而不是gradle。这是因为gradle针对特定的开发环境的构建脚本,新的gradle可能不能兼容旧版的构建环境。为了解决这个问题,使用Gradle Wrapper 来间接使用 gradle。相当于在外边包裹了一个中间层。对开发者来说,直接使用Gradlew 即可,不需要关心 gradle的版本变化。Gradle Wrapper 会负责下载合适的的gradle版本来构建项目。

Android 三个文件重要的 gradle 文件

Gradle项目有3个重要的文件需要深入理解:项目根目录的 build.gradle , settings.gradle 和模块目录的 build.gradle 。

  • 1.settings.gradle 文件会在构建的 initialization 阶段被执行,它用于告诉构建系统哪些模块需要包含到构建过程中。对于单模块项目, settings.gradle 文件不是必需的。对于多模块项目,如果没有该文件,构建系统就不能知道该用到哪些模块。

  • 2.项目根目录的 build.gradle 文件用来配置针对所有模块的一些属性。它默认包含2个代码块:buildscript{…}和allprojects{…}。前者用于配置构建脚本所用到的代码库和依赖关系,后者用于定义所有模块需要用到的一些公共属性。

buildscript {
  repositories {
    jcenter()
  }

  dependencies {
     classpath 'com.android.tools.build:gradle:3.2.1'
  }
}


allprojects {
  repositories {
    jcenter()
  }
}


task clean(type: Delete) {
  delete rootProject.buildDir

}

 

buildscript:定义了 Android 编译工具的类路径。repositories中, jCenter是一个著名的 Maven 仓库。

allprojects:中定义的属性会被应用到所有 moudle 中,但是为了保证每个项目的独立性,我们一般不会在这里面操作太多共有的东西。

模块级配置文件 build.gradle 针对每个moudle 的配置,如果这里的定义的选项和顶层 build.gradle定义的相同。它有3个重要的代码块:plugin,android 和 dependencies。

定制项目属性(project properties)

在项目根目录的build.gradle配置文件中,我们可以定制适用于所有模块的属性,通过ext 代码块来实现。如下所示:

ext {
  compileSdkVersion = 28
  buildToolsVersion = "28.0.0"
}

然后我们可以在模块目录的build.gradle配置文件中引用这些属性,引用语法为rootProject.ext.{属性名}。如下:

android {
  compileSdkVersion rootProject.ext.compileSdkVersion
  buildToolsVersion rootProject.ext.buildToolsVersion
}

 

Android studio gradle Task

这里写图片描述

//构建

gradlew app:clean //移除所有的编译输出文件,比如apk

gradlew app:build //构建 app module ,构建任务,相当于同时执行了check任务和assemble任务

//检测

gradlew app:check //执行lint检测编译。

//打包

gradlew app:assemble //可以编译出release包和debug包,可以使用gradlew assembleRelease或者gradlew assembleDebug来单独编译一种包

gradlew app:assembleRelease //app module 打 release 包

gradlew app:assembleDebug //app module 打 debug 包

//安装,卸载

gradlew app:installDebug //安装 app 的 debug 包到手机上

gradlew app:uninstallDebug //卸载手机上 app 的 debug 包

gradlew app:uninstallRelease //卸载手机上 app 的 release 包

gradlew app:uninstallAll //卸载手机上所有 app 的包

这些都是基本的命令,在实际项目中会根据不同的配置,会对这些task 设置不同的依赖。比如 默认的 assmeble 会依赖 assembleDebug 和assembleRelease,如果直接执行assmeble,最后会编译debug,和release 的所有版本出来。如果我们只需要编译debug 版本,我们可以运行assembleDebug。

除此之外还有一些常用的新增的其他命令,比如 install命令,会将编译后的apk 安装到连接的设备。

gradle项目实战

最近参与基础架构组的crashly收集项目,其中一个模块就是收集项目中使用到的插件和sdk的四级(或三级)包路径和混淆后的mapping文件, 然后调用python文件实现文件和数据字段的上传。请求网络上传数据这块python代码量非常少就几行代码的事情,所以我在项目中使用了两个.gradle文件分别扫描项目中使用到的插件和sdk的四级(或三级)包路径,然后在.gradle文件中的task中调用python文件上传数据,这样工作就告一段落,然后当我找QA和负责打包的同事提交我的几个文件时才知道我这做法是多么lowbee, 这样实现需要每个业务线的项目都接入我的几个.gradle文件和.py文件,每次.gradle和.py文件修改在提交上去后各个业务线需要拉去最新的代码,这样的话我需要把各个业务线的项目源码都下载到本地,挨个改一遍在提交,这种做法没有做到通用性和可维护性。在同事的建议下,我又硬着头皮开始研究自定义gradle插件,每个业务线只需要apply插件就可以,这个方案不错,开始折腾gradle插件。

        首先说下功能就是定义2个任务,其中一个任务就是扫描业务方的项目过滤出使用到的四级(或三级)包路径,然后生成一个文件连同参数一起使用py脚本上传到后台。

        但是这里遇到了2个问题:

       1. 在插件module下的python文件在插件中是没法访问的,插件中是很容易拿到业务方的目录,如果将python文件放在业务方的项目中,同样是扩展性不好;

       2. 在插件中上传文件和其他参数我引入了第三方库的依赖,执行 uploadArchives 任务将代码上传到本地仓库后,在业务方demo中报错,无法访问到插件中的依赖。

        我在技术群里问了几遍结果都没有人回应,仅有的一个同行回复是遇到类似的问题,然后他的建议是去网上下载一个有类似功能的demo,然后修改下逻辑。顺着这条线索我去网上找到第四个才算是试成功了,下面将操作过程重演一遍。      

1)  首先新建一个Library库,删掉里面的文件最后保留和新建的文件夹如下图所示:

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

2) 删掉build.gradle中的内容,最终配置如下所示:

自定义gradle插件入门_第2张图片

3)  在groovy文件夹下新建包名并在该名下新建文件CrashlyPlugin.groovy, 内容如下:

自定义gradle插件入门_第3张图片

4)    在gradle-plugins文件夹下新建一个文件com.ke.crashly.plugin.properties, 内容如下:

5)    sync一下项目,然后找到开发工具右侧的Gradle下的:crashlyplugin -> Tasks -> upload -> 双击uploadArchives,就会在当前项目下生成一个目录crashlyrepo, 接下来就可以在项目中使用这个插件了;

6)    在工程的根目录下的build.gradle文件下添加如下代码:

自定义gradle插件入门_第4张图片

7)    在工程的主module的build.gradle文件最后添加如下代码:

自定义gradle插件入门_第5张图片

8)    sync一下工程,同样是在开发工具右侧的Gradle标签下,找到:app -> Tasks -> other ->  uploadRepoNameForCrashly控制台就会打印百度首页网页代码,也可以在Terminal下输入命令: gradle task -q uploadRepoNameForCrashly -s.

生命周期函数的应用

自定义gradle插件入门_第6张图片

project.gradle.buildFinished {
   // 可以上传mapping.txt文件
}
project.afterEvaluate {
  // 可以获取在build.gradle文件中的自定义的配置信息
  
  it.android.applicationVariants.all { variant ->
    // 当存在多个变体时,在这里可以遍历多个变体和applicationId, map的形式保存
    // 根据构建模式的信息最终确定applicationId
  }
}
project.gradle.startParameter.getTaskNames().each {
  // 命令行输入 ./gradlew clean assembleRelease -s 
  // 会输出 clean assembleRelease 
  // 可以利用这个信息确定构建模式 
}

gradle插件的三种形式

1. 在build.gradle中编写自定义插件

2. buildSrc工程项目

将插件的源代码放在rootProjectDir/buildSrc/src/main/groovy目录中,Gradle会自动识别来完成编译和测试。Android Studio会找rootPrjectDir/buildSrc/src/main/groovy目录下的代码。

坑:当新建libraray module并命名为buildSrc后会提示Plugin with id'com.android.library' not found.

这是因为buildSrc是Android的保留名称,只能作为plugin插件使用,我们修改buildSrc的build.gradle文件后就不报错了,且这个工程的构建时间最早。

3. 独立项目或独立Module

无论是在build.gradle中编写自定义插件,还是buildSrc项目中编写自定义插件,都只能在自己的项目中进行使用。如果想要分享给其他人或者自己用,可以在一个独立的项目中编写插件,这个项目会生成一个包含插件类的JAR文件,有了JAR文件就很容易进行分享了。

这里主要讲一下buildSrc工程项目, 这个工程可以支持多个gradle插件本地调试。

1. 目录结构如下所示

自定义gradle插件入门_第7张图片

2. buildSrc#build.gradle文件内容

apply plugin: 'groovy'
apply plugin: 'maven'

repositories {
  mavenCentral()
}

dependencies{
  //gradle sdk
  implementation gradleApi()
  //groovy sdk
  implementation localGroovy()
}

// 如果有多个gradle插件, 在下面添加文件路径
sourceSets {
  main {
    java.srcDirs = ['src/main/java'

    ]
    groovy.srcDirs = ['src/main/groovy',
        '../lifecycle_plugin/src/main/groovy'

    ]
    resources.srcDirs = ['src/main/resources',
        '../lifecycle_plugin/src/main/resources'

    ]
  }
}
3. gradle插件的build.gradle文件
apply plugin: 'groovy'

dependencies{
  //gradle sdk
  implementation gradleApi()
  //groovy sdk
  implementation localGroovy()
}
//以上配置比较固定


//以下内容主要用来上传插件
apply plugin: 'maven'
repositories {
  mavenCentral()
}

// classpath: 'group:name:version' // buildSrc模式下, name为module名称
group = 'com.ke.plugin'
version = '1.0.0-SNAPSHOT'


uploadArchives{
  repositories {
    mavenDeployer{
      repository(url: uri('../repositories')) // 和gradle插件同级目录下
    }
  }
}

4. gradle插件实现的功能, 拷贝mapping.txt文件到指定目录下

class LifeCyclePlugin implements Plugin {

  @Override
  void apply(Project project) {
    project.extensions.create("pluginConfig", PluginConfig)

    // 命令行输入 ./gradlew clean assembleRelease -s
    project.gradle.startParameter.taskNames.each {
      println("taskName: " + it.toString())
      // taskName: clean
      // taskName: assembleRelease
    }

    // writeReleaseApplicationId 在这个tasks里面
    project.afterEvaluate {
      project.tasks.each {
      }
      println("mappingPath ---> " + project.extensions.pluginConfig.mappingPath)
    }

    project.gradle.taskGraph.whenReady {
      project.gradle.taskGraph.allTasks.each {
      }
    }

    project.tasks.create(name: "copyMappingFileTask", type: Copy) {
//      from(new File('build/outputs/mapping/release/mapping.txt'))
      from(new File("build/outputs/mapping"))
      include '*/mapping.txt'
      into "build/output/mapping"
    }

    project.task('copyMappingFile') {
      doLast {
        try {
          File resultFile = new File(project.buildDir, "/output/mapping.txt")
          if (!resultFile.exists()) {
            resultFile.parentFile.mkdirs()
            resultFile.createNewFile()
          }
          List plugMappings = searchFiles(new File(project.buildDir.absolutePath + "/outputs/mapping"), "mapping.txt")
          if (plugMappings && plugMappings.size() > 0 && resultFile.exists()) {
            // 使用NIO来操作流
            FileChannel channel = new FileOutputStream(resultFile.absolutePath).getChannel()
            for (File file : plugMappings) {
              FileChannel fc = new FileInputStream(file).getChannel()
              channel.transferFrom(fc, channel.size(), fc.size())
              fc.close()
            }
            channel.close()
          }
        } catch (Exception e) {
          e.printStackTrace()
        }
      }
    }

    project.tasks.whenTaskAdded { task ->
      if ("assembleRelease".equals(task.name)) {
//        task.dependsOn(project.tasks.getByName('copyMappingFile'))
      }
      if ('transformClassesWithDexBuilderForRelease'.equals(task.name)) {
//        task.dependsOn(project.tasks.getByName('copyMappingFileTask'))
      }
    }
  }

  /**
   * 获取给定目录下指定文件名的文件集合
   */
  static List searchFiles(File folder, final String keyword) {
    List result = new ArrayList()

    File[] subFolders = folder.listFiles(new FileFilter() {
      @Override
      public boolean accept(File file) {
        if (file.isDirectory()) {
          return true
        }
        if (file.getName().toLowerCase().contains(keyword)) {
          return true
        }
        return false
      }
    })

    if (subFolders != null) {
      for (File file : subFolders) {
        if (file.isFile() && file.getName().toLowerCase().contains(keyword)) {
          // 如果是文件则将文件添加到结果列表中
          result.add(file)
        } else {
          // 如果是文件夹,则递归调用本方法,然后把所有的文件加到结果列表中
          result.addAll(searchFiles(file, keyword))
        }
      }
    }
    return result
  }
}

5. 本地使用该gradle插件

根目录下的build.gradle文件加入如下代码


buildscript {
    
    repositories {
       ...
        maven {
            // ../ 代表当前父目录的父目录
            url (uri('./repositories'))
        }
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'

        // group : PluginProjectName : version
        classpath 'com.ke.plugin:lifecycle_plugin:1.0.0-SNAPSHOT'
    }
}

module下的build.gradle添加一行代码

apply plugin: 'com.ke.lifecycle.plugin'

就可以在工程中调试gradle插件的功能。

 

针对共享插件, 修改的地方就是将产物上传到maven, 配置文件如下所示:

apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
group = GROUP
version = VERSION_NAME


def getPropertyFromLocalProperties(key) {
    File file = project.rootProject.file('local.properties');
    if (file.exists()) {
        Properties properties = new Properties()
        properties.load(file.newDataInputStream())
        return properties.getProperty(key)
    }
}

def getRepositoryUrl() {
    return isSnapshot() ? getPropertyFromLocalProperties("SNAPSHOT_REPOSITORY_URL") : getPropertyFromLocalProperties("RELEASE_REPOSITORY_URL")
//    return isSnapshot() ? SNAPSHOT_REPOSITORY_URL : RELEASE_REPOSITORY_URL
}

def isSnapshot() {
    return version.endsWith("SNAPSHOT");
}

def hasAndroidPlugin() {
    return getPlugins().inject(false) { a, b->
        def classStr = b.getClass().name
        def isAndroid = ("com.android.build.gradle.LibraryPlugin" == classStr) || ("com.android.build.gradle.AppPlugin" == classStr)
        a || isAndroid
    }
}
task sourcesJar(type: Jar) {
    if (hasAndroidPlugin()) {
        from android.sourceSets.main.java.srcDirs
        classifier = 'sources'
    } else {
        from sourceSets.main.allSource
        classifier = 'sources'
    }
}
artifacts {
    archives sourcesJar
}

uploadArchives {

//    repositories.mavenDeployer {
//        repository(url: uri('/Users/xxx/.m2/repository/')) // 本地仓库的路径
//        pom.groupId = "${project.group}"  //groupId ,自行定义,一般是包名
//        pom.artifactId = "${project.name}" //artifactId ,自行定义
//        pom.version = "${version}" //version 版本号
//    }

    repositories.mavenDeployer {
        repository(url: repositoryUrl) {
            authentication(userName: getPropertyFromLocalProperties("USER"), password: getPropertyFromLocalProperties("PASSWORD"))
        }
    }
}
bintray {
    user = getPropertyFromLocalProperties("bintray.user")
    key = getPropertyFromLocalProperties("bintray.apikey")
    configurations = ['archives']
    pkg {
        repo = 'maven'
        name = "${project.group}:${project.name}"
        userOrg = 'xxx'
        licenses = ['Apache-2.0']
        websiteUrl = 'xxx'
        vcsUrl = 'xxx'
        publish = true
    }
}
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'

一些常量信息都是在local.properties中配置。

Gradle plugin 调试

1. run - > Edit Configurations - > Remote  name = debug      

2. 命令行输入: ./gradlew clean build -Dorg.gradle.daemon=false -Dorg.gradle.debug=true

 

你可能感兴趣的:(项目实战)