注册

gradle 实用技巧

前言


总结一些日常开发中非常有用的 gradle 脚本、自定义功能实现。


实现


以下实现基于 AGP 8.0.2 版本 ,AGP 的 API 隔三岔五就会迎来一波破坏性的变更,导致脚本和插件无法使用,因此这里需要关注一下版本。


输出打包后 apk 文件路径及 apk 大小。


Android Studio 最新版本 Run 之后,每次输出的 apk 并没有在这 app/build/outputs 文件夹下(不知道 Android 官方是出于什么考虑要更改这个路径),而是移动到了 build\intermediates\apk\{flavor}\debug\ 目录下。为了方便后续快速找到每次运行完成的 apk ,可以在每次打包后输出 apk 文件路径及大小,从而可以关注一下日常开发过程中自己
的 apk 体积大概是一个什么样的范围。


static def getFileHumanSize(length) {
def oneMB = 1024f * 1024f
def size = String.valueOf((length / oneMB))
def value = new BigDecimal(size)
return value.setScale(2, BigDecimal.ROUND_HALF_UP)
}
/**
* 打包完成后输出 apk 大小*/
android {
applicationVariants.all { variant ->
variant.assembleProvider.configure() {
it.doLast {
variant.outputs.forEach {
logger.error("apk fileName ==> ${it.outputFile.name}")
logger.error("apk filePath ==> ${it.outputFile}")
logger.error("apk fileSize ==> ${it.outputFile.length()} , ${getFileHumanSize(it.outputFile.length())} MB")
}
}
}
}
}

apk fileName ==> app-huawei-global-debug.apk
apk filePath ==> D:\workspace\MinApp\app\build\intermediates\apk\huaweiGlobal\debug\app-huawei-global-debug.apk
apk fileSize ==> 11987818 , 11.43 MB

可以看到 apk 的路径在 build/intermediates 目录下。当然,我们可以通过下面的方法修改这个路径,定义成我们习惯的路径。


gradle 自定义功能的模块化


日常开发中,会有很多关于 build.gradle 的修改和更新。日积月累,build.gradle 的内容越来越多,代码几乎要爆炸了。其实,可以用模块化的思路将每一个小功能单独抽取出来,这样不仅可以减少 build.gradle 的规模,同时小功能可以更加容易的复用。


比如上面定义的输出打包后 apk 文件路径及 apk 大小的功能,我们就可以把他定义在 report_apk_size_after_package.gradle 这样一个文件中,然后在要使用的 build.gradle 中导入即可。


比如我们要在 app module 中使用这个功能,那么就可以直接在其 build.gradle 文件中按照相对路径引入即可。


gradle_dep.png


apply from: file("../custom-gradle/report_apk_size_after_package.gradle") // 打包完成后输出 apk 大小


修改 release 包的输出路径及文件名


输出 apk 后改名的需求,应该已经很普遍了。在最终输出的 apk 文件中,我们可以追加一些和代码相关的信息,方便通过 apk 文件名迅速确定一些内容。


def getCommit() {
def stdout = new ByteArrayOutputStream()
exec {
commandLine "git"
args "rev-parse", "--short", "HEAD"
standardOutput = stdout
}
return stdout.toString().trim()
}

def getBranch() {
def stdout = new ByteArrayOutputStream()
exec {
commandLine "git"
args "rev-parse", "--abbrev-ref", "HEAD"
standardOutput = stdout
}
return stdout.toString().trim()
}

def gitLastCommitAuthorName() {
return "git log -1 --pretty=format:'%an'".execute(null, rootDir).text.trim().replaceAll("\'", "")
}

def gitLastCommitAuthorEmail() {
return "git log -1 --pretty=format:'%ae'".execute(null, rootDir).text.trim().replaceAll("\'", "")
}


android {
def i = 0
applicationVariants.all { variant ->
if (variant.assembleProvider.name.contains("Debug")) {
// 只对 release 包生效
return
}

// 打包完成后复制到的目录
def outputFileDir = "${rootDir.absolutePath}/build/${variant.buildType.name}/${variant.versionName}"
//确定输出文件名
def today = new Date()
def path = ((project.name != "app") ? project.name : rootProject.name.replace(" ", "")) + "_" + variant.flavorName + "_" + variant.buildType.name + "_" + variant.versionName + "_" + today.format('yyyy_MM_dd_HH_mm') + "_" + getBranch() + "_" + getCommit() + "_" + gitLastCommitAuthorName() + ".apk"
println("path is $path")
variant.outputs.forEach {
it.outputFileName = path
}
// 打包完成后做的一些事,复制apk到指定文件夹
variant.assembleProvider.configure() {
it.doLast {
File out = new File(outputFileDir)
copy {
variant.outputs.forEach { file ->
copy {
from file.outputFile
into out
}
}
}
}
}
}
}

打 release 包后的日志


let me do something after assembleHuaweiGlobalRelease
apk fileName ==> MiniApp_huaweiGlobal_release_1.0.0_2306292226_2023_06_29_22_26_master_b0c6937_rookie.apk
apk filePath ==> D:\workspace\MinApp\app\build\outputs\apk\huaweiGlobal\release\MiniApp_huaweiGlobal_release_1.0.0_2306292226_2023_06_29_22_26_master_b0c6937_rookie.apk
apk fileSize ==> 4959230 , 4.73 MB

通过上面的日志,可以看到 MiniApp_huaweiGlobal_release_1.0.0_2306292226_2023_06_29_22_26_master_b0c6937_rookie.apk 包含了 ProjectName、flavor、debug/release、打包时间、分支、commitId 即最后一个 commitor 邮箱这些信息。通过这样的信息,可以更加方便快速的定位问题和解决问题。


妙用 flavor 实现不同的功能


使用 flavor 可以定制代码的不同功能及组合。不用把所有内容一锅乱炖似的放在一起搞。比如 MiniApp 随着演示代码的增多,已经逐渐丧失了 Mini 的定位,Apk 大小已经来到了 22 MB之多。究其原因,就是把所有代码验证和功能都放在一起导致的,音视频、compose、C++ 代码全都混在一起。部分功能不常用,但是每次为了验证一部分小功能,却要连带编译这些所有功能,同时打出的 apk 包体积也变大了,从编译到安装,无形中浪费了很多时间。


因此,可以通过 flavor 将一些不常用的功能,定义到不同的 flavor 中,真正需要的时候,编译相应 flavor 的包即可。


首先我们可以从 type 维度定义两个 flavor


    flavorDimensions "channel", "type"
productFlavors {
xiaomi {
dimension "channel"
}
oppo {
dimension "channel"
}
huawei {
dimension "channel"
}

global {
dimension "type"
}
local {
dimension "type"
}
}

在 type 维度,我们可以认为 global 是功能完整的 flavor,而 local 是部分功能缺失的 flavor 。那么具体缺失哪些功能呢?这就要从实际情况出发了,比如产品定义,代码架构及模块组合之类的。回到 Mini App 中,我们使用不同 flavor 的目标就是通过减少非常用功能模块,获得一个体积相对较小的 apk. 因此,可以做如下配置。


    if (source_code.toBoolean()) {
globalImplementation project(path: ':thirdlib')
} else {
globalImplementation 'com.engineer.third:thirdlib:1.0.0'
}
globalImplementation project(path: ':compose')
globalImplementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer:v8.1.8-release-jitpack'

如上我们只在 global 这个 flavor 依赖 thirdlib, compose, GSYVideoPlayer 这些组件。这样 local flavor 就不会引入这些组件,那么就会带来一个问题,local flavor 编译的时候没有这些组件的类,会出现找不到类的情况。


class_missing.png


对于这种情况,我们可以在项目 src 和 main 同级的目录下,创建 local 文件夹,然后在其内部按照具体 Class 文件的路径创建相应的类即可。


package com.engineer.compose.ui

import com.engineer.BasePlaceHolderActivity

/**
* Created on 2022/7/31.
* @author rookie
*/

class MainComposeActivity : BasePlaceHolderActivity()



package com.engineer.third

import com.engineer.BasePlaceHolderActivity

/**
* Created on 2022/7/31.
* @author rookie
*/

class CppActivity : BasePlaceHolderActivity()

package com.engineer

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.engineer.android.mini.ext.toast

/**
* Created on 2022/8/1.
* @author rookie
*/

open class BasePlaceHolderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
"please use global flavor ".toast()
finish()
}
}

local_flavor.png


这里的思路其实很简单,类似于 leanCanary ,就是在不需要这个功能的 flavor 提供空实现,保证编译可以正常通过即可。缺什么类,建按照类的完整路径创建相应的 Class 文件,这样既保证了编译可以通过,同时在不需要次功能的 flavor 又减少无冗余的代码。


flavor 扩展


其实顺着上面的思路,基于不同 flavor 我们可以做更多的事情。基于 Java 虚拟机的类加载机制限制,相同的类只能有一个,因此我们无法做的事情是,通过 flavor 创建同名的类,去覆盖或重写其他 flavor 的逻辑,这种在编译阶段(其实是在创建同名类的阶段)就会报错。


但是,有些功能是可以被覆盖和定制的。比如包名、App 的名称、icon 之类。这些配置可以通过 AndroidManifest.xml/gradle 进行配置。可以在 local 目录下创建这个 flavor 特有的一些资源文件,这样就可以实现基于 flavor 的产品功能定制了。


比如最简单的修改 applicationId


        global {
dimension "type"
}
local {
dimension "type"
applicationId "com.engineer.android.mini.x"
}

这样,local 和 global 就有了各自不同的 applicationId, 这两种不同 flavor 的包就可以安装在同一台设备了。当然,现在这两个包的 label 和 icon 都是一样的,完全看不出区别。这里就可以利用 flavor 各自的文件夹,来定制各类资源和命名了。


flavor 过滤


不同维度的 flavor 会导致最终的 variant 变多。比如定义 product、channel、type 这些几个 dimension 的之后,后续新增的 flavor 会以乘积的形式增长,但是有些 flavor 又是我们不需要的,这个时候我们就可以过滤掉某些不需要的 flavor 。


比如以上面定义的 channel,type 这两个维度为例,在这两个维度下分别又扩展了 xiaomi/opop/huawei,global/local 这些 flavor 。按照规则会有 2x3x2=12 种 flavor,但实际情况可能不需要这么多,为了减少编译的压力,提升代码的可维护性,我们可以对 flavor 进行过滤。


    variantFilter { variant ->
println "variant is ${variant.flavors*.name}"
def dimens = variant.flavors*.name
def type = dimens[1]
def channel = dimens[0]
switch (type) {
case "global":
if (channel == "xiaomi") {
setIgnore(true)
}
break
case "local":
if (channel == "oppo") {
setIgnore(true)
}
break
}
}

这样我们就成功的过滤掉了 xiaomiGlobal 和 oppoLocal 的 flavor ,一下子就去掉了 4 个 flavor 。


基于现有 task 定制任务


再回顾一下上面的 修改 release 包的输出路径及文件名 的代码实现,我们是在打包完成之后进行了 apk 文件的重命名。


        // 打包完成后做的一些事,复制apk到指定文件夹
variant.assembleProvider.configure() {
it.doLast {
File out = new File(outputFileDir)
copy {
variant.outputs.forEach { file ->
copy {
from file.outputFile
into out
}
}
}
}
}

这里的 doLast 就是说,无论是否需要,每次都会在 assemble 这个 task 完成之后做一件事。这样在某些情况下显得非常的不灵活,尤其是当 doLast 闭包中要做的事情非常繁重的时候。这里的 copy 操作显然是比较轻量的,但是换做是其他操作,比如 apk 安全加固等操作,并不是每次必然需要的操作。这种情况下,就需要我们换一种方式去实现相应的逻辑了。


我们就以加固为例,一般情况下,我们需要对各个版本的 release 包进行加固。因此,我们可以基于现有的 assembleXXXRelease 这个 task 展开。


android {
applicationVariants.all { variant ->
if (variant.assemble.name.contains("Debug")) {
// 只对 release 包生效
return
}

def taskPrefix = "jiagu"
def groupName = "jiagu"
def assembleTask = variant.assembleProvider.name
def taskName = assembleTask.replace("assemble", taskPrefix)
tasks.create(taskName) {
it.group groupName
it.dependsOn assembleTask
variant.assembleProvider.configure() {
it.doLast {
logger.error("let me do something after $assembleTask")
}
}
}
}
}

添加上面的代码后,再执行一下 gradle sync ,我们就可以看到新添加的 jiagu 这个 group 和其中的 task 了。


jiagu.png


这里使用创建 task 的一种方式,使用 createdependsOn ,动态创建 task,并指定其依赖的 task 。


这样当我们执行 ./gradlew jiaguHuaweiLocalRelease 时就可以看到结果了。


> Task :app:assembleHuaweiLocalRelease
let me do something after assembleHuaweiLocalRelease

>
Task :app:jiaguHuaweiLocalRelease

BUILD SUCCESSFUL in 56s
82 actionable tasks: 12 executed, 70 up-to-date

可以看到我们自定义的 task 已经生效了,会在 assembleXXXRelease 这个 task 完成之后执行。


关于 gradle 的使用,可以说是孰能生巧,只要逐渐熟悉了 groovy 的语法和 Java 语法之间的差异,那么就可以逐渐摸索出更多有意思的用法了。


本文源码可以参考 Github MiniApp


小结


可以看到基于 gradle 构建流程,我们仅仅通过编写一些脚本,可以做的事情还是很多的。但是由于 groovy 语法过于灵活,不像 Java 那样有语法提示,因此尝试一些新的语法时难免不知所措。面对这种情况,去看他的源码就好。通过源码,我们就可以知道某个类有哪

作者:IAM四十二
来源:juejin.cn/post/7250071693543145529
些属性,有哪些方法。

0 个评论

要回复文章请先登录注册