Gradle 从入门到放弃

幻昼 2023年09月15日 301次浏览

Groovy

  1. 类对象的访问

    class Person {
      private def name
    }
    
    static void main(String[] args){
      // 可通过参数名进行构造赋值
      def person = new Person(name: "kuky")
      // 可通过 @ 来访问私有变量
      println person.@name
    }
    
  2. 闭包,柯里化

    // 闭包可作为参数传给方法
    static def func(closure){
      closure()
    }
    
    // 左柯里化和右柯里化只支持两个参数的闭包
    // 如果多个参数的闭包需要通过基于索引柯里化进行处理
    static void main(String[] args){
      func {
        println 'test closure argument'
      }
      
      // 闭包
      def content = { String str, int count-> str * count }
      
      // 左柯里化,给第一个参数设置默认值,并返回一个新的闭包
      def multiContent = content.curry("test")
     	
      // 右柯里化,给最后一个参数设置默认值,并返回一个新的闭包
      def twiceContent = content.rcurry(2)
      
      println multiContent(2) == twiceContent("test") // true 计算的结果均为 "testtest"
      
      // 基于索引的柯里化
      def multiCalucate = { double a, double b, double c, double e -> a + b + c + e}
      
      // 第一位为参数位置索引 index,从 index 位开始给参数按顺序赋值
      def fixed = multiCalucate.ncurry(1, 2d, 3d)
      
      println multiCalucate(1d, 2d, 3d, 4d) == fixed(1d, 4d) // true 计算结果均为 10.0
    }
    
  3. 拦截/注入

    static void main(String[] args){
      // 通过重写 metaClass.invokeMethod 进行拦截
      // name 为方法名,Object arg 为方法带的参数
      String.metaClass.invokeMethod = {
        String name, Object arg->
        	def method = delegate.metaClass.getMetaMethod(name)
        	if (name == "toString" && method != null){
            println "toString was interceptted"
        	}
      }
      
      'aaa'.toString() // toString was interceptted
      
      // 通过 metaClass 注入方法
      String.metaClass.appendFunc = { int a->
        println "an append func for String calss, a=$a"
        'appendFunc end'
      }
      
      // an append func for String calss, a=1
      // appendFunc end
      println 'aaa'.appendFunc(1) 
      
      // 通过 Category 注入后,通过 use 即可使用
      use(StringUtilExt){
        println 1.isNotEmpty()
        println "1".isNotEmpty()
      }
    }
    
    // 通过 Category 注入方法
    @Category(Object)
    class StringUtilExt {
        def isNotEmpty() {
            println 'isNotEmptyExt'
            if (this instanceof Integer) {
                println '数字类型'
                return true
            }
    
            println '非数字类型'
            return this != null && this.length() > 0
        }
    }
    

Gradle

本文稿使用的 Gradle 环境为 7.3,不同版本之间会有 api 差异

Gradle构建周期

Gradle 构建周期 分为三个阶段: Initialization, Configuration, Execution

  • Initialization: 初始化阶段,支持单项目和多项目构,确定哪些项目将参与构建,并为项目创建 Project 实例
  • Configuration: 配置阶段,分析每个 build.gradle 文件,确定任务子集和任务的关系,并对任务做一些初始化工作
  • Execution: 执行阶段,根据配置阶段创建和配置的任务子集,执行任务
Settings

当有多个项目需要同时构建的时候,必须在根目录配置 settings.gradle 文件,在该文件中,配置了哪些项目进行多项目构建。除了配置多项目,该文件也可以配置一些 libraries。该配置文件会在 initialization 阶段执行,读取该配置文件并生成 Settings 对象。

Gradle 通过 settings.gradle 文件来确定是单项目构建还是多项目构建,同时也允许单个子项目单独进行构建,那么当执行没有 settings.gradle 文件的项目时,会通过以下方式进行查找

  • 查找父级目录是否存在 settings.gradle 文件
  • 如果没有找到,则按照单项目进行构建
  • 如果找到了,通过读取该文件,判断当前项目是否已经配置在多项目构建中,如果没有配置,则按照单项目构建,否则多项目构建

Gradle 会为每个参与构建的项目生成一个 Project 对象,每个对象的名称默认为顶层目录的名称,除了 rootProject,每个 Project 都会有一个父 Project,每个 Project 都可以有自己的子 Project

Task

Gradle Project 的本质是一个 Task 的集合,每个 Task 执行一些基本的工作,通过 TaskContainer.create() 构建 task 并添加到 project,添加的时候可以指定 Tasktype 例如指定 Zip 可执行文件压缩操作(也可通过自定义实现)。

// 通过 type 指定任务类型为 zip 压缩任务
// 执行任务后可以在 build/zips 目录下找到 outputs.zip 文件
// 如果目标文件不存在,构建后会提示 `> Task :zipFile NO-SOURCE`
task zipFile(type: Zip) {
    from("$buildDir/outputs") // 目标文件
    destinationDir(file("$buildDir/zips")) // 输出文件夹
    archiveName("outputs.zip") // 输出文件名
}

一个 Task 又是又一串 Action 对象组成,当 Task 被执行的时候,每个 Action 依次执行,可以通过 doFirst 或者 doLastTask 添加 Action

如果未配置 gradle,在每个 gradle 项目下有个 gradlew 文件,可通过该文件进行 gradle 命令操作

  • 使用 ./gradlew tasks --all 查看全部的任务
  • 使用 ./gradlew xxx 执行 xxx 任务

脚本构建基础

构建 task

创建任意空文件夹,在文件夹创建文件 build.gradle 并添加如下代码

tasks.register('count'){
	doLast{
		4.times { println it}
	}
}

执行命令行 gradle -q count,执行后文件夹会自动生成 .gradle 隐藏文件,控制台会输出 0 1 2 3

任务依赖

如果 Task A 的执行依赖于 Task B 的执行结果,可通过 dependsOn 处理依赖关系

tasks.register('hello'){
  println 'Hello world'
}

tasks.register('intro'){
  dependsOn hello
  println 'I am gradle'
}

执行命令 gradle -q intro,则会先执行 hello 任务再去执行 intro

defaultTasks 指定默认任务

可以通过 defaultTasks 指定默认执行任务,例如添加 defaultTasks 'count'后执行命令 gradle -q 则会自动执行 count 任务。

在多任务构建中,每个子项目都可以有自己的 defaultTask,如果该子项目没有 task 则默认使用父项目的

自定义 Task

自定义 Task 需要继承 DefaultTask 实现

abstract class BuildInfo extends DefaultTask {
  // input 注解为输入参数
  // 如果使用了 @Optinal 则表示该输入可选
  @Input
  abstract Property<String> getVersion()
  
  // OutputFile 表示为输出文件
  @OutputFile
  abstract RegularFileProperty getOutputFile()
    // TaskAction 表示任务的实际操作
  @TaskAction
  void setProperty() throws IOException{
    def property = new Properties()
    property.set("version", getVersion().get())
    
    def outputFile = getOutputFile().getAsFile().get()
    if (!outputFile.parentFile.exists()) outputFile.parentFile.mkdirs()
    if (!outputFile.exists()) outputFile.createNewFile()
    try(OutputStream output = new FileOutputStream(outputFile)){
      property.store(output, null)
    }
  }
}

// 在 task 指定任务类型 type 为 BuildInfo
tasks.register("buildInfo", BuildInfo){
  version = 'v1.0.0'
  // buildDirectory.file 返回 Provider<RegularFile> 对象
  outputFile = layout.buildDirectory.file('outputs/build-info.properties')
}

执行命令 gradle -q buildInfo 可以发现在对应的文件中写入了参数信息

使用 Gradle 扩展属性

使用 ext 闭包对任意对象属性进行扩展

  • project 进行使用 ext 进行属性扩展,对所有子 project 可见

  • 一般在 root project 中进行 ext 属性扩展,为子工程提供复用属性,通过 rootProject 直接访问

  • 任意对象都可以使用 ext 来添加属性: 使用闭包,在闭包中定义扩展属性。直接使用 = 赋值,添加扩展属性。

ext {
    springVersion = "3.1.0.RELEASE"
    emailNotification = "build@master.org"
}

tasks.register('check'){
  println springVersion
  println emailNotification
}

命令行执行 gradle -q check 可看到控制台的输出

扩展属性也可以在根目录的 gradle.properties 文件中定义

// gradle.properties
CURRENT_VERSION=V1.0.0

// build.gradle
println CURRENT_VERSION
配置任意对象
// 定义一个 user 类
class User {
   String name
   int age

   @Override
   public String toString() {
       return "User{" + "name='" + name + '\'' + ", age=" + age + '}';
   }
}

// 注册 task
tasks.register('userconfig') {
   doLast {
     	// 通过 configure 对对象进行赋值
       def user = configure(new User()) {
           name = 'kuky'
           age = 18
       }
       println user
   }
}

// 读取 `user.gradle` 文件中的配置
tasks.register('userfile') {
   dependsOn userconfig

   doLast {
       def user = new User()
       apply from: 'user.gradle', to: user
       println user
   }
}

// 在 user.gradle 文件中配置属性
name = 'kuky'
age = 18

执行命令行 gradle -q userfile 则会在控制台输出同样结果

Gradle 插件

Gradle 插件 分为脚本插件和二进制插件(通过 Plugin 接口实现)

  • 脚本插件

脚本插件的处理比较简单,通过 apply from: 即可

// 创建一个 listen.gradle 文件,用于做 gradle 的一些监听
// listen.gradle
allprojects {
    afterEvaluate { Project project ->
        println "project $project after evaluate "
    }
}

gradle.afterProject { Project project ->
    if (project.state.failure) {
        println "evaluate project: $project failed"
    } else {
        println "evaluate project: $project succeed"
    }
}

gradle.taskGraph.beforeTask { Task task ->
    println "executing task: $task...."
}

gradle.taskGraph.afterTask { Task task, TaskState state ->
    println state.failure ? "${task.name} failed" : "${task.name} done"
}

// 在 build.gradle 文件中引入该脚本文件
// build.gradle
apply from: 'listen.gradle' // apply from 引入脚本文件

tasks.register('ok')
tasks.register('broken') {
    it.dependsOn(ok)
    it.doLast { throw new RuntimeException("broken") }
}

执行命令行 gralde -q broken 可看到如下输出

  • 二进制插件

二进制插件需要 Plugin 接口实现

// 定义一个 ListenPlugin 类,内容和 ScriptPlugin 类似
class ListenPlugin implements Plugin<Project> {
    @Override
    void apply(Project pro) {
        pro.allprojects {
            afterEvaluate {Project project ->
                println "project $project after evaluate "
            }
        }

        pro.gradle.afterProject { Project project ->
            if (project.state.failure) {
                println "evaluate project: $project failed"
            } else {
                println "evaluate project: $project succeed"
            }
        }

        pro.gradle.taskGraph.beforeTask { Task task ->
            println "executing task: $task...."
        }

        pro.gradle.taskGraph.afterTask { Task task, TaskState state ->
            println state.failure ? "${task.name} failed" : "${task.name} done"
        }
    }
}

// 在 build.gradle 通过 apply plugin 引入二进制插件
// 替换 script plugin 的 apply from
apply plugin: ListenPlugin
// ... 

执行命令行 gralde -q broken 可看到相同的输出

多项目构建

多任务构建包含一个 rootProject 和一个或多个 subProject,必须在根目录配置 settings.gradle 文件

可以通过 gradle -q projects 来查看项目构建结构

假设 app 子项目是一个 java application,并通过 build.gradle 来配置 mainClass

plugins{ id "application" }

applications{
  mainClass = "com.kuky.Main"
}
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello Multi-Gradle-Project");
    }
}

执行命令行 gradle -q run 则会输出 Hello Multi-Gradle-Project

配置 subproject 只需要在 settings.build 中进行配置即可

rootProject.name = "GradleDemo"
// 通过 : 来定义项目路径,lib:libdoc 即为 lib 目录下的 libdoc 项目
include 'app', 'lib:libdoc', 'lib:libshare'

// 修改 libdoc 的项目名等,可通过 ProjectDescriptor 查看
// 获取某个 project 可以通过 `gradle -q projects` 得到的项目名进行获取
project(':lib:libdoc').name = 'doc'

settings.gradle 可以修改 gradle 项目的一些属性,具体可以参考 ProjectDescriptor

Gradle 依赖管理

dependency management resolution
声明存储库

Gradle 可以基于 MavenIvyflat 格式解析来自一个或多个存储库的依赖关系。

repositories 会转换成 RepositoryHandler 对象

repositories {
  // 多个存储库的时候,如果前面的存储库没有对应的依赖,就会按顺序去存储库查找
  // 引入 mavenCentral 和 google maven 存储库
  mavenCentral() 
  google() 
  // 通过 url 引入存储库
  maven {
    // 优先在 url 中查找,如果没有找到则在 artifactUrls 备用地址中找
    // artifactUrls 可多次配置
    url "https://repo2.mycompany.com/maven2" 
    artifactUrls "https://repo.mycompany.com/jars"
    
    // 存储库过滤
    content { 
      includeGroup "myCompany" // 只引入仓库中 'myCompany' 组中的内容
      excludeGroupByRegex "my\\.company.*" // 包含以 'myCompany' 开头的所有内容
    }    
  }
  
  // 当前项目下的依赖目录,也可声明多个 libs 目录
  flatDir{ dirs 'libs' }
  flatDir{ dirs 'lib1', 'lib2'}
  
  // Gradle 可以使用本地 Maven 存储库中可用的依赖项
  // Gradle 会将已解析的依赖项存储在自己的缓存中
  // 推荐在如下情况使用 mavenLocal `https://docs.gradle.org/current/userguide/declaring_repositories.html#sec:case-for-maven-local`
  mavenLocal()
  
  // 引入 ivy 存储库
  ivy { 
    // 最简单的引入只需要定义 url 即可
    url "https://repo2.mycompany.com/maven2"
    // 配置一些账号信息等
    credentials{
      username "username"
      password "passwod"
    }
  }
}
Gradle 依赖传递

项目之间的依赖传递关系可通过命令行 ./gradlew app:dependencies --configuration releaseRuntimeClasspath 查看

  1. xx.xx.xx(*) 表示已经有了 xx.xx.xx 依赖,不再重复依赖
  2. xx.xx.xx -> xxx.xxx.xxx 表示 xx.xx.xxxxx.xxx.xxx 代替
  3. xx.xx.xx -> xxx.xxx.xxx(*) 表示 xx.xx.xxxxx.xxx.xxx 代替,并且已经有依赖不再重复
移除冲突依赖

比如在项目中需要引入 com.google.android.material:material:1.4.0 依赖库,通过命令可以查看到依赖树(部分截图)

可以在依赖树看到子依赖都有依赖 androidx.annotation:annotation,如果说版本不一致肯定会导致依赖冲突,可通过如下解决

dependencies {
  	implementation ("com.google.android.material:material:1.4.0"){
  			// 移除对应的子依赖
 	 			exclude group:'androidx.annotation', module: 'annotation'
		}

				// 指定具体版本的依赖
		implementation("androidx.annotation:annotation:1.0.0")
}
强制指定版本
// 强制指定版本可通过如下处理
configurations.all {
    resolutionStrategy {
        force 'androidx.annotation:annotation:1.0.0'
    }
}

dependencies{
  //....
}
依赖约束力度
  • required 依赖某个版本,但如果别的依赖提供了更高的版本则优先用更高的版本,默认

  • strictly 严格限制某个版本,常用于依赖版本降级

dependencies {
  //	rxandroid 2.1.1 间接依赖了 rxjava:2.2.6
  implementation("io.reactivex.rxjava2:rxandroid:2.1.1") 
  // 严格约束 rxjava 版本为 2.2.0 
  // 约束版本也可使用区间 [2.0, 2.2.0]!!
  implementation("io.reactivex.rxjava2:rxjava:2.2.0!!")
}
使用 Version Catalog 实现依赖统一管理

7.x 之前的版本,如果多个模块需要实现同个版本的依赖管理,一般会通过 buildSrc 或者 composite build 来实现,从 7.x 开始,Gradle 引入了 version catalog,在 7.4 的版本正式稳定使用,如果需要在之前的版本使用,需要通过 settings.gradle 中通过 enableFeaturePreview("VERSION_CATALOGS") 开启。

  1. settings.gradle 中配置 version catalog

    // settings.gradle 文件
    dependencyResolutionManagement{
      // 在 versionCatalogs.libs 闭包中定义
      versionCatalogs{
        libs{ 
          // 使用 version 函数定义一个版本号,参数分别为 别名,版本号
          version("appcompat", "1.4.1")
          version("compose", "1.2.0-alpha07")
          
          // 使用 library 函数定义一个依赖,参数分别为
          // 参数 1 为依赖别名, 定义别名的时候,如果通过 -,_ 分割多个字符,依赖引入的时候需要通过 . 来替换分隔符
          // 例如 `android-appcompat` 在依赖的时候通过 libs.android.appcompat 引入
          // 参数 2 为依赖
          // versionRef 函数为对应的版本号
          library("android-appcompat", "androidx.appcompat:appcompat").versionRef("appcompat")
          library("compose-ui", "androidx.compose.ui:ui").versionRef("compose")
          library("compose-foundation", "androidx.compose.foundation:foundation").versionRef("compose")
          library("compose-compiler", "androidx.compose.compiler:compiler").versionRef("compose")
          
          // bundle 可以讲多个依赖组合成一个整体,通过 bundle 即可依赖
          // bundle 函数参数分别为别名和依赖列表
          // 在依赖侧,直接通过 `implementation(libs.bundles.compose)` 即可依赖
          bundle("compose", ["compose-ui", "compose-foundation", "compose-compiler"])
          
          // plugin 用来统一管理插件版本
          // 参数分别为别名和插件 id,通过 version 函数可以指定插件的版本
          // 在使用插件侧,需要通过 alias() 函数引入插件
          plugin("kotlin", "org.jetbrains.kotlin.android").version("1.6.20")
        }
        
        // catalog 允许多个 libs
        // 引入依赖的时候通过指定 libs 的名即可
        // testImplementation(testLibs.junit)
        testLibs{
          library("junit", "junit:junit:4.13.2")
        }
      }
    }
    
    
    // 根目录 build.gradle 文件
    plugins{
      // 通过 alias 引入定义的 kotlin 插件
      // apply false 表示不立刻解析该插件
      alias(libs.plugins.kotlin) apply false
    }
    
    
    // app/build.gradle 文件
    plugins{
      alias(libs.plugins.lotlin)
    }
    
    dependencies{
      testImplementation(testLibs.junit) // 引入 testLibs 下的 junit
      
      implementation(libs.android.appcompat) // 引入 appcompat 依赖
      implementation(libs.bundles.compose) // 引入 compose 组合依赖
    }
    
  2. toml 文件中配置 version catalog

    # libs.versions.toml
    # 除了在 `settings.gradle` 文件中配置 version catalog
    # 还可以通过 toml 文件进行配置
    [versions] # 在这里指定版本号
    compileSdk = 31 # 指定 compileSdk
    
    appcompat = "1.4.1"
    compose = "1.2.0-alpha07"
    
    [libraries] # 在这里指定依赖的版本
    android-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
    compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose"}
    compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
    compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compose" }
    
    [bundles] # 在这里指定依赖的集合
    compose = ["compose-ui", "compose-foundation", "compose-compiler"]
    
    [plugins] # 在这里指定插件
    kotlin = { id = "org.jetbrains.kotlin.android", version = "1.6.20" }
    

    处理完 toml 文件后,需要在 settings.gradle 中将 toml 文件指定给 version catalog

    // settings.gradle 文件
    versionCatalogs {
      libs {
        // 根据 toml 具体位置进行指定
        // 配置完成后就可以同上述一致进行依赖
        from(files("${rootDir.getAbsolutePath()}/libs.versions.toml"))
      }
      
      testLibs{
        // 指定测试依赖
        from(files("${rootDir.getAbsolutePath()}/testLibs.versions.toml"))
      }
    }
    
    // build.gradle 文件
    // 获取 versions 中的 compileSdk
    android{
      compileSdk libs.versions.compileSdk.get().toInteger()
    }
    

AGP(Android Gradle Plugin)

本文稿环境为 AGP 7.1.2,不同版本之间会有 api 差异

安利查看 AGP 源码的一个方法,直接在项目依赖对应 AGP 版本即可,不需要手动下载源码

implementation "com.android.tools.build:gradle:7.1.2"

新建 Android 项目,项目根目录包含一个 setting.gradlebuild.gradle 文件

settings.gradle 配置了多项目运行的一些基本配置,

pluginManagement 用于管理需要用到的插件,

dependencyResolutionManagement 用于管理需要用到的远程依赖,

以及 rootProjectsubProject,新建项目一般只有 app 一个 subProject

RepositoriesMode 用于配置加载策略,具体的可以查看 RepositoriesMode

build.gradle 通过 plugins 配置了用到的插件,通过 apply 使用,如果指定值为 false 表示该插件不会被立即解析使用,还注册了一个 clean taskDelete 通过实现 DeleteSpec 用于删除指定的文件,delete 为指定的路径,所以该 task 用于删除生成的 build 目录,可以在运行 app 后,执行 ./gradlew -q clean 查看,生成的 build 文件被删除

app/build.gradle 配置了 Android Application 的配置信息,通过 'com.android.application' 插件引入了 android {} 闭包,映射的类为 BaseAppModuleExtension, BaseAppModuleExtension 通过一系列的继承和实现,顶级父类为 BaseExtension, BaseExtension 为全部 Android Plugin 的父类,包括

  • ApplicationExtension - 用于创建 android application
  • LibraryExtension - 用于创建 android library
  • TestExtension - 用于创建测试
  • DynamicFeatureExtension - 用于创建一些动态属性

BaseExtension 实现了接口 AndroidConfig(旧版本的属性都在这,目前该接口已经过时,配置的额属性已迁移到 BaseExtension

defaultConfig

defaultConfig 映射为类 DefaultConfigDefaultConfig 通过继承 BaseFlavor,实现 BaseFlavor, VariantDimension 等接口及其子接口,来扩展属性

  1. applicationId && applicationIdSuffix 配置应用最终的包名,一般 suffix 被省略,可通过 applicationIdSuffix 来区分不同情况下的包名,例如

    buildTypes {
      release {
        applicationId 'com.xxx.app' // 构建应用的包名为 com.xxx.app
      }
      
      debug {
        initWith release
        applicationIdSuffix '.debug' // 构建应用的包名为 com.xxx.app.debug
      }
    }
    

    同样 versionNameSuffixversionName 是一样的效果

  2. externalNativeBuild 用于配置 cpp/c 的信息,具体可以参考 externalNativeBuild

  3. dimension 用于多渠道包的维度,在 defaultConfig 意义不大

  4. manifestPlaceholders 一般在多渠道配置不同的 AndroidManifest.xml 属性,例如给不同的包配置不同图标

    buidTypes {
      release {
        manifestPlaceholders = [icon: '@mipmap/app_icon']
      }
      
      debug {
        manifestPlaceholders = [icon: '@mipmap/app_debug_icon']
      }
    }
    

    配置 manifestPlaceholdersAndroidManifest.xml 种通过 ${key} 进行调用

    <application
                 icon="${icon}">
    	<activity/>
    </application>
    
  5. multiDexEnabled 用于开启 MultiDex

  6. multiDexKeepProguard 开启分包的时候将配置的类打进主包

    defaultConfig {
      // multidex-keep-rules.pro 为配置文件
      multiDexKeepProguard file('multidex-keep-rules.pro')
    }
    
  7. ndk 主要用于 ABI 的配置,同时也可以配置一些 cFlags, moduleName等等

    defaultConfig {
      ndk {
          abiFilters "armeabi-v7a", "arm64-v8a"
       }
    }
    
  8. javaCompileOptions 用于配置编译 java 时的一些参数

    defaultConfig {
      javaCompileOptions {
          annotationProcessorOptions {
              classNames 'xxxx' // 编译类,是个 vararg String
              arguments = ["arg1": "test"] // 编译时的参数
          }
       }
    }
    
  9. proguardFiles && testProguardFiles 混淆规则文件,AGP 自带了两个混淆规则 proguard-android.txtproguard-android-optimize.txt 文件,可通过 getDefaultProguardFile(String) 方法获取完整路径

  10. signingConfig 签名属性,一般在多渠道进行配置

  11. resConfig / resourceConfigurations 需要保留的资源

    defaultConfig {
      // resConfig, resConfigs 该版本已过时。使用 resourceConfigurations 代替
      resourceConfigurations = ['zh', 'en'] // apk 只保留中文和英文的资源
    }
    
  12. vectorDrawables 需要 minSdk < 21 的时候使用

    defaultConfig {
      vectorDrawables{
        	// 当 sdk < 21 时生成对应密度下的 png 图片
          generatedDensities 'mhdpi', 'xhdpi'
        	// true 会使 generatedDensities 属性失效,加入 svg 兼容包,不生成 png
          useSupportLibrary true
      }
    }
    
  13. buildConfigField(type: String, name: String, value: String) 方法用于在 BuildConfig 类中生成对应的参数

    defaultConfig {
      buildConfigField("String", "appName", "'xxxApp'") // 生成 String 类型的时候,需要用 '' 再次包裹
      buildConfigField("int", "versionCode", "1")
    }
    

    在类中可直接通过 BuildConfig.appNameBuildConfig.versionCode 直接调用

  14. resValue(type: String, name: String, value: String) 方法用于创建 res 属性

    defaultConfig {
        resValue('string', 'age', '12 years') // 生成 R.string.age 属性,可直接调用
      	resValue('color', 'black', '#000') // 生成 R.color.black
    }
    

buildTypes

buildTypes 映射类为 BuildType, BuildType 通过继承 AbstractBuildType, 实现 BuildType , VariantDimension 等接口及其一系列子接口扩展属性,所以其配置属性类似 defaultConfig 主要通过 VariantDimension 及其子接口确定,buildTypes 用于配置不同类型包的一些不同属性

// buildTypes 内置 release 和 debug 包,通过 `CommonExtensionImpl` 源码可以得知,
// release 和 debug 通过方法 getByName(String) 获取
buildTypes {
  release { // 也可以写成 getByName("release"){}
    
    // 开启代码压缩, 该属性由 AbstractBuildType 继承而来
    minifyEnabled true
    shrinkResources true // shrinkResources 需要在 minifyEnabled 开启的情况下开启
    
    // 以下属性同 defaultConfig,可用于配置不同类型包的特殊参数
    manifestPlaceholders = [icon: "@mipmap/ic_launcher"]
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
		buildConfigField('String', 'type', '"Release"')
  }
  
  debug { // 也可以写成 getByName("debug"){}
    // debuggable 属性由 AbstractBuildType 继承而来,在 debug 模式下默认为开启
    debuggable true 
    jniDebuggable true
    renderscriptDebuggable true
    
    // 
		applicationIdSuffix '.debug'
		manifestPlaceholders = [icon: "@mipmap/ava"]
		buildConfigField('String', 'type', '"Debug"')
  }
  
  custom { // 自定义属性包
    initWith debug // initWith 方法用于从别的类型下复制完全一样的属性
    
    //
		manifestPlaceholders = [icon: "@mipmap/ic_launcher"]
		applicationIdSuffix '.debugcustom'
		buildConfigField('String', 'type', '"Custom"')
  }
}
  1. minifyEnabled && shrinkResources 用于压缩代码和资源文件

  2. debuggable && jniDebuggable && renderscriptDebuggable 用于配置是否可断点调试,debug 模式下默认开启

  3. initWith 方法为复制相同属性来初始化当前的模式

    以上的方法和参数通过 AbstractBuildType 继承而来,别的属性和 defaultConfig 类似,通过 VariantDimension 接口及其子接口实现而来

构建不同变体可通过 Android Studio Build Variants 处理

signingConfigs

signingConfigs 映射类为 SigningConfig, 通过继承 DefaultSigningConfig, 实现SigningConfig 接口, 用于配置打包签名属性

  1. storeFile 指定签名文件,如果使用相对路径,是相对于设置该属性的 gradle 文件位置
  2. storePassword 签名文件密码
  3. keyAlias 签名的别名
  4. keyPassword 签名密码
  5. storeType 签名类型,不填时,默认为 jks 类型
  6. isV1SigningEnabled 是否使用 v1 签名,默认为true, 如果设置为 false,会导致在 7.0 以下无法正常安装
  7. isV2SigningEnabled 是否使用 v2 类型的签名方案。默认为 true,需要 7.0 以后支持

一般配置签名文件,官方建议通过在根目录创建 keystore.properties 文件来存放签名配置,再通过 gradle 引入

productFlavors && flavorDimensions

productFlavors 映射类为 ProductFlavorProductFlavor 通过继承 BaseFlavor,实现 ProductFlavor, VariantDimension 等接口及其子接口,来扩展属性

产品风味类似构建不同类型包,在 productFlavors 中也可以配置和 defaultConfig 一样的属性,因为 defaultConfig 也属于 ProductFlavor

flavorDimensions 为字符串列表,用于配置 productFlavorsdimension,所有的 productFlavor 都必须配置 dimension 属性(flavorDimensions 只有一个的情况下除外)

android {
  
  // 旧版本的通过 flavorDimensions 'abm', 'xxx' 配置
  // 当前版本该方法已过时,通过 add 方法添加即可
  flavorDimensions.add("abm")
  
  productFlavors {
    demo {
      dimension 'abm' // 单个 flavor 的情况下可以省略,会默认配置
      applicationIdSuffix '.demo'
    }
    
    v2 {
      // dimension 'abm' 可省略配置
      applicationIdSuffix '.v2'
    }
  }
}

通过 Build Variants 可以看到,生成了不同 flavor 的变体

配置多个 flavorDimensions 同单个类似,但是必须在 flavor 指定 dimension,多个 dimension 会有优先级,优先级按添加的顺序从高到低排列

android {
  buildTypes{
    debug {
      applicationIdSuffix '.debug'
    }
    
    release {
      applicationIdSuffix '.release'
    }
    
    custom {
      initWith debug
      applicationIdSuffix '.customdebug'
    }
  }
  
  flavorDimensions.add("api") // 新增一个 api dimension,优先级会高于 abm
  flavorDimensions.add("abm")
  
  // agp 会按照优先级对 product 进行组合
  // 最终变体的组合数量为 (api dimension 数量) * (abm dimension 数量) * (buildTypes 数量)
  // 例如本例中,api dimension 数量为 2,abm dimension 数量 2, buildTypes 数量为 3,最终有 12 个变体
  // 组合方式为 minV30v2Debug, minV30v2Release, minV30v2Custom ...
  // 组合包名方式为,取最高等级 dimension 的 applicationId,按照 dimension 优先级组合 applicationIdSuffix,
  // 再组合 buildTypes 的 applicationIdSuffix 得到最后的包名
  // 例如 minV30DemoDebug 的最终包名为 `com.kuky.api.min30.demo.debug`
  productFlavors {
    demo {
      applicationId 'com.kuky.abm'
      dimension 'abm'
      applicationIdSuffix '.demo'
    }
    
    v2 {
      dimension 'abm'
      applicationIdSuffix '.v2'
    }
    
    minV21 {
      dimension 'api'
      applicationIdSuffix '.min21'
      versionNameSuffix '-min21v'
      minSdk 21
		}

    minV30 {
      applicationId 'com.kuky.api'
      dimension 'api'
      applicationIdSuffix '.min30'
      minSdk 30
    }
  }
}

当依赖的 module 存在多种风味的情况下,嘿,那就好玩了,需要在 build 中使用 matchingFallbacks 指定对应的风味,matchingFallbacks 可传入 flavor 列表,编译的时候会按照顺序进行查找,如果不进行配置则无法编译成功。

// lib build.gradle
android {
  flavorDimensions.add('api')
  flavorDimensions.add('abm')
  
  productFlavors {
    free1 {
      dimension 'api'
    }
    
    pro1 {
      dimension 'api'
    }
    
    free {
      dimension 'abm'
    }
    
    pro {
      dimnsion 'abm'
    }
  }
}

// app build.gradle
productFlavors {
  demo {
    // 指定 lib 下的风味 dimension
    matchingFallbacks = ['free', 'pro']
  }
  
  minV21 {
    // 指定 lib 下的风味 dimension
    matchingFallbacks = ['pro1', 'free1']
  }
}

通过 productFlavors 可以配置多个变体,但是不是所有的变体都是我们需要的,那就需要通过过滤器 variantFilter 进行一些过滤操作

variantFilter

variantFilter 映射类为 VariantFilter, 主要有以下的属性

  1. ignore: Boolean 是否忽略当前变体
  2. defaultConfig: ProductFlavor 当前变体的 defaultConfig
  3. buildType: BuildType 当前变体的构建类型
  4. flavors: List<ProductFlavor> 当前变体的风味包
  5. name: String 当前变体名
android {
  variantFilter {variant->
    def flavorNames = variant.flavors*.name // *.name 获取 flavors 列表的所有 name 属性
    if (flavorNames.contains("minV30") && flavorNames.contains("v2")) {
      ignore = true // 忽略 minV30V2 开头的所有变体,那么在 `Build Variants` 中就没有这系列的变体了
    }
  }
}

sourceSets

sourceSets 映射到接口 AndroidSourceSet, 用于配置 java, aidl, RenderScript 源文件以及一些资源文件

  1. java 用于指定 java 资源集
  2. kotlin 用于指定 kotlin 资源集
  3. resources 用于指定 java 资源文件
  4. manifest 用于指定 AndroidManifest.xml 文件
  5. res 用于指定 Android 资源文件
  6. assets 用于指定 assets 目录
  7. aidl 用于指定 aidl 目录
  8. jniLibs 用于指定依赖库文件,旧版本为 jni
  9. renderscript 用于指定 Android 渲染脚本目录
  10. setRoot(String) 方法用于设置资源集的根目录

1 ~ 9 配置属性为 AndroidSourceDirectorySet, 通过 srcDirs 来配置资源文件目录集合

// 多渠道包的情况下,可以指定不同的目录文件夹
sourceSet{
  main { // main 为默认的
    jniLibs.srcDirs = ['libs']
  } 
  
  demo { // demo flavor 的配置, 不会覆盖 main 的属性
    // 旧版本有 excludes, includes 属性,用于排除一些文件目录
    // 当前版本源码未找到,可能是被移除?
    java.srcDirs = [
      'src/main/java',
      'src/main/filters' // 该文件夹只有在 demo flavor 的渠道下生效
    ]   
  }
}

lintOptions / lint

lintOptions 属性已过时,被 lint 代替,lint 主要用于协助发现代码质量问题,通过 ./gradlew app:lint 启动 lint 检查,运行完毕后会生成 lint-result 文件,也可以指定渠道包进行分析 ./gradlew app:lintAnalyzeMin21V2Debug

lint 的配置可通过 官方文档 查看,常用的属性应该是 abortOnError,发现错误的时候立即停止构建,构建 release 包的时候,该属性默认为打开,可能会遇到如下问题,可通过关闭解决

Lint found errors in the project; aborting build.
  
  Fix the issues identified by lint, or add the following to your build script to proceed with errors:
  android {
      lintOptions { // lintOptions 已被 lint 代替
          abortOnError false
      }
  }

splits

splits 映射为 Splits 类,主要用于 apk 拆分

  1. abi: AbiSplit 根据架构进行拆分
    • enbale 是否开启
    • exclude 需要移除的架构,如果在 defaultConfig 配置了 ndk.abiFilters 则需要在这些范围内进行选择
    • include 需要构建的架构,在设置 include 之前建议通过 reset() 方法重置包含目录
    • isUniversalApk 是否包含全架构
  2. density: DensitySplit 根据屏幕尺寸拆分
    • enbale 是否开启
    • exclude 需要移除的屏幕尺寸
    • include 需要构建的尺寸
    • compatibleScreens 指定与应用程序兼容的屏幕尺寸, 会在 AndroidManifest 添加 <compatible-screens> 节点
  3. abiFilters 配置的架构属性
  4. densityFilters 配置的屏幕尺寸属性
splits {
  abi {
    enable true
    exclude("x86", "x86_64")
  }

  density {
    enable true
    compatibleScreens "small", "large", "normal", "xlarge"
  }
}

配置完成后打包查看属性,架构移除了 x86, x86_64 并按照屏幕尺寸进行了分包,在 AndroidManifest 配置了对应的节点

externalNativeBuild

externalNativeBuild 映射类为 ExternalNativeBuild 实现接口 CoreExternalNativeBuildExternalNativeBuild, 用于控制 native 的编译

  1. cmake: Cmake
    • path 设置 CMakeLists.txt 的路径
    • buildStagingDirectory 配置 native 构建后文件的存放路径
    • version 设置 Android 编译 CMake 的版本
  2. ndkBuild: NdkBuild
    • path 设置 Android.mk 的路径
    • buildStagingDirectory 配置 native 构建后文件的存放路径

目前更多的项目会通过 cmake 来处理 native 编译

externalNativeBuild {
  cmake {
    path file('src/main/cpp/CMakeLists.txt')
    buildStagingDirectory "./outputs/cmake"
    version "3.18.1"
  }
}

aaptOptions / androidResources

aaptOptions 已过时,被 androidResources 代替,映射为接口 AndroidResources

  1. additionalParametersaapt 执行时添加额外参数,可添加的参数可通过 aapt --help 查看,或者文档

    androidResources {
      // 修改 manifest package 包名
      additionalParameters "--rename-manifest-package", "com.test.gradle"
    }
    
  2. failOnMissingConfigEntry 如果找不到配置条目,则强制 aapt 返回错误

  3. noCompress 添加不被压缩的文件扩展名,如果设置的为 ‘’ 则全部压缩都被禁止

  4. ignoreAssetsPattern 忽略 assets 下的文件,例如设置 ignoreAssetsPattern '*.svg' 则忽略 assets 下的 svg 文件

adbOptions / installation

adbOptions 已过时,被 installation 代替,映射为接口 Installation

  1. installOptions 用于增加执行 adb install 的参数,可选类型如下
    • -r: replace existing application
    • -t: allow test packages
    • -d: allow version code downgrade (debuggable packages only)
    • -p: partial application install (install-multiple only)
    • -g: grant all runtime permissions
  2. timeOutInMs 设置 adb 操作的超时时长

applicationVariants

applicationVariants 会返回当前项目包含的变体集合,通过 all 来遍历

import com.android.build.gradle.internal.api.ApplicationVariantImpl
import com.android.build.gradle.internal.api.ApkVariantOutputImpl

applicationVariants.all{/*ApplicationVariantImpl*/ variant->
  // variant 的属性比较多,但是基本都是上述的属性,这边不重复
  // outputs 为变体的输出列表,可通过修改 outputs 的内容来修改最终输出文件
  variant.outputs.all{/*ApkVariantOutputImpl*/ output->
    def date = new Date().format("yyyyMMddHHmmss")
    // 修改 app versionName
    output.versionNameOverride = "v${date}"
    // 修改 app versionCode,年月日作为版本号
    output.versionCodeOverride = Long.parseLong(date) / 10000 
    // 修改 apk 文件名
    output.outputFileName = "${output.versionNameOverride}-${variant.buildType.name}-${date}.apk"
  }
}

aar 打包发布

  1. aar 可通过 gradletask assemble 直接打包,高版本的 Android StudioSync 期间默认关闭了 Gradle tasks 的生成,需要通过 Preferences -> Experimental -> Do not build Gradle tasks during Gradle sync 关闭,再 Sync 之后就可以看到

    将生成的 aar 文件放到 libs 目录,可直接依赖

  2. 通过 maven-publish 插件来生成并发布,gradle 6.0 之前是通过 maven 插件发布,不过这个已废弃

    plugins{
      id "maven-publish" // 引入 maven-publish 插件
    }
    
    // 因为 components 只能在 afterEvaluate 阶段生成
    // 需要通过 components 来获取变体信息
    afterEvaluate {
      publishing{
        repositories{
          // 指定 maven 地址为本地目录
          // 也可以是私有服务器的地址,如果是私有服务器需要配置用户登录信息
          maven{ 
            url("${rootProject.projectDir.getAbsolutePath()}/repository")
            // 如果是私有仓库,需要配置认证信息
            credentials{
              username = "xxxxx"
              password = "xxxxx"
            }
          }
        }
        
        publications{
          releaseLib(MavenPublication){
            // 获取 release 变体,也可以指定成别的 flavour 的变体
            from components.relese 
            // 配置依赖信息
            groupId = "com.kuky.library"
            artifactId = "mylibrary"
            version = "0.0.1"
          }
        }
      } 
    }
    
    // settings.gradle
    dependencyResolutionManagement{
      repositories{
        // 指定依赖库的远程 url
        maven{ url("${rootProject.projectDir.getAbsolutePath()}/repository") }
      }
    }
    
    // build.gradle
    dependencies{
      implementation "com.kuky.library:mylibrary:0.0.1" // 依赖发布的 aar
    }
    

    publishing 映射类为 PublishingExtension,其属性就只有

    • repositories 发布的仓库
    • publications 配置需要发布内容的格式

    publications 映射类为 MavenPublication ,具体属性和方法可查看官方文档,官方文档中还提供了发布 Jar 的过程,

    相比于发布 aar,多了打 Jar 包的 task,并将该 task 指定到 publications.artifact 属性,而且发布不会限制在 afterEvaluate 阶段才可以生成

迁移kts

AGP 源码分析

AGP 源码分析可以参考这篇博客 🍵补齐Android技能树——从AGP构建过程到APK打包过程

项目地址

项目代码已上传 Github