彻底认识包(依赖)冲突
什么是包(依赖)冲突?
包冲突是指在一个项目的不同部分开发时调用的代码库、资源包的版本号不一致。 包冲突的实质是不同部分同一个功能实现用的代码和资源不一致。
举例说明:在一个Android项目中,假设主工程是A ,它调用(依赖)代码库B版本为2.0,简称B2.0,同时依赖代码库C,而C又依赖库B1.0,可以看到项目不同部分依赖的B出现了版本不一致,这就叫包依赖冲突。
包(依赖)冲突会带来什么影响?
在回答这个问题之前,我们首先要明确:在最终运行阶段每个类是唯一的,也就是说一个代码库只有一个版本,所以在打包时只能选一个版本B打包进Apk中。
基于以上原因,不管采用B的版本2.0和1.0都貌似不妥,都有可能出现运行时候异常,比如C调用B1.0中一个类SayHello,假如该类在B2.0中被删掉,那么C的代码中用到SayHello类就会在运行时报Class not found的错误。除了举例中的错误,你还可能遇到各种各样的运行错误,比如用的是B1.0版本,那么主工程A发现B2.0已经解决的bug在运行中依旧存在。
所以包冲突带来最主要问题就是:运行时异常。
谈到这里,你可能有一个疑惑:为啥不把把整个项目各个模块依赖的库版本统一后再做编译,有问题提前暴露出来,至少能够有效避免Class not found这类运行期间crash。
在回答这个问题之前先看下简化的Android打包流程:
在编译阶段,有的依赖库是已经编译好的,并且是按照他们自己设定的依赖版本编译好的,比如最开始那个Demo中,库C是是按照B1.0编译好的,但不包含Lib B的代码。然后主工程编译就是按照B2.0库编译,同样不包含库B代码,Apk中三方库的版本就是编译时候主工程的确定的版本,所以最后会将B2.0打包进入dex中。同样我们自己的Library Module跟三方库一样,也是提前先按照自己设定依赖编译好,这里也解释了,为啥有些三方库引入后会出现编译通过,运行报错的bug。
继续上面疑问,为啥打包流程这样设计,不把把整个项目各个模块依赖的库版本统一后再做编译?
1、一般主工程和三方库不是统一时期,同一批人做的,所以他们包含的同一个库很大概率是不同的。 比如还是例子中的三方库C,可能是团队C在2016年已经做成,它当时用的是最新的库B1.0,我们做主工程时候是2021年,这期间B从1.0 进化到2.0解决了不少Bug、代码发生很大变化,我们做主工程当然要用最新的B2.0,但是团队C已经解散,没人去维护升级C库,所以对于三方库C来说,如果它不用它自己设定的B1.0提前编译,而是提供源码给我们,让我们用2.0编译,就会出现编译不过的现象。
2 如果要设计编译阶段版本统一,就可能要求三方库把源码提供给我们而不是已经编译好的jar包,这显然对三方库的代码安全性、商业机密造成严重泄漏。
3 提前编译好,可以使我们编译时候,只需要编译主工程和少量lib工程,大大加快了编译速度。
由于以上原因,我们很难在做到的整个项目各个模块版本统一再编译。
其实上面也提到包冲突依赖可能造成的第二个问题:编译失败。这个问题暂且放一下,大家思考下,1 编译失败一般是指谁编译失败? 2 在什么场景下会出现这个问题?这个问题答案会在解决包冲突中逐步揭开。
怎么解决包(依赖)冲突?
一切冲突解决的终极理想方案是开发调用版本统一! 比如之前例子中,我们添加库B的时候,发现C中用了B1.0,那么最好的解决方案就是,在满足主工程A功能需求情况下A也用B1.0,如果B1.0不满足,通知C升级到B2.0。
虽然很多情况下做不到开发用统一版本,但我们可以把统一版本做为冲突依赖的解决重要方向。比如库B在从1.0升级到2.0时候,B2.0兼容所有B1.0所有功能,我们便可以放心把B2.0打包进最后Apk中。
但现实往往不能通过这个理想方案去解决,比如下面的情况:
三方库C和三方库D使用的版本是B1.0 和B1.1,我们作为主工程开发者很难去推动库C 和库D的开发者更新它们的依赖库B,如果不是一个公司的更是不知道怎么去有效推动。因为整个项目Apk最后运行的时候,只能是B某一个版本,那这时间我们就要做各种各样的折中平衡方案了,比如比如选择B1.1作为运行时候版本,把用到库B的地方反复测试,并祈祷他没有测试到的地方也没有问题,虽然听起来很可笑,但这的确是事实。
所以包冲突依赖解决思路大致如下流程:
依赖冲突解决流.png
现在就流程中用到的一些工具和方法做说明:
1如何检测包依赖冲突
window 环境下->Andorid Studio命令行->gradlew app:dependencies > dps.txt
Mac环境下->Andorid Studio命令行->./gradlew app:dependencies > dpes
都会生成依赖图,如下图所示,就能看到每个库和整个项目的依赖图
依赖图谱示例.png
当然命令还可以设置多种参数用来过滤显示不同的关注部分。
2 如何设定选取的版本号
如果不做特殊设定,主工程编译和最后的项目运行都选择的是依赖库的最高版本号。可以决定库版本号的工具主要有:
force
exclude
他们都是在主工程的构建配置文件 build.gradle中使用,也只是对主工程编译起作用。其中force是强制使用一个库的版本来编译和运行,不去考虑是否有更高的版本;
exclude指明在依赖一个三方库时候,不考虑它依赖的某个特定库版本号对主工程编译和最后运行使用的库版本的影响。比如主工程A依赖B1.0,依赖C,C依赖B2.0,我们在添加依赖C的时候,采用exclude B,那么主工程编译成Apk决定使用B的版本时候就不考虑C依赖的B版本号。
具体使用可以通过搜索引擎搜:依赖冲突解决,Gradle依赖项学习等
比如:https://www.paincker.com/gradle-dependencies
小结
包冲突是指在一个项目的不同部分引用(依赖)的代码库、资源包的版本号不一致,由于在最终运行阶段,一个代码库只能存在一份,所以冲突解决的终极理想方案是开发用版本统一,但是现实情况往往做不到版本统一,于是我们需要用各种折中方案,找到一个最适合的版本库,既可以能够通过编译,也能够满足项目业务需求。