第
101
次推文
LZ-Says
我介意着你的不介意。
前言
借着韩哥哥要求重构的机会,正好好好回顾下以前遗忘/忽略的知识点。
记录下有关 Gradle 优化之路:
- Android|模块化探索抽取 basic 简化子 module 冗余
- Android 重构 | 统一管理 Gradle 依赖版本
大概的方向或者说最终目标精简后如下:
一次引用,全文(项目)使用,避免团队协作引入重复依赖;
自带依赖更新提示;
支持跳转等常规操作。
最重要的,依然是便于维护。
从最初的创建 config.gradle 到现在的 basic_depend.gradle,虽说今天更比昨天强,但是依然不是很满意。
ext 方式虽然是 Google 官方目前推荐,并且当前一些主流库也采用此种方式,实际使用起来,个人还是有部分不方便。比如说不支持跳转,不支持更新等等,人呐,总想得到更多。
在查阅了多个文档后,再次准备优化/升级一波,继续让韩总蒙圈。
一、buildSrc 搞起来
将官方的描述用 Google 翻译了一遍,如下:
复杂的构建逻辑通常很适合作为自定义任务或二进制插件进行封装。自定义任务和插件实现不应存在于构建脚本中。buildSrc 只要不需要在多个独立项目之间共享代码,就可以非常方便地使用该代码。 该目录 buildSrc 被视为包含的构建。发现目录后,Gradle 会自动编译并测试此代码,并将其放入构建脚本的类路径中。对于多项目构建,只能有一个 buildSrc 目录,该目录必须位于根项目目录中。buildSrc 应该比脚本插件更可取,因为它更易于维护,重构和测试代码。 buildSrc 使用适用于 Java 和 Groovy 项目的相同源代码约定。它还提供对 Gradle API 的直接访问。 Google Develop
思索许久,个人简单总结下:
- buildSrc 存在于 Gradle 编译期;
- 同样 buildSrc 支持(单独项目)共享代码,例如一个项目中多个 module 都可以直接调用。
buildSrc 实践
描述下操作步骤:
- 在项目根目录下创建 buildSrc 目录,随后新建 build.gradle.kts 文件;
- 创建 src 目录,以及对应管理版本文件;
- 替换直接使用原有依赖
build.gradle.kts 内容如下:
代码语言:javascript复制// 导入 Kotlin 插件
import org.gradle.kotlin.dsl.`kotlin-dsl`
plugins {
`kotlin-dsl`
}
repositories {
jcenter()
}
/**
* 禁用测试报告(Gradle 默认会自动创建测试报告)
*/
tasks.withType<Test> {
reports.html.isEnabled = false
reports.junitXml.isEnabled = false
}
/**
* isFork:将编译器作为单独的进程运行。
* 该过程在构建期间将被重用,因此分叉开销很小。分叉的好处是,内存密集型编译是在不同的过程中进行的,从而导致主 Gradle 守护程序中的垃圾回收量大大减少。
* 守护程序中较少的垃圾收集意味着 Gradle 的基础架构可以运行得更快,尤其是在您还使用的情况下 --parallel。
*
* isIncremental:增量编译。Gradle 可以分析直至单个类级别的依赖关系,以便仅重新编译受更改影响的类。自 Gradle 4.10 起,增量编译是默认设置。
*/
tasks.withType<JavaCompile> {
options.isFork = true
options.isIncremental = true
}
/**
* 禁用关于使用实验性 Kotlin 编译器功能的警告
*/
kotlinDslPluginOptions {
experimentalWarning.set(false)
}
Dependencies.kt,这是我定义的版本管理的文件,部分内容如下:
代码语言:javascript复制@file:Suppress("SpellCheckingInspection")
/**
* @author HLQ_Struggle
* @date 2020/7/27
* @desc 统一管理类
*/
// 统一管理项目中的版本信息
object Versions {
// Build Config
const val compileSDK = 29 // 编译 SDK 版本
const val buildTools = "29.0.3" // Gradle 编译项目工具版本
const val minSDK = 23 // 最低兼容 Android 版本
const val targetSDK = 29 // 最高兼容 Android 版本
// App Version
const val appVersionCode = 1 // 当前版本编号
const val appVersionName = "1.0" // 当前版本信息
// 。。。
}
// 统一管理项目中使用的依赖库
object Deps {
// Gradle
const val androidGradle = "com.android.tools.build:gradle:${Versions.androidGradlePlugin}"
// Kotlin
const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}"
const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
const val kotlinxCoroutines =
"org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.kotlinxCoroutines}"
// ...
}
举个两个栗子,如何使用:
- 根目录下 build 如何使用:
直接通过在 Dependencies 文件中定义的分组名去获取对应的属性即可,如下所示:
代码语言:javascript复制buildscript {
// ...
dependencies {
classpath Deps.androidGradle
classpath Deps.kotlinGradlePlugin
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
// ...
- 其它 module 目录下 build 如何使用:
同理,当然也可以采用直接倒入整个对应分组方式,直接使用对应属性,例如:
代码语言:javascript复制// 这里采用直接倒入定义的 Deps 以及 Versions 分组方式
import static Deps.*
import static Versions.*
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
// 这里就可以直接使用对应的属性
compileSdkVersion compileSDK
buildToolsVersion buildTools
defaultConfig {
minSdkVersion minSDK
targetSdkVersion targetSDK
versionCode appVersionCode
versionName appVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
// ...
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// 同理,这里也是一样,直接使用对应的属性名即可
implementation kotlinStdLib
implementation appcompat
implementation coreKtx
api 'com.google.android.material:material:1.2.0'
testImplementation junit
androidTestImplementation extJunit
androidTestImplementation espresso
api mmkv
api 'com.airbnb.android:lottie:3.4.1'
}
这种方式比较有好的几个特点如下:
- 支持跳转;
- 支持智能提示;
- Gradle 编译时介入,感脚很湿高大上
但是关键的更新提示呢?
ummm,不开森。
加个 gif 配图吧~
手动编写 buildSrc 需要注意:
- 目录结构:例如:buildSrc/src/main/kotlin(java)
- 在 build.gradle.kts 中添加 jcenter(),否则 kotlin-dsl 加载失败
二、refreshVersions 使用(2020/09/15)
网上搜到关于 refreshVersions 的描述,觉得蛮合适,尝试一波。
大概的优势在于以下几点:
- 集中管理依赖
- 以最小成本提示依赖升级
操作步骤如下:
Step 1:修改 settings.gradle 文件
代码语言:javascript复制// settings.gradle.kts
import de.fayard.refreshVersions.RefreshVersionsSetup
// Here you might have some pluginManagement block:
pluginManagement {
//...
}
buildscript {
repositories { gradlePluginPortal() }
dependencies.classpath("de.fayard.refreshVersions:refreshVersions:0.9.5")
}
rootProject.name = 'Your Android Project Name'
include ':app'
include ':helper'
include ':weight'
// include other module
RefreshVersionsSetup.bootstrap(settings)
Step 2:同步后执行命令
代码语言:javascript复制./gradlew migrateToRefreshVersionsDependenciesConstants --console=plain
根据提示进行依赖替换:
随后生成 versions.properties 文件:
代码语言:javascript复制## suppress inspection "SpellCheckingInspection" for whole file
## suppress inspection "UnusedProperty" for whole file
##
## Dependencies and Plugin versions with their available updates
## Generated by $ ./gradlew refreshVersions
## Please, don't put extra comments in that file yet, keeping them is not supported yet.
version.androidx.appcompat=1.1.0
## # available=1.2.0-alpha01
## # available=1.2.0-alpha02
## # available=1.2.0-alpha03
## # available=1.2.0-beta01
## # available=1.2.0-rc01
## # available=1.2.0-rc02
## # available=1.2.0
## # available=1.3.0-alpha01
## # available=1.3.0-alpha02
## 。。。
有一点觉得不舒服的地方是,它内置了 Android 一部分的依赖,而对于我们实际开发中使用其它依赖,则显示不太友好了,如下图:
研究好一段时间,各种蒙圈,实际的效果还是不是太满意,如果能在 buildSrc 的基础上新增版本更新就更好了。
三、buildSrc 结合 task(2020/09/17)
不得不说,掘金大佬很多,很友善,这不,沉璧浮光cbfg 大佬教我一招~
文末已附上链接,感兴趣的小伙伴可以直接拉到底自行学习~
我简单总结下大佬的实践思路:
- 新建 versions.gradle 用于存放依赖/插件配置,在这里支持依赖更新/提示;
- 新建 updateDependencies.gradle task,用于将更新后的依赖/插件同步 groovy;
- 使用直接调用 groovy 即可。
Step 1:在项目根目录下创建 buildSrc 目录
Step 2:新建 version.gradle 依赖/插件管理
大佬在日志中以及写的很明确了,这里我单独说下我期间遇到的坑,或者是重点吧,让看到此文的小伙伴更快的上手。
- version 之间是一些版本的配置 , 解析后会放到 Dependencies.kt 的 object Versions 中,必须存在,如下:
/*<version>*/ <--- 必须存在
// 对应的版本信息
def compileSDK = 29 // 编译 SDK 版本
def buildTools = "29.0.3" // Gradle 编译项目工具版本
def minSDK = 23 // 最低兼容 Android 版本
def targetSDK = 29 // 最高兼容 Android 版本
/*</version>*/ <--- 必须存在
- dep 之间是插件/依赖库引用路径,解析后会放到 Dependencies.kt 的 object Deps 中,同样必须存在,如下: /*<dep>*/ <--- 必须存在 // gradlePlugin implementation "com.android.tools.build:gradle:4.0.1" // permissionsDispatcher:Android 6.0 动态权限管理 implementation "org.permissionsdispatcher:permissionsdispatcher:4.7.0" /*</dep>*/ <--- 必须存在
这里需要注意下,// 后第一位代表你在使用中调用的名称,:后代表对当前依赖的描述。
完整的 version.gradle 内容如下(篇幅有限,移除部分项目中使用依赖):
代码语言:javascript复制dependencies {
/* readme *
*
* 为了统一管理插件/依赖库版本,避免版本冲突,统一将插件/依赖库信息配置在此文件中,
* 通过gradlew updateDependencies task
* 解析此文件生成对应内容到Dependencies.kt中进行统一引用
*
* <version> </version> 之间是一些版本的配置 , 解析后会放到Dependencies.kt的object Versions中
*
* <dep> </dep> 之间是插件/依赖库引用路径 , 解析后会放到Dependencies.kt的object Deps中
*
* 配置插件/依赖库引用说明:
* 0、版本配置格式:def <name> = <value>
* 1、配置插件/依赖库引用路径前备注格式://<插件/依赖库名> : <备注>,这个部分会被解析确定插件/依赖库引用名称
* 2、配置插件/依赖库引用路径时以 implementation 作为开头
* 3、更新配置后执行 updateDependencies.gradle 的 updateDependencies task 同步更新到Dependencies.kt
*
* Extra:
* [Google's Maven Repository] (https://dl.google.com/dl/android/maven2/index.html)
*/
/*
* Version 部分
*/
/*<version>*/
def compileSDK = 29 // 编译 SDK 版本
def buildTools = "29.0.3" // Gradle 编译项目工具版本
def minSDK = 23 // 最低兼容 Android 版本
def targetSDK = 29 // 最高兼容 Android 版本
/*</version>*/
/*
* 插件/依赖库路径部分
*/
/*<dep>*/
// gradlePlugin
implementation "com.android.tools.build:gradle:4.0.1"
/**
* Third Lib
*/
// indicator:指示器
implementation "com.hlq-struggle:indicator:1.0.0"
// permissionsDispatcher:Android 6.0 动态权限管理
implementation "org.permissionsdispatcher:permissionsdispatcher:4.7.0"
// permissionsDispatcherProcessor
implementation "org.permissionsdispatcher:permissionsdispatcher-processor:4.7.0"
// mmkv:基于 mmap 的高性能通用 key-value 组件
implementation "com.tencent:mmkv-static:1.1.0"
/*</dep>*/
}
Step 3:新建(拷贝)大佬提供的 updateDependencies.gradle
相关日志写的很清楚了,大家仔细阅读即可。
以下内容主要是将 version 中按照规则写好的依赖/插件进行同步 groovy 中。
代码语言:javascript复制/**
* 将versions.gradle/xVersion.gradle中配置的版本信息生成到src/main/groovy/Dependencies.groovy中
* 执行该task方法:
* 方法1:
* New: 在gradle task列表面板点击'Execute Gradle Task'(类似大象的)按钮,在输入框输入'-p buildSrc updateDependencies'然后点回车键;
* Deprecated: 在gradle task列表面板点击'Run Gradle Task'(类似大象的)按钮,在'Gradle Project'栏选中buildSrc模块,在'Command line'栏输入'updateDependencies'然后点击'OK';
* 方法2:
* New: 在Terminal输入'gradlew -p buildSrc updateDependencies'然后执行
* Deprecated: 在Terminal输入'gradlew updateDependencies'然后执行
* 方法3:
* AS->Edit Configurations->Gradle,点击左上角' '添加配置:
* Name: $projectName:buildSrc [updateDependencies]
* Gradle project: $projectName/buildSrc
* Tasks: updateDependencies
* 点击'Apply'保存此配置,后续在项目的 gradle task 列表中就可以找到此 task 双击执行了
*/
task("updateDependencies") {
String inputFilePath = "versions.gradle"
String outputFilePath = "src/main/groovy/Dependencies.groovy"
// 将inputFilePath声明为该Task的inputs
inputs.file(inputFilePath)
// 将outputFilePath声明为outputs
outputs.file(outputFilePath)
doLast {
File inputFile = file(inputFilePath)
if (!inputFile.exists()) {
return
}
String inputTxt = inputFile.text
StringBuilder builder = new StringBuilder()
/*
* 解析拼接版本object
*/
builder.append("/**n")
.append(" * 版本信息n")
.append(" */n")
.append("interface Versions {n")
String startFlag = "/*<version>*/"
String endFlag = "/*</version>*/"
int start = inputTxt.indexOf(startFlag)
int end = inputTxt.indexOf(endFlag)
builder.append(" ")
.append(inputTxt.substring(start startFlag.length(), end).trim())
.append("n}nn")
/*
* 解析拼接依赖object
*/
builder.append("/**n")
.append(" * 依赖库路径n")
.append(" */n")
.append("interface Deps {n")
startFlag = "/*<dep>*/"
endFlag = "/*</dep>*/"
start = inputTxt.indexOf(startFlag)
end = inputTxt.indexOf(endFlag)
String depsTxt = inputTxt.substring(start startFlag.length(), end).trim()
int implementationIndex
int doubleSlashIndex
while (true) {
implementationIndex = depsTxt.indexOf("implementation")
if (implementationIndex == -1) {
break
}
doubleSlashIndex = depsTxt.lastIndexOf("//", implementationIndex)
String namePart
String name
while (true) {
namePart = depsTxt.substring(doubleSlashIndex 2, implementationIndex)
name = namePart.split(":")[0].trim()
if (!name.contains("/")) {
break
}
doubleSlashIndex = depsTxt.lastIndexOf("//", doubleSlashIndex - 2)
}
depsTxt = depsTxt.replaceFirst("implementation", String.format("def %s =", name))
}
builder.append(" ")
.append(depsTxt)
.append("n}n")
String resultTxt = builder.toString()
file(outputFilePath).withPrintWriter("utf-8", { writer ->
writer.print(resultTxt)
writer.flush()
})
}
}
Step 4:新建最后的 build.gradle
代码语言:javascript复制apply plugin: 'groovy'
apply from: 'updateDependencies.gradle'
Step 5:执行同步命令
当然大佬提供了三种方案,挑选个人习惯的一种即可。
在 Step 3 中拷贝如下命令:
代码语言:javascript复制-p buildSrc updateDependencies
注意我画红线的地方,这是 AS 提供的一个类似历史记录的操作,很方便的记录下我们上次使用的 task,省的每次都输入。
执行速度还是蛮快的,随后变生成了我们的 groovy 文件:
大概截取此文件内容,其实就是和我们的 versions.gradle 一样,不信你看:
Step 6:如何使用?
到这里其实就很 easy 了,简单举例。
Versions 使用:
Deps 使用:
如何更新以及同步?
图片没抓到,查看原文吧~
感谢掘金大佬~
四、基于 basic 继续封装抽取 build
基本完善之后,默默的赶紧还是有点不舒服的地方,例如:
现在的架构是一个 app 下对应其它 module,而每个 module 都会有一些相同却不相同的内容,如果后期调整,难倒我要一个个去修改吗?岂不是又让鸡老大一通鄙视么。
想想,再好好想想。
之前曾经做过一个 basic 抽取,同样将共有参数/信息提取到 basic.gradle 中,每个 module apply,这样就是减少不少代码量。
请看我封装好的 basic.gradle:
代码语言:javascript复制apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
// 指定用于编译项目的 API 级别
compileSdkVersion Versions.compileSDK
// 指定在生成项目时要使用的 SDK 工具的版本,Android Studio 3.0 后不需要手动配置。
buildToolsVersion Versions.buildTools
// 指定 Android 插件适用于所有构建版本的版本属性的默认值
defaultConfig {
minSdkVersion Versions.minSDK
targetSdkVersion Versions.targetSDK
versionCode 1
versionName "1.0"
// 仅保留中文资源
resConfigs "zh"
// 启用多 dex 文件
multiDexEnabled true
ndk {
// 设置支持的SO库架构
abiFilters "armeabi", "x86"
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
// 配置 Java 编译(编码格式、编译级别、生成字节码版本)
compileOptions {
encoding = 'utf-8'
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
// 开启视图绑定 兼容 Gradle 4.x 及以上版本
buildFeatures {
dataBinding = true
viewBinding = true // gradle 5.x
}
lintOptions {
// lint 异常后继续执行
abortOnError false
}
}
/**
* implementation:不会向下传递,仅在当前 module 生效; api:向下传递,所依赖的 module 均可使用
*/
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation Deps.kotlinStdlibJdk7
implementation Deps.appcompat
implementation Deps.coreKtx
testImplementation Deps.junit
androidTestImplementation Deps.extJunit
androidTestImplementation Deps.espressoCore
}
我的 app build.gradle:
代码语言:javascript复制apply plugin: 'com.android.application'
apply from: "../basic.gradle"
android {
// 指定 Android 插件适用于所有构建版本的版本属性的默认值
defaultConfig {
applicationId "com.pwccn.fadvisor"
}
// 封装项目的所有构建类型配置
buildTypes {
debug {
// Log 控制器 - 输出日志
buildConfigField "boolean", "LOG_DEBUG", "true"
// 对调试 build 停用 Crashlytics
ext.enableCrashlytics = false
// 禁止自动生成 build ID
ext.alwaysUpdateBuildId = false
// 关闭资源缩减
shrinkResources false
// 关闭代码缩减
minifyEnabled false
// 关闭 zipAlign 优化
zipAlignEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
// Log 控制器 - 禁止输出日志
buildConfigField "boolean", "LOG_DEBUG", "false"
// 启用资源缩减
shrinkResources true
// 启动代码缩减
minifyEnabled true
// 开启 zipAlign 优化
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
/**
* implementation:不会向下传递,仅在当前 module 生效;api:向下传递,所依赖的 module 均可使用
*/
dependencies {
implementation Deps.legacySupportV4
implementation Deps.lifecycleExtensions
implementation Deps.lifecycleViewModelKtx
// 模块化部分导入部分
// helper
implementation project(path: ':helper')
// weight
implementation project(path: ':weight')
// 常用三方依赖导入部分
// 。。。
}
我的 helper module:
代码语言:javascript复制apply plugin: 'com.android.library'
apply from:"../basic.gradle"
dependencies {
api Deps.constraintLayout
api Deps.xPopup
// helper
implementation project(path: ':helper')
}
ummm,basic 配合 buildSrc,果然觉得巴适许多。
强烈推荐第三种以及第四种结合使用,那感觉,真的是 feel 倍儿爽~
参考资料
- 配置项目全局属性
- Use buildSrc to abstract imperative logic
- refreshVersions
- 掘金之路(一)统一管理插件和依赖库信息->buildSrc
- maven.google.com
- BuildSrcDemo
欢迎各位关注
不定期发布
见证成长路