微店的Android插件化的版本一致性問題解決實踐

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.0gradle

這裏就存在一個版本不一致的問題了,此時若是 bundle 中使用了一個 C 庫不向下兼容的 api,運行時就會跪ui

bundle 是 provide 引入 C 庫,最後打包後 C 庫的實現代碼在 host 中lua

對於上述問題,有個簡單的解決方案就是禁用 bundle 的傳遞依賴功能: spa

只要 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 的依賴樹已分析完畢

翻了幾天的官方文檔,找到了如下解決方案:

// 此方法爲阻塞方法,保證當前 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技術團隊官方公衆號

微店App技術團隊
相關文章
相關標籤/搜索