利用 Gradle 构建 Android 前置后置任务的详细介绍


利用 Gradle 构建 Android 前置后置任务的详细介绍

前言

“构建 Android 前置后置任务” 指的是打 Android 包的时候通常会有的之前的一些操作以及之后的一些操作。例如在打包前自动修改版本号、打包成功后进行加固等等。

上一篇简单介绍了将 Android 构建从 python / shell 等脚本语言转成使用 gradle 的大概的思路,本篇我来详细讲述一下具体的操作流程,以及在最后说一些笔者碰到的问题。希望读者看完以后,可以

  • 理解使用 gradle 构建 Android 应用的代码架构
  • 在相关文档的帮助下,可以自己实现相关处理步骤。(增加前置、后置任务等)

介绍 – 使用 python 作为引子,转而介绍 gradle 的基本思想

如果直接进行使用 gradle 的代码架构的说明以及代码展示的话,可能会使人丈二和尚摸不着头脑。所以我从我自己容易理解的角度出发,介绍一下这种改动的比较容易理解的思路。以我的理解能力可以理解的话,相信读者朋友们都是可以看明白这样做的得失的。

假设 :

我们已经有一个这样的 python 脚本用来给 Android 打包,它有如下特征:

  • 存在了很久,从三年前项目开始就存在,后面是一系列地修修补补。
  • 功能复杂, 功能包括但不限于:
    • 1)打包前:根据配置文件修改版本号;根据本次打包的某个特征值修改 gradle 编译参数(甚至因为不同时期的修改可能使用了两种不同的方法修改不同的参数——不过这里简单考虑就不需要那么复杂啦)
    • 2)打包后:根据打包配置进行加固;并且打出多个渠道包;将包上传到一个指定的存储平台。
    • 根据打包时候配置的测试环境或者是线上环境,上面提到的功能每个都不一样(比如说测试环境上传到一个位置,而线上环境是另外一个。)
  • 因为没有专人维护,每次打包需要新的功能的时候就进行改动,导致代码架构很差。修改起来很麻烦。
  • 有两个不同的应用 productA & productB 都使用了这个打包脚本,使得打包脚本里有很多对此的 if(currentProduct == productA ){ } 的逻辑。

然后我们接收到了一个任务 : 将这个打包脚本重构成容易理解、易于扩展的形式。

??,那么我们应该怎么做呢?以笔者的拙见,如下的思路是比较好的一种方案:

  • 不同功能的实现代码内聚到一起,做成一个个独立的 python-module 用来提供单一的功能。例如可以
    • 将加固的代码整理成一个 jiaguModule,对外暴露一个 jiagu(srcApk, dstApk , jiaguMethod ) #参数分别表示 加固源 / 生成 apk 的位置 / 加固方式方法。
    • 将生成渠道包的代码整理成一个 channelModule ,对外暴露一个 buildChannels(srcApk , channels)# 参数分别表示 渠道源 apk / 要生成哪些渠道信息
  • 两个不同的应用使用不同的脚本,不混在一起,在脚本里将上述模块提供的功能组装起来实现相互独立的构建。

如此一来,相当于我们将之前的 python 脚本拆成了两部分 : 一部分是封装的功能模块; 一部分是脚本内容。而在脚本内容里,实际上是一些基于封装的功能模块的流程设计。

这里的 封装功能模块 也可以使用单独的函数来实现。

这样我们就得到了下面的新的 Python 脚本:

  • 封装的功能模块
    • 修改版本号模块
    • 加固模块
    • 打渠道包模块
    • 上传模块
  • 实际脚本模块
    • productA 的构建脚本
    • productB 的构建脚本

这个重构的 python 脚本很棒,只是有一个缺点:它是 python 语言的,对于 Android 开发而言,其可读性以及可维护性都不那么高,如果可以改成 kotlin 的话,那就太让人欣慰了。

然后我们来把这个重构后的 python 脚本修改成 gradle 脚本的形式。为什么需要修改呢?因为我们希望将 python 改为熟悉的 kotlin ,以更好理解和修改。改动很简单,只是将 封装的功能模块修改成 gradle task , 实际脚本模块修改成 gradle 脚本 。为了防止有的读者不理解 gradle task 的基本知识,下面简单介绍一下笔者所理解的 gradle task。

关于 gradle task 的简单介绍

本节需要自行了解一下 buildSrc 的使用方法

gradle task 应该如何声明?

这里有一个简单的方法,我们在 Android 项目根目录下新建一个目录叫做 buildSrc ,这个目录的用法和普通的 android 模块一样,可以添加依赖库如 okhttp 等。和普通 android 模块不同之处在于这个目录里的源码会在 gradle 脚本构建之前 编译,也就是说在 gradle 脚本里,可以直接访问到这些类以及这些类的数据(比如说常量等)。

至于 buildSrc具体的用法以及目录里面怎么设置依赖库等。就不在此详细说明了。因为这毕竟不是本文的重点,如果有读者想了解的话,建议搜索一下网上相关的文章,有很多浅显易懂的文章的。搜索关键词 “android buildSrc 怎么用” 即可。

难读的官方文档地址:如何使用 kotlin 声明 task 类型 。如果发现很难理解的话,也很正常,不需要强行读,可以查看一些其他文章或者留言问笔者……

如果有读者不会使用 buildSrc 的话,建议先了解一下,这样才能知道后面具体该如何操作。不过这并不影响大体的流程,在这里只需要知道 buildSrc 里的类可以在 gradle 脚本中被访问到即可。而 gradle task 就可以被放到 buildSrc 模块中,在脚本中像访问第三方库代码一样使用。

gradle task 的表现形式一直是很吓人的,要么是 Android gradle plugin ,要么是第三方 plugin ,这些实在是太难懂啦,笔者也搞不清楚。所以在这里不妨先粗略简单地理解一下 : gradle task 其实就是一个可执行函数(先忽略掉它所有的 doFirst / doLast 之类的属性)。

上面重构后的 python 脚本里,其中的 封装的功能模块 就会被转移成 gradle task 的形式。这个的意味也很明显:表示这些 task 是用来提供功能的,就像是函数调用一般。

不过我们写好了 task,也只是写好了功能而已,并没有调用,是无法生效的。接下来说下如何调用这些 task 来执行各种操作。

gradle 脚本里对 task 的引用

在 gradle 里,各个 task 是可以互相构建依赖关系的,我们在上面声明了很多功能 task,就是在 gradle 脚本中进行使用的。至于 gradle 的使用,一言难尽,笔者无力叙说很详细,所以直接在下面列出了 demo 代码。请各位查看,如果有相关疑惑或者指正,欢迎提出????。

修改总结

下面会列出 demo 代码,在此先口头说明下需要哪些改动,以使得读者更加理解。(举例不够严谨,读者见谅)

  • 增加一个打包前执行的操作。这里以修改版本号为例
  • 增加一个打包后执行的操作。这里以打包后加固为例
  • 打包步骤设计脚本。
  • 辅助帮助上面三者跑起来的 gradle 脚本的改动(包含两个,一个是 buildSrc 的依赖配置;一个是引入上面的步骤设计脚本)

改动如下

new file:   a.build.gradle.kts
modified:   app/build.gradle
new file:   buildSrc/build.gradle
new file:   buildSrc/src/main/java/postassemble/Jiagu.kt
new file:   buildSrc/src/main/java/preassemble/ModifyVersion.kt

demo 代码

// buildSrc/src/main/java/postassemble/ModifyVersion.kt
// 声明一个任务,用来说明打包前执行的操作的样子
package preassemble

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile

// 此处举例只是为了说明 outputFile 的能力。
// 将给定的 [newVersion] 写入 [configFile] , 如果 [configFile] 不存在,会新建。
abstract class ModifyVersion : DefaultTask() {

    @org.gradle.api.tasks.Input
    lateinit var newVersion: String

    @OutputFile
    lateinit var configFile: java.io.File

    @org.gradle.api.tasks.TaskAction
    fun action() {
        println("${configFile} 里的版本号已被改变,改成了 ${newVersion}")
    }
}
// buildSrc/src/main/java/postassemble/Jiagu.kt
// 声明加固任务类型
package postassemble

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile

// 该任务将 srcFile 加固,输出到 outputFile
// 使用的加固方法由 jiaguMethod 指定
abstract class Jiagu : DefaultTask() {

    @InputFile
    lateinit var srcFile: java.io.File

    @OutputFile
    lateinit  var outputFile: java.io.File

    @Input
    lateinit  var jiaguMethod: String

    @org.gradle.api.tasks.TaskAction
    fun jiagu() {
        println("${srcFile} 通过 ${jiaguMethod} 加固完成,加固后的文件位于${outputFile}")
    }
}
// buildSrc/build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        google()
        maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' }
        maven { url 'https://maven.oschina.net/content/groups/public/' }
        maven { url "https://plugins.gradle.org/m2/" }
    }
}

plugins {
    id "org.jetbrains.kotlin.jvm" version "1.5.0"
}

allprojects {
    repositories {
        google()
        maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' }
        maven { url 'https://maven.oschina.net/content/groups/public/' }
        maven { url "https://plugins.gradle.org/m2/" }
    }
}
dependencies {
    // 可以在这里添加 okhttp / glide 之类的三方库,然后在 src/ 中的源码就可以使用了。
}

// a.build.gradle.kts
// 注意 : 该脚本文件定义了任务以及任务之间的关系。需要引入。
import preassemble.ModifyVersion
import postassemble.Jiagu

project.afterEvaluate {
    val rootDir = project.rootDir
    // 步骤说明 : 新建一个任务,名字叫作 "modifyVersion"
    val modifyVersion = project.tasks.register("modifyVersion") {
        newVersion = "1.0"
        configFile = File(rootDir, "config")
    }.get()
    // 步骤说明 : 获取打包任务
    val assembleTask = tasks.findByName("assembleRelease") ?: throw IllegalStateException("找不到打包任务")
    // 步骤说明 : 新建一个任务,任务名字叫作 "jiagu"
    val jiagu = tasks.register("jiagu") {
        // 因为我的 demo 项目没有配置签名,所以打出来的有 -unsigned ,仅做举例.
        // 这里应该是加固任务输入的源 apk。
        srcFile = java.io.File("build/outputs/apk/release/app-release-unsigned.apk")
        outputFile = File("app/build/outputs/apk/release/app-release-jiagu.apk")
        jiaguMethod = "cccc"
    }.get()

    // 步骤说明 : 设置任务之间的依赖
    assembleTask.dependsOn(modifyVersion)
    jiagu.dependsOn(assembleTask)
}

遇到的不合理的情况(坑)

  • *.gradle.kts 文件没有语法高亮。

    1. 打开 “Preferences” -> “Language & Frameworks” -> “Kotlin” -> “Kotlin Scripting”

    2. 右边下面的 “Gradle Kotlin DSL Scripts” 中添加指定的 .gradle.kts 文件才行。

    添加之后,Android Studio 就会进行语法高亮和自动提示了。

  • 我在 gradle.kts 文件里有个任务先修改了某个 build.gradle 里的属性,然后才执行 assembleRelease ,但是打出来的 apk 这个改动没有生效,为什么?

    这是因为在调用 gradle 执行任务开始后,再去修改 gradle 文件已经是不生效了。

    比如在上面的 demo 中,如果直接执行 ./gradlew :app:jiagu 的话, 虽然 assembleRelease 是中间的任务,但是其执行的时候也是以调用 ./gradlew :app:jiagu 时候的 gradle 脚本内容为准的。

    所以在 gradle.kts 里想使用任务 A 修改某个 build.gradle 文件,然后再打包的话。必须先执行该命令才行。即需要:

    1. ./gradlew :app:A (该任务修改 app/build.gradle , 比如说替换了里面声明的 app_name 属性)
    2. ./gradlew :app:jiagu (举例,表示最终任务。此处还以加固为最终目标)
  • 为什么我的脚本里使用 tasks.findByName("assembleRelease") 找不到任务?

    • 可能是因为没有在 project.afterEvaluate { } 来获取任务。 assembleRelease 任务是 android 插件执行后才会存在的,所以需要等到项目 evaluate 完成,这时候插件新建的任务才会存在。
    • 可能是 :app 模块没有 assembleRelease 任务。 比如说 设置了 flavor 等情况,那时候需要视具体的任务名进行修改。
  • 在 buildSrc 里添加了依赖项(比如 okhttp / gson 等),但是 build/src 里的代码无法自动 import 三方库的类,找不到类,这是为什么?

    这个我感觉是 AS 的 BUG,实际是存在的,只是此时的自动 import 失效了。

    我的解决方法是手动查看某个类的位置,然后手动输入 import 。比如说先查看到 OkHttpClient 的包名是 okhttp3,然后在要使用的文件头部输入 import okhttp3.OkHttpClient