图片来自:unsplash.com
本文作者: lizongjun

前言

gradle 中的 dependencies 指令算是日常开发运用比较多的一个指令,可以帮助咱们定位一些二方、三方库版别依靠的问题。

不过在运用 dependencies 时有一些细节之前一向没有搞清楚,遂研究了一下部分细节。本文整体参考 gradle 官方文档,咱们感兴趣也可以自己深入研究下。

比方随便找一份 dependencies 输出如下,

读懂 gradle dependencies

可以发现,除了咱们熟知的树状打开结构,表明依靠的层级;在版别号前后是有一些特殊标识的:->(c)(*)

这些特殊标识别离有什么效果,对咱们剖析版别依靠会有什么影响?本文会依次剖析一些这些场景的版别标识符号。

Dependency resolution

-> 标识代表 依靠抵触,也是在 dependencies graph 中最常见的一种标识。

比方 1.3.2 -> 1.6.0,表明当时依靠树中依靠的版别是 1.3.2,但因为大局的依靠抵触,最终被晋级到了 1.6.0 版别。gradle 处理依靠抵触的总体原则是取抵触中的最高版别,但有许多特例。

特例状况咱们本次不详细打开,只看惯例状况,实际上仅是惯例状况也有让人利诱之处。

咱们假定下面这样一个demo,

// module A, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_d:1.0.0'
}
// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.0.0'
}

当咱们检查 app module 的 dependencies 输出时(下文 dependencies 的输出都是根据 app module的),成果如下,

--- com.netease.cloudmusic.android:module_a:1.0.0
     +--- com.netease.cloudmusic.android:module_c:1.0.0
     --- com.netease.cloudmusic.android:module_d:1.0.0

现在引进依靠抵触,

// module A, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_d:1.0.0'
}
// module B, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.1.0'
}
// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_b:1.0.0'
}

此刻也比较简单,因为 module B 中依靠了 1.1.0 版别的 module C,依靠产生抵触以最高版别为准,所以最终 dependencies 的输出如下,此刻 -> 表明了抵触的成果,

+--- com.netease.cloudmusic.android:module_a:1.0.0
|    +--- com.netease.cloudmusic.android:module_c:1.0.0 -> 1.1.0
|    --- com.netease.cloudmusic.android:module_d:1.0.0
--- com.netease.cloudmusic.android:module_b:1.0.0
     --- com.netease.cloudmusic.android:module_c:1.1.0

再杂乱一点,引进一个直接抵触,

// module A, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_d:1.0.0'
}
// module A, tag 1.1.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.1.0'
    implementation 'com.netease.cloudmusic.android:module_d:1.1.0'
}
// module B, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.1.0'
}
// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_b:1.0.0'
}

此刻,module B 不再直接依靠 module C,而是通过依靠高版别的 module A,直接引进了 1.1.0 版别的 module C,dependencies 输出如下,

+--- com.netease.cloudmusic.android:module_a:1.0.0 -> 1.1.0
|    +--- com.netease.cloudmusic.android:module_c:1.1.0
|    --- com.netease.cloudmusic.android:module_d:1.1.0
--- com.netease.cloudmusic.android:module_b:1.0.0
     --- com.netease.cloudmusic.android:module_a:1.1.0 (*)

注意此刻 module A 到 module C 这条引用链上的版别标识:关于 module A,因为依靠抵触,版别变为 1.0.0 -> 1.1.0 ;但关于 module C,版别并不是 1.0.0 -> 1.1.0,而直接是 1.1.0。也便是说叶子节点的版别是以父节点版别的右值为准的。

假如咱们再修改一下 demo,可以更清晰的解说这儿的逻辑。咱们把 demo 调整成如下这样,

// module A, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_d:1.0.0'
}
// module A, tag 1.1.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.1.0'
    implementation 'com.netease.cloudmusic.android:module_d:1.1.0'
}
// module B, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.1.0'
}
// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_b:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_c:1.2.0'
}

module A、B、C、D 之间的依靠联系不变,但咱们在 app module 直接依靠 1.2.0 版别的 module C,此刻 dependencies 是怎样的呢?

+--- com.netease.cloudmusic.android:module_a:1.0.0 -> 1.1.0
|    +--- com.netease.cloudmusic.android:module_c:1.1.0 -> 1.2.0
|    --- com.netease.cloudmusic.android:module_d:1.1.0
+--- com.netease.cloudmusic.android:module_b:1.0.0
|    --- com.netease.cloudmusic.android:module_a:1.1.0 (*)
--- com.netease.cloudmusic.android:module_c:1.2.0

可以很清晰的看到,关于产生抵触的版别:从父节点找子节点,看的是父节点的右值;而从子节点向父节点追溯,看的子节点的左值。

但单纯从视觉的直觉上看,咱们可能会误以为 1.2.0 版别的 module C 是由 module A 引进的,导致排查问题时南辕北辙,特别在排查大型项目的 dependencies 输出时,必定要注意每一层节点之间的抵触版别的左值与右值。

Dependency omitted

在前面的 demo 里,(*) 这个标识现已呈现过了,这个标识因为跟惯例语境下的含义不太相同,所以也具有必定的利诱性。(*) 符号字面意思是删减,但并不是依靠联系上的删减,只是是展现层面的删减。

还是以如下 demo 为例,

// module A, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_d:1.0.0'
}
// module B, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.0.0'
}
// module E, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.0.0'
}
// module F, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.0.0'
}
// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_b:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_e:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_f:1.0.0'
}

输出如下,

+--- com.netease.cloudmusic.android:module_a:1.0.0
|    +--- com.netease.cloudmusic.android:module_c:1.0.0
|    --- com.netease.cloudmusic.android:module_d:1.0.0
+--- com.netease.cloudmusic.android:module_b:1.0.0
|    --- com.netease.cloudmusic.android:module_a:1.0.0 (*)
+--- com.netease.cloudmusic.android:module_e:1.0.0
|    --- com.netease.cloudmusic.android:module_a:1.0.0 (*)
--- com.netease.cloudmusic.android:module_f:1.0.0
     --- com.netease.cloudmusic.android:module_a:1.0.0 (*)

这儿 (*) 代表省掉了 module A 以下的依靠联系子树,因为假定咱们按照 demo 来输出一个完好的依靠联系图,应该是下面这样的,

+--- com.netease.cloudmusic.android:module_a:1.0.0
|    +--- com.netease.cloudmusic.android:module_c:1.0.0
|    --- com.netease.cloudmusic.android:module_d:1.0.0
+--- com.netease.cloudmusic.android:module_b:1.0.0
|    --- com.netease.cloudmusic.android:module_a:1.0.0
|         +--- com.netease.cloudmusic.android:module_c:1.0.0
|         --- com.netease.cloudmusic.android:module_d:1.0.0
+--- com.netease.cloudmusic.android:module_e:1.0.0
|    --- com.netease.cloudmusic.android:module_a:1.0.0
|         +--- com.netease.cloudmusic.android:module_c:1.0.0
|         --- com.netease.cloudmusic.android:module_d:1.0.0
--- com.netease.cloudmusic.android:module_f:1.0.0
     --- com.netease.cloudmusic.android:module_a:1.0.0
          +--- com.netease.cloudmusic.android:module_c:1.0.0
          --- com.netease.cloudmusic.android:module_d:1.0.0

假如都按这种方式展现,明显冗余信息太多了,特别关于一个大型项目,依靠联系杂乱时,几乎是不行阅览的。所以为了简练、便利了解,dependencies 指令会默许缩略重复的依靠联系子树,只在它第一次呈现时,才完好展现;后续呈现都以 (*) 符号替代。

这也解说了为什么咱们在从上向下阅览一个 dependencies graph 时,会感觉越接近最初,依靠联系越杂乱、层级越深,越接近末尾依靠联系越简单。其实并不是因为 gradle 对依靠联系做了排序,只是是因为接近尾部,大部分子树都被缩略掉了。

Dependency constraint

(c) 这个标识对应 dependecy constraint,这部分逻辑的详细解说可以参考 这个章节,它对应 gradle 中的 constraints 闭包(如下),

dependencies {
    constraints {
        implementation('com.netease.cloudmusic.android:module_c:1.1.0') {
            because 'previous versions have a bug impacting this application'
        }
    }
}

constraints 闭包的效果可以简单解说成不通过直接依靠来晋级某个直接依靠的版别,比方下面这个 demo,

// module A, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_d:1.0.0'
}
// module A, tag 1.1.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.1.0'
    implementation 'com.netease.cloudmusic.android:module_d:1.1.0'
}
// module A, tag 1.2.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_d:1.2.0'
}
// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.0.0'
}

假定,现在咱们发现 module C 在 1.0.0 版别有一个 bug,需要晋级 module C 到 1.1.0 版别来修正;但囿于种种原因咱们不能直接运用 1.1.0 版别的 module A,比方咱们暂时不希望晋级 module D 到 1.1.0 版别。

面临这种问题时,咱们可能会按下面这种写法来躲避,

// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.0.0'
    implementation 'com.netease.cloudmusic.android:module_c:1.1.0'
}

此刻依靠联系如下,

+--- com.netease.cloudmusic.android:module_a:1.0.0
|    +--- com.netease.cloudmusic.android:module_c:1.0.0 -> 1.1.0
|    --- com.netease.cloudmusic.android:module_d:1.0.0
--- com.netease.cloudmusic.android:module_c:1.1.0

但这种写法的缺陷是:咱们引进了一个不必要的依靠,在 app module 直接依靠了 module C。

假定当 module A 晋级到 1.2.0 版别,此刻 module A 不再依靠 module C 了,

// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.2.0'
    implementation 'com.netease.cloudmusic.android:module_c:1.1.0'
}

但因为咱们显式的依靠了 module C,导致 module C 不再是由 module A 来引进,依靠联系产生了紊乱,这并不符合咱们的预期,特别在杂乱项目中会引进许多不必要的麻烦。

+--- com.netease.cloudmusic.android:module_a:1.2.0
|    --- com.netease.cloudmusic.android:module_d:1.2.0
--- com.netease.cloudmusic.android:module_c:1.1.0

假如换成运用 constraints 闭包来完成上面的 demo 就不同了,

// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.0.0'
    constraints {
        implementation 'com.netease.cloudmusic.android:module_c:1.1.0'
    }
}

此刻,就会产生 (c) 这个标识,

+--- com.netease.cloudmusic.android:module_a:1.0.0
|    +--- com.netease.cloudmusic.android:module_c:1.0.0 -> 1.1.0
|    --- com.netease.cloudmusic.android:module_d:1.0.0
--- com.netease.cloudmusic.android:module_c:1.1.0 (c)

当 module A 晋级到 1.2.0 之后,

// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.2.0'
    constraints {
        implementation 'com.netease.cloudmusic.android:module_c:1.1.0'
    }
}

对 module C 的依靠会主动失效,

+--- com.netease.cloudmusic.android:module_a:1.2.0
     --- com.netease.cloudmusic.android:module_d:1.2.0

而假如将 demo 改成这样,持续晋级 module A,

// module A, tag 1.3.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.3.0'
    implementation 'com.netease.cloudmusic.android:module_d:1.3.0'
}
// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.3.0'
    constraints {
        implementation 'com.netease.cloudmusic.android:module_c:1.2.0'
    }
}

此刻输出如下,仍然会展现 (c) 标识,但最终版别选取了更高的 1.3.0 版别。

+--- com.netease.cloudmusic.android:module_a:1.3.0
|    +--- com.netease.cloudmusic.android:module_c:1.3.0 
|    --- com.netease.cloudmusic.android:module_d:1.3.0
--- com.netease.cloudmusic.android:module_c:1.2.0 -> 1.3.0 (c)

一起关于 constraints 闭包,也可以用来完成 dependency version alignment。

以文章最初展现的那个成果为例,这儿 kotlinx-coroutines-bom 望文生义是 kotlin 协程的 bom(Bill Of Materials)模块,

--- org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.0
	 +--- org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.0
	 |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0 (c)
	 |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0 (c)
	 |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.6.0 (c)
	 |    --- org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.0 (c)
	 +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.0 (*)
	 --- org.jetbrains.kotlin:kotlin-stdlib-common:1.6.0

在这个 bom 库的 build.gradle 文件 中,有如下逻辑,

dependencies {
    constraints {
        rootProject.subprojects.each {
            if (rootProject.ext.unpublished.contains(it.name)) return
            if (it.name == name) return
            if (!it.plugins.hasPlugin('maven-publish')) return
            evaluationDependsOn(it.path)
            it.publishing.publications.all {
                ...
                api(group: it.groupId, name: it.artifactId, version: it.version)
            }
        }
    }
}

本质上便是通过 constraints 闭包,来确保 kotlinx-coroutines-androidkotlinx-coroutines-corekotlinx-coroutines-jdk8kotlinx-coroutines-core-jvm 这几个子模块的版别共同。

可见,相似协程这种一对多的库,可以通过抽取一个 bom 模块,使用 constraints 闭包来束缚各子 module 版别共同,避免因为版别不共同而引发的问题。

Downgrading versions

与 dependecy constraint 对应的方案是 downgrading versions,用来处理依靠版别的降级,这儿不过多介绍它们的用法,只看它们对dependencies输出的影响。

这儿要点看一下 forcestrictly 关键字的差异,还是如下 demo,比方咱们想降级 module C 的版别,

// module A, tag 1.1.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.1.0'
    implementation 'com.netease.cloudmusic.android:module_d:1.1.0'
}
// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.1.0'
    implementation('com.netease.cloudmusic.android:module_c') {
        version {
            strictly '1.0.0'
        }
    }
}

此刻 dependencies 输出如下,

+--- com.netease.cloudmusic.android:module_a:1.1.0
|    +--- com.netease.cloudmusic.android:module_c:1.1.0 -> 1.0.0
|    --- com.netease.cloudmusic.android:module_d:1.1.0
--- com.netease.cloudmusic.android:module_c:{strictly 1.0.0} -> 1.0.0

可以看到,在 dependencies 中有一个 strictly 关键字。

但假如运用 force 属性,写一个相似的 demo,

// module A, tag 1.1.0, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_c:1.1.0'
    implementation 'com.netease.cloudmusic.android:module_d:1.1.0'
}
// app, build.gradle
dependencies {
    implementation 'com.netease.cloudmusic.android:module_a:1.1.0'
    implementation('com.netease.cloudmusic.android:module_c:1.0.0') {
        force = true
    }
}

得到 dependencies 输出如下,

+--- com.netease.cloudmusic.android:module_a:1.1.0
|    +--- com.netease.cloudmusic.android:module_c:1.1.0 -> 1.0.0
|    --- com.netease.cloudmusic.android:module_d:1.1.0
--- com.netease.cloudmusic.android:module_c:1.0.0

可读性则不如 strictly 关键字,没有任何标识可以区别,并且在 gradle 的较高版别,force 关键字现已被符号废弃了。

总结

通过对 dependencies graph 中几个常见的版别标识符进行剖析,尤其是产生依靠抵触时的详细表现,咱们现已可以区别 dependencies 中产生依靠抵触、依靠晋级、依靠降级时版别符号的差异。使用这种差异,可以更好的帮忙咱们剖析、定位版别依靠的问题。

参考资料

  • docs.gradle.org/current/use…

本文发布自网易云音乐技术团队,文章未经授权制止任何方式的转载。咱们常年招收各类技术岗位,假如你准备换工作,又刚好喜爱云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!