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技術團隊
官方公衆號