Gradle 依赖切换源码实践示例详解
作者:开发者如是说 发布时间:2023-02-21 13:21:44
引言
最近,因为开发的时候经改动依赖的库,所以,我想对 Gradle 脚本做一个调整,用来动态地将依赖替换为源码。这里以 android-mvvm-and-architecture 这个工程为例。该工程以依赖的形式引用了我的另一个工程 AndroidUtils。在之前,当我需要对 AndroidUtils 这个工程源码进行调整时,一般来说有两种解决办法。
1、一般的修改办法
一种方式是,直接修改 AndroidUtils 这个项目的源码,然后将其发布到 MavenCentral. 等它在 MavenCentral 中生效之后,再将项目中的依赖替换为最新的依赖。这种方式可行,但是修改的周期太长。
另外一种方式是,修改 Gradle 脚本,手动地将依赖替换为源码依赖。此时,需要做几处修改,
修改 1,在 settings.gradle 里面将源码作为子工程添加到项目中,
include ':utils-core', ':utils-ktx'
project(':utils-core').projectDir = new File('../AndroidUtils/utils')
project(':utils-ktx').projectDir = new File('../AndroidUtils/utils-ktx')
修改 2,将依赖替换为工程引用,
// implementation "com.github.Shouheng88:utils-core:$androidUtilsVersion"
// implementation "com.github.Shouheng88:utils-ktx:$androidUtilsVersion"
// 上面的依赖替换为下面的工程引用
implementation project(":utils-core")
implementation project(":utils-ktx")
这种方式亦可行,只不过过于繁琐,需要手动修改 Gradle 的构建脚本。
2、通过 Gradle 脚本动态修改依赖
其实 Gradle 是支持动态修改项目中的依赖的。动态修改依赖在上述场景,特别是组件化的场景中非常有效。这里我参考了公司组件化的切换源码的实现方式,用了 90 行左右的代码就实现了上述需求。
2.1 配置文件和工作流程抽象
这种实现方式里比较重要的一环是对切换源码工作机制的抽象。这里我重新定义了一个 json 配置文件,
[
{
"name": "AndroidUtils",
"url": "git@github.com:Shouheng88/AndroidUtils.git",
"branch": "feature-2.8.0",
"group": "com.github.Shouheng88",
"open": true,
"children": [
{
"name": "utils-core",
"path": "AndroidUtils/utils"
},
{
"name": "utils-ktx",
"path": "AndroidUtils/utils-ktx"
}
]
}
]
它内部的参数的含义分别是,
name
:工程的名称,对应于 Github 的项目名,用于寻找克隆到本地的代码源码url
:远程仓库的地址branch
:要启用的远程仓库的分支,这里我强制自动切换分支时的本地分支和远程分支同名group
:依赖的 group idopen
:表示是否启用源码依赖children.name
:表示子工程的 module 名称,对应于依赖中的artifact id
children.path
:表示子工程对应的相对目录
也就是说,
一个工程下的多个子工程的
group id
必须相同children.name
必须和依赖的artifact id
相同
上述配置文件的工作流程是,
def sourceSwitches = new HashMap<String, SourceSwitch>()
// Load sources configurations.
parseSourcesConfiguration(sourceSwitches)
// Checkout remote sources.
checkoutRemoteSources(sourceSwitches)
// Replace dependencies with sources.
replaceDependenciesWithSources(sourceSwitches)
首先,Gradle 在 setting 阶段解析上述配置文件
然后,根据解析的结果,将打开源码的工程通过 project 的形式引用到项目中
最后,根据上述配置文件,将项目中的依赖替换为工程引用
2.2 为项目动态添加子工程
如上所述,这里我们忽略掉 json 配置文件解析的环节,直接看拉取最新分支并将其作为子项目添加到项目中的逻辑。该部分代码实现如下,
/** Checkout remote sources if necessary. */
def checkoutRemoteSources(sourceSwitches) {
def settings = getSettings()
def rootAbsolutePath = settings.rootDir.absolutePath
def sourcesRootPath = new File(rootAbsolutePath).parent
def sourcesDirectory = new File(sourcesRootPath, "open_sources")
if (!sourcesDirectory.exists()) sourcesDirectory.mkdirs()
sourceSwitches.forEach { name, sourceSwitch ->
if (sourceSwitch.open) {
def sourceDirectory = new File(sourcesDirectory, name)
if (!sourceDirectory.exists()) {
logd("clone start [$name] branch [${sourceSwitch.branch}]")
"git clone -b ${sourceSwitch.branch} ${sourceSwitch.url} ".execute(null, sourcesDirectory).waitFor()
logd("clone completed [$name] branch [${sourceSwitch.branch}]")
} else {
def sb = new StringBuffer()
"git rev-parse --abbrev-ref HEAD ".execute(null, sourceDirectory).waitForProcessOutput(sb, System.err)
def currentBranch = sb.toString().trim()
if (currentBranch != sourceSwitch.branch) {
logd("checkout start current branch [${currentBranch}], checkout branch [${sourceSwitch.branch}]")
def out = new StringBuffer()
"git pull".execute(null, sourceDirectory).waitFor()
"git checkout -b ${sourceSwitch.branch} origin/${sourceSwitch.branch}"
.execute(null, sourceDirectory).waitForProcessOutput(out, System.err)
logd("checkout completed: ${out.toString().trim()}")
}
}
// After checkout sources, include them as subprojects.
sourceSwitch.children.each { child ->
settings.include(":${child.name}")
settings.project(":${child.name}").projectDir = new File(sourcesDirectory, child.path)
}
}
}
}
这里,我将子项目的源码克隆到 settings.gradle
文件的父目录下的 open_sources
目录下面。这里当该目录不存在的时候,我会先创建该目录。这里需要注意的是,我在组织项目目录的时候比较喜欢将项目的子工程放到和主工程一样的位置。所以,上述克隆方式可以保证克隆到的 open_sources
仍然在当前项目的工作目录下。
然后,我对 sourceSwitches
,也就是解析的 json 文件数据,进行遍历。这里会先判断指定的源码是否已经拉下来,如果存在的话就执行 checkout 操作,否则执行 clone 操作。这里在判断当前分支是否为目标分支的时候使用了 git rev-parse --abbrev-ref HEAD
这个 Git 指令。该指令用来获取当前仓库所处的分支。
最后,将源码拉下来之后通过 Settings
的 include()
方法加载指定的子工程,并使用 Settings
的 project()
方法指定该子工程的目录。这和我们在 settings.gradle
文件中添加子工程的方式是相同的,
include ':utils-core', ':utils-ktx'
project(':utils-core').projectDir = new File('../AndroidUtils/utils')
project(':utils-ktx').projectDir = new File('../AndroidUtils/utils-ktx')
2.3 使用子工程替换依赖
动态替换工程依赖使用的是 Gradle 的 ResolutionStrategy 这个功能。也许你对诸如
configurations.all {
resolutionStrategy.force 'io.reactivex.rxjava2:rxjava:2.1.6'
}
这种写法并不陌生。这里的 force
和 dependencySubstitution
一样,都属于 ResolutionStrategy 提供的功能的一部分。只不过这里的区别是,我们需要对所有的子项目进行动态更改,因此需要等项目 loaded 完成之后才能执行。
下面是依赖替换的实现逻辑,
/** Replace dependencies with sources. */
def replaceDependenciesWithSources(sourceSwitches) {
def gradle = settings.gradle
gradle.projectsLoaded {
gradle.rootProject.subprojects {
configurations.all {
resolutionStrategy.dependencySubstitution {
sourceSwitches.forEach { name, sourceSwitch ->
sourceSwitch.children.each { child ->
substitute module("${sourceSwitch.artifact}:${child.name}") with project(":${child.name}")
}
}
}
}
}
}
}
这里使用 Gradle 的 projectsLoaded
这个点进行 hook,将依赖替换为子工程。
此外,也可以将子工程替换为依赖,比如,
dependencySubstitution {
substitute module('org.gradle:api') using project(':api')
substitute project(':util') using module('org.gradle:util:3.0')
}
2.4 注意事项
上述实现方式要求多个子工程的脚本尽可能一致。比如,在 AndroidUtils 的独立工程中,我通过 kotlin_version
这个变量指定 kotlin 的版本,但是在 android-mvvm-and-architecture 这个工程中使用的是 kotlinVersion
. 所以,当切换了子工程的源码之后就会发现 kotlin_version
这个变量找不到了。因此,为了实现可以动态切换源码,是需要对 Gradle 脚本做一些调整的。
在我的实现方式中,我并没有将子工程的源码放到主工程的根目录下面,也就是将 open_sources
这个目录放到 appshell 这个目录下面。而是放到和 appshell 同一级别。
这样做的原因是,实际开发过程中,通常我们会克隆很多仓库到 open_sources
这个目录下面(或者之前开发遗留下来的克隆仓库)。有些仓库虽然我们关闭了源码依赖,但是因为在 appshell 目录下面,依然会出现在 Android Studio 的工程目录里。而按照上述方式组织目录,我切换了哪个项目等源码,哪个项目的目录会被 Android Studio 加载。其他的因为不在 appshell 目录下面,所以会被 Android Studio 忽略。这种组织方式可以尽可能减少 Android Studio 加载的文本,提升 Android Studio 响应的速率。
来源:https://juejin.cn/post/7174753036143689787


猜你喜欢
- 源码地址:https://gitee.com/fighter3/eshop-project.git持续更新中……大家好,我是三分恶。这一节我
- 随着JDK 14的发布(https://waylau.com/jdk-14-released/),各大Java IDE也开始支持JDK 14
- 主要介绍springboot项目中配置文件的加密前言为了保证服务器相关信息的保密,一般会采用加密的方式进行对配置文件原文的加密,今天介绍下s
- 概念:有enum关键字修饰的类,成为枚举类1、枚举规则枚举类的对象可以有类里面定义,不支持重新new出来,枚举类有构造函数,其他的类都一样,
- RestTemplate第一次请求响应速度较慢问题使用RestTemplate请求微信的接口发现第一次请求需要8秒左右的时间,查阅了JDK资
- 依赖<dependency> <groupId>com.baomidou</groupId> <a
- 目前的App在安装后,第一次打开,都会显示两秒左右的logo,然后进入引导页。如果关闭App,再重新打开,则只会显示logo,然后直接进入主
- 目录一、首先导入生成二维码和微信支付环境二、在application.yml文件配置微信所有需的基本配置1.导入2.创建MyWXPayCon
- 曾经有一个朋友问过我一个问题, 一张512*512 150KB PNG格式图片和一张512*512 100KB 压缩比是8的JP
- 前言从windows窗口的概念开始,通过对比去理解Android窗口体系,本文没有深入源码,重在理解概念代码都是抄来抄去,概念也是互相借鉴
- 今天写程序的时候用到了附加属性,我是用VS内置的propa的代码段来实现的,代码如下:class Attach {
- 为什么Android要申请权限简单说下在Android6.0及6.0以上一些google认为涉及“危险和用户隐私”的一些权限不仅要做清单文件
- 《Spring Boot Actuator详解与深入应用》预计包括三篇,第一篇重点讲Spring Boot Actuator 1.x的应用与
- 有经验的程序员应该都见过,一个方法坐拥几十上百个参数。方法为何要有参数?因为不同方法间需共享信息。但方法间共享信息的方式不止一种,除了参数列
- 1、添加android support包因为上面的几个类都是在android support包中才提供,我们先添加包。在Eclipse-&g
- 某些Google Play服务(例如Google登录和App Invites)要求我们提供签名证书的SHA-1,以便google paly为
- 弃用内容先来纠正一个误区。主要之前在版本更新介绍的时候,存在一些表述上的问题。导致部分读者认为这次的更新是Datasource本身初始化的调
- 问题(1)synchronized的特性?(2)synchronized的实现原理?(3)synchronized是否可重入?(4)sync
- MybatisMyBatis ,是国内最火的持久层框架采用了ORM思想解决了实体类和数据库表映射的问题。对JDBC进行了封装,屏蔽了JDBC
- Bezier Curve算法是根据参数曲线方程来得到光滑曲线的一种算法,曲线方程的参数由控制点决定。其本质是由调和函数根据控制点插值而成,其