Android的插件化開發,這個坑很是深,其中有一個問題就是 bundle 和 host 的版本不一致性問題,若是 bundle 中 sdk 的版本和 host 中 sdk 版本不一致,就頗有可能出現 api 兼容性問題,致使運行時 crash。api
一開始會想:"讓 host 和 bundle 中的版本號抽離成一個文件不就好了?"緩存
答案確定是不行,由於這樣只能讓直接依賴的版本一致,不能讓傳遞依賴的版本一致化。app
具體爲何不行,舉幾個例子:ide
狀況一: gradle
如上圖所示,host 在 resolve dependencies
以後,依賴的 C 庫版本爲 1.1
,而 bundle 由於沒有直接依賴 C 庫,因此 resolve dependencies
以後 C 庫的版本是 1.0
ui
這裏就存在一個版本不一致的問題了,此時若是 bundle 中使用了一個 C 庫不向下兼容的 api,運行時就會跪lua
bundle 是 provide 引入 C 庫,最後打包後 C 庫的實現代碼在 host 中spa
對於上述問題,有個簡單的解決方案就是禁用 bundle 的傳遞依賴功能: 插件
只要 bundle 沒有傳遞依賴,全部版本都手動指定,這樣能夠避免版本號不一致的問題3d
缺點:
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技術團隊
官方公衆號