Android的插件化开发,这个坑很是深,其中有一个问题就是 bundle 和 host 的版本不一致性问题,若是 bundle 中 sdk 的版本和 host 中 sdk 版本不一致,就颇有可能出现 api 兼容性问题,致使运行时 crash。api
一开始会想:"让 host 和 bundle 中的版本号抽离成一个文件不就好了?"缓存
答案确定是不行,由于这样只能让直接依赖的版本一致,不能让传递依赖的版本一致化。markdown
具体为何不行,举几个例子:app
状况一: ide
如上图所示,host 在 resolve dependencies
以后,依赖的 C 库版本为 1.1
,而 bundle 由于没有直接依赖 C 库,因此 resolve dependencies
以后 C 库的版本是 1.0
gradle
这里就存在一个版本不一致的问题了,此时若是 bundle 中使用了一个 C 库不向下兼容的 api,运行时就会跪ui
bundle 是 provide 引入 C 库,最后打包后 C 库的实现代码在 host 中lua
对于上述问题,有个简单的解决方案就是禁用 bundle 的传递依赖功能: spa
只要 bundle 没有传递依赖,全部版本都手动指定,这样能够避免版本号不一致的问题插件
缺点:
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
的版本号resolve dependencies
的时候,host 的依赖树已分析完毕翻了几天的官方文档,找到了如下解决方案:
// 此方法为阻塞方法,保证当前 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 它。
该代码为示意代码,不保证能够顺利运行,知道原理的应该能够快速写出来。
qigengxin,@WeiDian,2016年加入微店,目前主要负责微店App的基础支撑开发工做。
微店App技术团队
官方公众号