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 没有传递依赖,所有版本都手动指定,这样可以避免版本号不一致的问题
缺点:
人工前期的工作量较大,因为 bundle 阻断了传递依赖,如果需要用到非顶级依赖的库,需要手动引入
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 evaluationDependsOn(":app" ) def appProject = project(":app" )def configMap = [:]def moduleName = projects.project.namedef 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) } } appProject.configurations.all { def configurationName = processConfigurationName(it.name) def versionMap = configMap[configurationName] if (versionMap == null ) { versionMap = [:] configMap[configurationName] = versionMap } def ft = it.copyRecursive() ft.setCanBeResolved(true ) 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" ) { logger.error("宿主缺少 $moduleName 插件对应依赖:${groupName}" ) } else if (hostVersion != null && !updateSet.contains(groupName) && hostVersion != version) { updateSet.add(groupName) logger.warn("更新 $moduleName 插件依赖: ${groupName}:${version} -> $hostVersion" ) } dependency.children.forEach { dfsOutputUpdateDependencies(it, versionMap, moduleName, updateSet) } } 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) { it.useVersion(hostVersion) } } }
最后把上述代码写到一个 gradle 文件中,然后在插件的 build.gradle 里面 apply 它
该代码为示意代码,不保证可以顺利运行,知道原理的应该可以快速写出来