Kotlin DSL Gradle 指南(下篇)

1,701 阅读5分钟

打包

Gradle 作为构建工具,编译打包 apk 是 Gradle 主要作用之一,apk 由各种文件组成,比如代码文件和资源文件,可以理解为 Gradle 本质上是在帮我们管理这些散落在各处的文件。

签名配置

打包需要先配置签名信息,但是这些 AndroidStudio 都可以自动生成,几乎不用我们手动编写,下面来操作一下。

image.png

image.png

image.png

确认之后,在 app 模块的 gradle 中就会生成相关的代码。

android {
    signingConfigs {
        create("release") {
            storeFile = file("/home/lbrd/Downloads/xzjKeystore.jks")
            storePassword = "123456"
            keyAlias = "key0"
            keyPassword = "123456"
        }
    }
    ...
    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
            signingConfig = signingConfigs.getByName("release")
        }
    }
}

实际项目开发中一般不会直接把这些敏感信息写在这里,我们可以写在 gradle.properties 文件中,这里定义的属性都是全局的。

gradle.properties 中加入

storeFile=/home/lbrd/Downloads/xzjKeystore.jks
storePassword=123456
keyAlias=key0
keyPassword=123456

引用其中定义的变量

signingConfigs {
    create("release") {
        storeFile = file(project.findProperty("storeFile") as String)
        storePassword = project.findProperty("storePassword") as String
        keyAlias = project.findProperty("keyAlias") as String
        keyPassword = project.findProperty("keyPassword") as String
    }
}

可以借助 AndroidStudio 的 Gradle 工具执行打包,如下所示:

image.png

有些人的 AndroidStudio 可能会没有 Gradle Task 工具,这时需要在设置中打开,然后同步一下即可。

image.png

build 完之后,我们就可以在如下路径找到对应的 apk 了。

image.png

这个 apk 的文件名太简单了,没有辨识度,我们来改一下,在 android {...} 中添加如下配置:

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

android.applicationVariants.all {
    outputs.all {
        if (this is com.android.build.gradle.internal.api.ApkVariantOutputImpl) {
            val config = project.android.defaultConfig
            val versionName = config.versionName
            val formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmm")
            val createTime = LocalDateTime.now().format(formatter)
            this.outputFileName = "${project.name}_${this.name}_${versionName}_$createTime.apk"
        }
    }
}

再次打包就会看到自定义的文件名了

image.png

多渠道打包

当我们的应用需要上架不同的应用市场,不同的渠道需要不同的定制化时,就需要多渠道打包。

先定义个 flavorDimensions

image.png

Add Product Flavor,添加具体的渠道。

image.png

image.png

不同的渠道可以配置不同的内容

image.png

这里配置了两个渠道,下面来看看 AndroidStudio 自动生成的 Gradle 代码。

android {
    ...
    flavorDimensions += listOf("channel")
    productFlavors {
        create("huawei") {
            dimension = "channel"
            applicationId = "com.huawei.app"
        }
        create("xiaomi") {
            dimension = "channel"
            applicationId = "com.xiaomi.app"
        }
    }
}

配置之后,当我们执行打包的时候,就会有渠道的选择了。

image.png

这里全选,就会得到两个不同渠道的安装包文件夹。

image.png

在开发调试阶段,如果想直接运行成某个特定的渠道,而不是每次都打包所有的渠道,可以选择 Build Variant,这样调试的时候就会生成特定的渠道包了。

image.png

image.png

想要做不同渠道的定制化开发,就得在代码中获取到这些渠道,从而区别对待,可以通过 buildConfigField 修改 BulidConfig,BuildConfig 是在构建时自动生成的 Java 类,里面存放一些静态常量,编译后可以直接使用类中的常量。

android.buildFeatures.buildConfig = true

productFlavors {
    create("huawei") {
        dimension = "channel"
        applicationId = "com.huawei.app"
        buildConfigField("String", "CHANNEL_VALUE", ""huawei"")
    }
    create("xiaomi") {
        dimension = "channel"
        applicationId = "com.xiaomi.app"
        buildConfigField("String", "CHANNEL_VALUE", ""xiaomi"")
    }
}

然后直接通过 BuildConfig 获取即可

private fun getChannel() = BuildConfig.CHANNEL_VALUE

打包成 jar 和 aar

jar 包是 Java 平台的标准打包格式,只包含编译后的 .class 文件和资源文件,不包含 Android 资源文件,如布局,图片等。aar 包是 Android 平台的打包格式,可以包含编译后的 .class 文件和 Android 资源文件,如布局,图片等。 先创建一个 Android Library

image.png

直接打包这个 Module

image.png

aar 在这

image.png

jar 在这

image.png

有了 aar 和 jar 之后,就可以将其放入需要用到的 Module 中,可以放到 libs 文件夹中,然后引入即可,也可以发布到远程仓库,实现远程依赖。

implementation(files("libs/mylibrary-release.aar"))

我们可以修改生成的文件名和路径,创建一个 Task。

tasks.register<Copy>("createJar") {
    // 先删除原来的
    delete("libs/tool.jar")
    // 拷贝的源文件路径
    from("build/intermediates/aar_main_jar/release/")
    // 目标路径
    into("libs/")
    include("classes.jar")
    // 重命名
    rename("classes.jar", "tool.jar")
}
tasks.getByName("createJar").dependsOn("build")

执行这个 Task 即可

image.png

有些时候,需要把应用模块打包成 aar,我们得先把应用模块变成库模块,如下所示:

image.png

image.png

去掉之后 AndroidManifest 长这样

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application>
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.ComposeApp" />
    </application>

</manifest>

同步一下,就由应用模块转变为库模块了,然后按照上面的方式打包即可。

自定义插件

Gradle 插件是一种用于扩展和定制 Gradle 构建系统行为的工具,负责处理 Android 应用程序的构建,打包,签名等任务,自定义插件只需实现 Plugin 接口,实现 apply 方法即可。

open class MyPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        println("apply")
    }
}

使用 apply 函数将插件应用到项目或模块中

apply<MyPlugin>()

我们可以发现,在实现的 apply 方法中,有个 Project 对象,而 task 是 Project 中的一个方法,所以也可以通过这个 Project 对象去创建 Task。

open class MyPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        target.task("pluginTask") {
            doLast {
                println("running pluginTask")
            }
        }
    }
}

Plugin 接口的 apply 方法在编译阶段就会执行,所以在 sync 执行构建后就会有输出。上面的都只是输出,乍一看似乎并没有什么实际的作用,那这里就再举个实例,一个压缩文件的自定义插件。

apply<ZipPlugin>()

open class ZipPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        target.afterEvaluate {
            val textFile = File(project.rootDir, "myFile.txt")
            project.task("zip", Zip::class) {
                archiveFileName.set("xzj.zip") //设置压缩文件名
                destinationDirectory.set(File("${target.buildDir}/custom")) //设置输出目录
                from(textFile) //将文件添加到压缩包中
            }
        }
    }
}

执行 Task,就可以在对应的目录上看到该压缩文件。

image.png

上面所说的插件都属于二进制插件,Gradle 还有种脚本插件,下面我们来创建一个脚本插件,也可以新建一个 gradle 文件,防止 build.gradle.kts 中代码臃肿。

image.png

afterEvaluate {
    task("zip", Zip::class) {
        val textFile = File(project.rootDir, "myFile.txt")
        archiveFileName.set("xzj.zip")
        destinationDirectory.set(File("${project.buildDir}/custom"))
        from(textFile)
    }
}

引入脚本插件跟二进制插件的方式不太一样,该参数是外部 Gradle 脚本的路径,Project 下的 build.gradle.kts 中引入该插件,如下所示:

apply("plugin.gradle.kts")

如果路径不正确的话,会因为找不到而编译出错。比方说我们需要在 app 下的 build.gradle.kts 引入该插件,就应该这样:

apply("../plugin.gradle.kts")

扩展插件

在 android {...} 闭包里有各类配置,如 minSdk,targetSdk,versionCode 等等,我们也可以通过扩展插件来实现自定义配置。

定义一些扩展属性

open class Car {
    var name: String = "BYD"
    var type: String = "ocean"
}

在 plugin 中使用扩展属性

class MyPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        val extension = target.extensions.create("test", Car::class.java)
        target.task("MyTask") {
            doLast {
                println("name: ${extension.name}")
                println("type: ${extension.type}")
            }
        }
    }
}

然后将插件引入

apply<MyPlugin>()

其实这还不够,我们先来看看 android{...} 这个闭包是怎么实现的?

fun org.gradle.api.Project.`android`(configure: Action<com.android.build.gradle.internal.dsl.BaseAppModuleExtension>): Unit =
    (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("android", configure)

这里我们就仿照这个,定义一个扩展函数,用于配置自定义属性。

fun Project.customConfig(action: Car.() -> Unit) {
    extensions.configure("test", action)
}

这里需要注意的是,extensions.configure 的 name,需要对应上面 extensions.create 的 name,不然会报错: Extension does not exist

这样就可以像配置 android{...} 一样,去配置我们的自定义属性了。

customConfig {
    name = "xiaomi"
    type = "SU7"
}