爆裂吧现实

不想死就早点睡

0%

插件化的版本一致性问题

Android 的插件化开发,这个坑非常深,其中有一个问题就是 bundle 和 host 的版本不一致性问题,如果 bundle 中 sdk 的版本和 host 中 sdk 版本不一致,就很有可能出现 api 兼容性问题,导致运行时 crash。

一开始会想:”让 host 和 bundle 中的版本号抽离成一个文件不就行了?

答案肯定是不行,因为这样只能让直接依赖的版本一致,不能让传递依赖的版本一致化


具体为什么不行,举几个例子:

情况一:

如上图所示,host 在 resolve dependencies 之后,依赖的 C 库版本为 1.1,而 bundle 因为没有直接依赖 C 库,所以 resolve dependencies 之后 C 库的版本是 1.0

这里就存在一个版本不一致的问题了,此时如果 bundle 中使用了一个 C 库不向下兼容的 api,运行时就会跪

bundle 是 provide 引入 C 库,最后打包后 C 库的实现代码在 host 中

对于上述问题,有个简单的解决方案就是禁用 bundle 的传递依赖功能:

只要 bundle 没有传递依赖,所有版本都手动指定,这样可以避免版本号不一致的问题

缺点:

  1. 人工前期的工作量较大,因为 bundle 阻断了传递依赖,如果需要用到非顶级依赖的库,需要手动引入
  2. bundle 中直接依赖的库必须在 host 中也写一份直接依赖,并且加上 force=true 不然如果 A 库中的 C 库升级到了 1.2,但是 host 中的依赖没有升级(还是 1.1)。最终因为 host 有传递依赖,bundle 没有传递依赖,导致 host 中编译版本为 C:1.2,bundle 中为 C:1.1

情况二:

情况二中 host 和 bundle 没有直接依赖 C 库,是传递依赖进来的,host 因为依赖了 B 库,导致 resolve dependencies 之后 C 库的版本为 1.1,而 bundle 还是 1.0


版本不一致问题直接导致了开发者必须去关心自己插件的依赖,会大幅降低开发效率,问题的根源很简单:bundle 中依赖的 version 和 host 不一致产生的,那么只要让 bundle 中依赖的 version 和 host 中依赖的 version 一致不就可以了

解决方案原理很简单:让 bundle 获取 host 的依赖树,并根据 host 的依赖树更新自己的依赖树

其实就是相当于单 application 时候的依赖自动升级(存在多版本的时候会自动选取最高的版本),只不过把依赖版本的检测范围扩大到了别的 module 中

知道原理后,接下来就是编写 gradle 插件,插件需要完成以下功能:

  • 插件的版本号修改:configurations 可以修改 dependencies 的版本号
  • 保证 bundle resolve dependencies 的时候,host 的依赖树已分析完毕

翻了几天的官方文档,找到了以下解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// 此方法为阻塞方法,保证当前 project 之后的代码运行在 host 工程构建脚本执行完之后
evaluationDependsOn(":app")
def appProject = project(":app")
def configMap = [:]
def moduleName = projects.project.name

// 辅助方法,处理 configuration name 映射
// 因为 bundle 中的 configuration name 大多数为 provided
def processConfigurationName(String name) {
if (name == null) {
return name
}
if (name.startsWith("provided")) {
return "compile"
}
name = name.toLowerCase()
name = name.replace("implementation", "compile")
name = name.replace("api", "compile")
name = name.replace("compileonly", "compile")
name = name.replace("runtimeonly", "compile")
return name
}

// 辅助方法,递归获取最终的依赖
def dfsGetDependencies(ResolvedDependency dependency, Map<String, String> versionMap) {
def groupName = "${dependency.moduleGroup}.${dependency.moduleName}"
def version = dependency.moduleVersion
versionMap[groupName] = version
dependency.children.forEach {
dfsGetDependencies(it, versionMap)
}
}

// host 依赖解析获取
appProject.configurations.all {
def configurationName = processConfigurationName(it.name)
def versionMap = configMap[configurationName]
if (versionMap == null) {
versionMap = [:]
configMap[configurationName] = versionMap
}
// 克隆一份 configuration,根据官方文档,克隆之后是 unResolve 的
// 这里必须克隆,否则会影响原先 host 的 resolve 过程
def ft = it.copyRecursive()
// resolve 依赖,下载或者从缓存中解析依赖树,阻塞方法
ft.setCanBeResolved(true)
// 收集 host 中的依赖
ft.resolvedConfiguration.getFirstLevelModuleDependencies().forEach {dfsGetDependencies(it, versionMap)}
}

// 打印输出下日志
def dfsOutputUpdateDependencies(ResolvedDependency dependency, Map<String, String> versionMap, String moduleName, Set<String> updateSet) {
def groupName = "${dependency.moduleGroup}.${dependency.moduleName}"
def version = dependency.moduleVersion
def hostVersion = versionMap[groupName]
if (hostVersion == null && groupName != "com.vdian.bundle.api.framework") {
// host 缺少对应依赖的生命
logger.error("宿主缺少 $moduleName 插件对应依赖:${groupName}")
} else if (hostVersion != null && !updateSet.contains(groupName) && hostVersion != version) {
// 根据宿主更新 bundle 中的依赖版本
updateSet.add(groupName)
logger.warn("更新 $moduleName 插件依赖: ${groupName}:${version} -> $hostVersion")
}
dependency.children.forEach {
dfsOutputUpdateDependencies(it, versionMap, moduleName, updateSet)
}
}

// 更新当前 bundle 的依赖
def updateSet = new HashSet<String>()
configurations.all {
def configurationName = processConfigurationName(it.name)
def versionMap = configMap[configurationName]
if (versionMap == null) {
logger.error("宿主缺少 configuration: ${configurationName}")
return
}
def ft = it.copyRecursive()
ft.setCanBeResolved(true)
ft.resolvedConfiguration.getFirstLevelModuleDependencies().forEach {
dfsOutputUpdateDependencies(it, versionMap, moduleName.toString(), updateSet)
}
it.resolutionStrategy.eachDependency {
def groupName = "${it.target.group}.${it.target.name}"
def hostVersion = versionMap[groupName]
if (hostVersion != null && hostVersion != it.target.version) {
// 根据宿主更新 bundle 中的依赖版本
it.useVersion(hostVersion)
}
}
}

最后把上述代码写到一个 gradle 文件中,然后在插件的 build.gradle 里面 apply 它

该代码为示意代码,不保证可以顺利运行,知道原理的应该可以快速写出来