Gradle 依赖切换源码实践示例详解

目录
  • 引言
  • 1、一般的修改办法
  • 2、通过 Gradle 脚本动态修改依赖
    • 2.1 配置文件和工作流程抽象
    • 2.2 为项目动态添加子工程
    • 2.3 使用子工程替换依赖
    • 2.4 注意事项
  • 总结

引言

最近,因为开发的时候经改动依赖的库,所以,我想对 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 id
  • open:表示是否启用源码依赖
  • 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 指令。该指令用来获取当前仓库所处的分支。

最后,将源码拉下来之后通过 Settingsinclude() 方法加载指定的子工程,并使用 Settingsproject() 方法指定该子工程的目录。这和我们在 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'
}

这种写法并不陌生。这里的 forcedependencySubstitution 一样,都属于 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 响应的速率。

总结

上述是开发过程中替换依赖为源码的“无痕”修改方式。不论在组件化还是非组件化需要开发中都是一种非常实用的开发技巧。按照上述开发开发方式,我们可以既能开发 android-mvvm-and-architecture 的时候随时随地打开 AndroidUtils 进行修改,亦可对 AndroidUtil 这个工程独立编译和开发。

源代码参考 android-mvvm-and-architecture 项目(当前是 feature-3.0 分支)的 AppShell 下面的 sources.gradle 文件。

以上就是Gradle 依赖切换源码实践示例详解的详细内容,更多关于Gradle 依赖切换的资料请关注我们其它相关文章!

(0)

相关推荐

  • Androd Gradle模块依赖替换使用技巧

    目录 背景 解决 步骤1: 步骤2: 步骤3: 背景 我们在多模块项目开发过程中,会遇到这样的场景,工程里依赖了一个自己的或者其他同事的 aar 模块,有时候为了开发调试方便,经常会把 aar 改为本地源码依赖,开发完毕并提交的时候,会再修改回 aar 依赖,这样就会很不方便,开发流程图示如下: 解决 一开始我们通过在 app 的 build.gradle 里的 dependency 判断如果是需要本地依赖的 aar,就替换为 implementation project 依赖,伪代码如下: d

  • Kotlin+buildSrc更好的管理Gadle依赖译文

    目录 前言 管理Gradle依赖的三种不同方法: 1) 手动管理 Google推荐:使用gradle的extra属性 Kotlin + buildSrc == Android Studio Autocomplete

  • Android开发gradle拉取依赖的加速配置

    目录 前言 情况1 : 情况2: 前言 镜像配置都是常规操作,必要时也可以上代理. 自己搭的nexus本质也是一种镜像,可以代理maven中央仓库. 各个仓库的测速,可以使用这个脚本: 通过测速,调整仓库的顺序 apply from: 'https://raw.githubusercontent.com/hss01248/flipperUtil/master/deps/depsLastestChecker.gradle' 情况1 : 每次点击sync project with gradle fi

  • Android Gradle 三方依赖管理详解

    目录 发展历史 最原始的依赖 使用 .gradle 配置 使用 gradle.properties 配置 使用 buildSrc 配置 使用 Composing Builds 配置 Version Catalogs 配置 开始使用 使用 settings.gradle.kts 配置 使用 libs.versions.toml 配置 使用插件配置 插件配置 插件使用 重写版本 使用方式 总结 发展历史 Gradle 的依赖管理是一个从开始接触 Android 开发就一直伴随着我们的问题(作者是An

  • IDEA 下 Gradle 删除多余无用依赖的处理方法

    目录 简介 如何使用 1.引入插件 2.应用插件 3.使用 Gradle 进行重新载入项目 4.生成报告 5. 删除无用依赖 特殊情况 Lombok 总结 简介 项目中经过很久开发,会有很多当初引入后来又不再使用的依赖,靠肉眼很难分辨删除. 这时候,我们可以使用分析无用依赖插件进行处理:gradle-lint-plugin 如何使用 注意: 他可能存在删除错误的引用依赖,需要删除后进行检查和测试 并且,这里仅支持单模块项目,如果是多模块项目请参考官方文档进行处理 官方文档地址: https://

  • Android三方依赖冲突Gradle中exclude的使用

    目录 一.场景 二.如何查看项目中的三方依赖? 三.使用exclude解决依赖冲突的问题 四.总结 一.场景 Android项目的开发过程中,我们项目中的gradle的dependencies闭包中会引入很多三方依赖库,引入的库越多,越容易产生库之间的依赖冲突. 列举冲突的场景: 1.同一个依赖库引入多个版本: 2.重复引入了同一个依赖库: 编译报错信息一般为:Program type already present 二.如何查看项目中的三方依赖? 方案一: Gradle task工具查看 按照

  • Gradle 依赖切换源码实践示例详解

    目录 引言 1.一般的修改办法 2.通过 Gradle 脚本动态修改依赖 2.1 配置文件和工作流程抽象 2.2 为项目动态添加子工程 2.3 使用子工程替换依赖 2.4 注意事项 总结 引言 最近,因为开发的时候经改动依赖的库,所以,我想对 Gradle 脚本做一个调整,用来动态地将依赖替换为源码.这里以 android-mvvm-and-architecture 这个工程为例.该工程以依赖的形式引用了我的另一个工程 AndroidUtils.在之前,当我需要对 AndroidUtils 这个

  • TDesign在vitest的实践示例详解

    目录 起源 痛点与现状 vitest 迁移 配置文件改造 开发环境 集成测试 ssr 环境 csr 环境 配置文件 兼容性 结果 CI测试速度提升 更清爽的日志信息 起源 在 tdesign-vue-next 的 CI 流程中,单元测试模块的执行效率太低,每次在单元测试这个环节都需要花费 6m 以上.加上依赖按照,lint 检查等环节,需要花费 8m 以上. 加上之前在单元测试这一块只是简单的处理了一下,对开发者提交的组件也没有相应的要求,只是让它能跑起来就好.另一方面单元测试目前是 TD 发布

  • 微服务架构之服务注册与发现实践示例详解

    目录 1 服务注册中心 4种注册中心技术对比 2 Spring Cloud 框架下实现 2.1 Spring Cloud Eureka 2.1.1 创建注册中心 2.1.2 创建客户端 2.2 Spring Cloud Consul 2.2.1 Consul 的优势 2.2.2 Consul的特性 2.2.3 安装Consul注册中心 2.2.4 创建服务提供者 3 总结 微服务系列前篇 详解微服务架构及其演进史 微服务全景架构全面瓦解 微服务架构拆分策略详解 微服务架构之服务注册与发现功能详解

  • react后台系统最佳实践示例详解

    目录 一.中后台系统的技术栈选型 1. 要做什么 2. 要求 3. 技术栈怎么选 二.hooks时代状态管理库的选型 context redux recoil zustand MobX 三.hooks的使用问题与解决方案 总结 一.中后台系统的技术栈选型 本文主要讲三块内容:中后台系统的技术栈选型.hooks时代状态管理库的选型以及hooks的使用问题与解决方案. 1. 要做什么 我们的目标是搭建一个适用于公司内部中后台系统的前端项目最佳实践. 2. 要求 由于业务需求比较多,一名开发人员需要负

  • vue从使用到源码实现教程详解

    搭建环境 项目github地址 项目中涉及了json-server模拟get请求,用了vue-router: 关于Vue生命周期以及vue-router钩子函数详解 生命周期 1.0版本 1.哪些生命周期接口 init Created beforeCompile Compiled Ready Attatched Detached beforeDestory destoryed 2.执行顺序 1. 不具有keep-alive 进入: init->create->beforeCompile->

  • 开源数据库postgreSQL13在麒麟v10sp1源码安装过程详解

    一.中标麒麟v10sp1在飞腾2000+系统安装略 二.系统依赖包安装 [root@ft2000db opt]# yum install bzip* [root@ft2000db opt]# nkvers ############## Kylin Linux Version ################# Release: Kylin Linux Advanced Server release V10 (Tercel) Kernel: 4.19.90-17.ky10.aarch64 Buil

  • React Context源码实现原理详解

    目录 什么是 Context Context 使用示例 createContext Context 的设计非常特别 useContext useContext 相关源码 debugger 查看调用栈 什么是 Context 目前来看 Context 是一个非常强大但是很多时候不会直接使用的 api.大多数项目不会直接使用 createContext 然后向下面传递数据,而是采用第三方库(react-redux). 想想项目中是不是经常会用到 @connect(...)(Comp) 以及 <Pro

  • swift MD5加密源码的实例详解

    swift MD5加密源码的实例详解 因为MD5加密是不可逆的,所以一般只有MD5加密的算法,而没有MD5解密的算法. 创建一个Sting+MD5.Swift字符串分类文件(同时此处需要创建一个bridge.h桥接文件,引入这个头文件 #import <CommonCrypto/CommonDigest.h>,md5加密方法需要使用的文件) 1.bridge.h桥接文件如下: #ifndef bridge_h #define bridge_h #import <CommonCrypto/

  • Android 网络html源码查看器详解及实例

    Android 网络html源码查看器详解及实例 IO字节流的数据传输了解 Handler的基本使用 1.作品展示 2.需要掌握的知识 FileInputStream,FIleOutputStream,BufferInputStream,BufferOutStream的读写使用与区别 //进行流的读写 byte[] buffer = new byte[1024 * 8]; //创建一个写到内存的字节数组输出流 ByteArrayOutputStream byteArrayOutputStream

  • jdk源码阅读Collection详解

    见过一句夸张的话,叫做"没有阅读过jdk源码的人不算学过java".从今天起开始精读源码.而适合精读的源码无非就是java.io,.util和.lang包下的类. 面试题中对于集合的考察还是比较多的,所以我就先从集合的源码开始看起. (一)首先是Collection接口. Collection是所有collection类的根接口;Collection继承了Iterable,即所有的Collection中的类都能使用foreach方法. /** * Collection是所有collec

随机推荐