Android實戰:有贊微商城Android組件化方案,保證項目穩定性 一、概述 二、架構調整 三、實現方案 四、規劃 五、總結

最近羣裏的朋友們在討論組件化方案,我看了大家分享的文章,感覺受益匪淺,現在分享給大家,希望對大家的學習和工作有所幫助。

一、概述

目前有贊移動端的主要工作內容是在“有贊微商城”和“有贊零售”兩條公司主要的業務線,隨着有贊 Saas 業務的增長,客戶端也不斷迭代,支持越來越多的功能。 在這個業務快速增長的情況下,移動端技術的整體架構也是一直在不斷調整,來保證開發效率和業務的快速迭代。

這篇文章,主要是介紹有贊微商城 Android組件化的一些思路和實現。

1.1 現狀

客戶端的架構,從一開始的“All IN ONE” 模式(即所有代碼都在 App 中),逐漸演變到目前的一個單 Project 多 Module 結構:

1.2 痛點

新的項目架構,也帶了了新的問題: - 日益複雜的 Common 模塊,邏輯複雜,依賴不清晰,不敢隨便改動 Common 代碼,造成大量冗餘代碼和無法維護的業務邏輯 - 隨着業務模塊的增多,打包速度一發不可收拾;從倒杯水的時間到下樓喫個飯的時間,大大減慢了開發節奏 - 由於業務模塊跟項目中的上層(App 殼)和下層(Common 模塊)耦合 - 業務模塊增多,由於業務模塊沒有自己的生命週期,無法實現模塊之間的隔離,整體模塊控制比較混亂

1.3 需要解決的問題

  • Common模塊輕量化,需要將 Common 層的業務向上抽離,通用的底層和基礎組件、公用 UI 組件抽成單獨的依賴
  • 移動端業務服務化,解耦現有業務,抽象出業務接口,業務模塊只向外暴露自己的接口,並實現跨模塊之間的調用
  • 能夠配置單模塊或者多模塊打包,不用每次調試都全量打包,費時費力,又影響開發的節奏
  • 業務模塊的依賴和發佈管理

二、架構調整

我們之前雖然有在做整個工程模塊化的開發,但是目前的模塊化框架可以說是不夠徹底的:

  • 模塊只是項目結構的概念(一個模塊一個 Module),在邏輯層並沒有模塊這個概念
  • 模塊本身並沒有生命週期控制
  • 公用服務中心化,公用邏輯部分全部都在 Common 模塊中
  • 模塊對外暴露的服務不可知,都是直接依賴模塊內部的代碼邏輯
  • 模塊無法單獨打包,針對模塊的代碼改動,只能全量打包之後才能看到效果

爲了解決以上的問題,我們需要對現有的架構進行調整。

2.1模塊的抽象

將模塊的功能抽象出一些基礎類,組成了模塊化支持組件,它提供的功能有:

  • 抽象出模塊本身作爲某一類業務的容器,即所有業務模塊需要實現自己的模塊類,繼承自我們的 BaseModule,並在 App 殼工程中進行註冊
  • 模塊對象跟 Activity 一樣,擁有生命週期的概念,需要在生命週期的不同階段處理自己相應的邏輯(註冊服務、初始化數據等)
  • 模塊可以註冊的對外暴露的服務的實現,在註冊模塊的時候,模塊攜帶的服務也會被註冊到 App 的服務中心

2.2 公共業務去中心化

跟很多客戶端的同學聊過,很多 APP 發展到一定階段之後,必然會誕生一個所謂的 Common 模塊。它就像一個大儲物櫃,每個人都把一些其他人可能用到的東西一股腦兒塞進去。 這麼個塞法,會有兩個問題:

  1. 冗餘:比如一些工具類,很多時候,當你找不到需要的工具類的時候,你可能會塞一個新的進去
  2. 維護成本高:所有公用的業務邏輯的實現都在 Common 中,對一些公用業務邏輯的影響面無法掌控

2.2.1 Common 裏面都有什麼?

  • 工具類
  • 公用的 UI 組件
  • 多個業務模塊都公用的業務類
  • 基礎組件的封裝類(圖片庫、網絡庫、Webview)
  • 封裝的一些基類(BaseActivity,BaseFragment 什麼的)

2.2.2 解決的思路

  • 將公用的業務模塊向上抽離到業務模塊中(所謂業務模塊的服務化)
  • 將基礎組件抽象到一個獨立的組件中
  • 將一些基礎類下沉到不包含業務邏輯的底層核心庫中

2.3 業務模塊服務化

“服務化”這個詞,在服務端的開發中經常被提到,簡單來說,就是根據業務劃分爲多個模塊,模塊之間的交互以互相提供服務的方式來完成。 而客戶端隨着業務模塊的增多,也必然存在業務模塊之間存在業務依賴的情況,而 Android 端依賴的方式有:

  1. A 模塊直接依賴 B 模塊,直接調用 B 模塊的代碼邏輯
  2. 將 A 和 B 模塊中的公用部分放到 Common 模塊中,通過調用 Common 模塊的代碼實現依賴

2.3.1 業務模塊服務依賴的實現

  • 後端的服務化是藉助於 Dubbo 來構建的 RPC 服務,依賴某個服務,只需要依賴其對外暴露的 API 模塊(只包含接口和數據結構的 Maven 依賴),不需要依賴其具體實現,具體服務調用的實現由框架來實現
  • 客服端的依賴也可以參考這樣的方式來實現模塊之間的依賴,例如商品模塊,可以提供一個 API 層,用來對外暴露數據結構和服務

2.3.2 API 層實現方式

對外暴露服務的方式有很多種:

  • 協議的方式:例如"app://order/detail/get?id=100",數據可以用 JSON 來進行傳遞,請求本地服務就像調用一個 Http 服務一樣,根據請求協議來獲取數據,然後解析數據進行操作
  • 接口的方式:像後端使用 Dubbo 服務那樣,訂單模塊對外提供一個獨立的 Maven 依賴,裏面包含了數據接口和對外提供的服務接口,適用方依賴之後直接調用

2.3.3 接口的方式實現 API

協議的方式的問題:如果服務提供的地方更改了之後,需要手動去查詢所有調用到的地方,進行更改,而且沒有版本管理,而且數據解析都需要手動進行轉換,改動的成本比較高,也有一定穩定性風險。 接口的方式的問題:需要額外提供一個依賴(單獨把 API 層打包成一個 aar 包),使用方需要添加 Mave 依賴,所以引入依賴和發佈的成本比較高。

我們最終選擇了接口的方式,這種方式的穩定性和版本控制做的更好,對於改動來說,編譯過程自動會幫你校驗改動的影響面,而引入依賴和發佈成本高的問題,完全可以交給構建工具(Gradle Plugin)來解決。

2.3.4 業務實現層

業務實現層需要做的,就是實現自己模塊本身的業務邏輯,並實現自己提供的 API 接口,暴露對外的服務。

2.4 基礎組件抽象

2.4.1 現有的基礎組件實現

項目中現在有很多的基礎組件都是統一在 Common 裏面進行封裝的,例如:賬號庫、網絡庫、圖片加載、Web 容器庫等等,這也帶來了一些問題:

  1. Common 太重
  2. 業務模塊跟基礎組件強耦合,在開發一些跨團隊的組件過程中,如果碰到使用的基礎庫不同的時候,需要比較多的時間來做封裝
  3. 升級基礎組件或替換依賴的成本比較高,一些 API 的更改需要改動每個調用的地方

2.4.2 實現思路

  • 將常用的基礎組件整理,抽象成單獨的一個抽象層,裏面定義了一系列基礎組件接口(圖片加載、Web 容器、JsBridge 調用、賬號等等)
  • 把統一實現的組件放到另一個依賴裏面,可以在 App 中進行具體實現的註冊,而業務模塊本身,可以只依賴抽象

2.4.3 依賴結構

2.5 單/多模塊打包

隨着業務量和業務複雜度的增長,還有多個三方組件的引入,客戶端工程代碼量也變得越來越龐大,直接造成的一個問題是:打包慢!一個簡單的場景:當你開發了一個商品模塊內部的功能之後,你需要打整個 App 的包才能進行測試,而打一個包的時間可能是 5~10 分鐘,如果一天打包 10 次,也是比較酸爽。我們的組件也需要支持單模塊或者選定的某些進行打包,其中的思路也是通過自定義 Gradle Plugin 在編譯階段,動態去更改 Module 實際依賴的 Android Gradle 插件來實現的。 經測試,同一臺電腦,完整打包(clean之後再安裝)耗時 4 分鐘,而單模塊打包(同樣也是 clean 之後安裝)耗時 1 分鐘,整體打包時間降低了 70% 以上。

2.6 架構圖

上面的一些改進點,總結成一張圖,就是這樣的:

三、實現方案

目前我們的方案提供 3 個基礎組件依賴和 1 個 Gradle 插件:

  • modular-core: 提供組件模塊化生命週期和模塊服務註冊相關的模塊化基礎組件
  • modular-support: 對項目中二方、三方包接口的抽象
  • modular-support-impl:對項目中二方、三方包接口的默認抽象
  • modular-plugin: 支持模塊生成 API 層目錄,生成 APP 運行環境,以及管理模塊發佈的 Gradle 插件

3.1 Modular-core

3.1.1 實現模塊類

業務模塊類需要繼承 BaseModule:

public class ModuleA extends BaseModule {

        @Override
        public void onInstalled() {
            registerBusinessService(ModuleAService.class, new CachedServiceFetcher() {
                @Override
                public ModuleAService createService(@NotNull ModularManage manager) {
                    if (service == null) {
                        service = new ModuleAServiceImpl();
                    }
                    return service;
                }
            });
        }
    }

3.1.2 模塊生命週期

模塊有以下幾個生命週期:

  • onInstalled() -> 模塊被註冊的時候調用:Module 在 App 中被註冊的時候
  • onCreate() -> 模塊第一次啓動的時候調用:Module 所屬的某個 Activity 第一次啓動的時候
  • onStart() -> 模塊啓動的時候調用:模塊第一次啓動之的時候
  • onStop() -> 模塊停止的時候調用:Activity 棧裏面沒有模塊所屬 Activity 的時候

3.1.3 模塊生命週期的實現

其實組件內關於生命週期捕獲和監聽,都是藉助於 Google 的 Android Architecture Components 中的 Lifecycle 庫來實現的。

  • 模塊生命週期的捕獲:首先需要將 Activity 的類註冊到 Module 中,然後全局監聽 Activity 的 Lifecycle.Event 事件,就可以獲取到模塊內 Activity 的運行情況
  • 模塊生命週期的監聽:BaseModule 本身繼承了 LifecycleOwner 接口,可以對其添加觀察者,來實現對模塊生命週期的監聽

3.2 Modular-plugin

這裏需要依賴對於 Android 的構建工具 Gralde 的擴展,它支持的高度可擴展特性,幫助我們在組件化開發中更加高效,不需要關係一些額外的工作,只需要關注開發的內容即可,對現有的代碼邏輯基本沒有侵入。

3.2.1 Gralde 的生命週期

這裏必須要提一些的就是 Gradle 的生命週期,因爲我們的很多擴展功能,都是在對 Gradle 執行的生命週期的各個階段做一些改動來實現的,大概的生命週期如圖:


3.2.2 單模塊打包

Android 打包成 Apk 並運行的條件有:

  • AndroidManifest.xml 的配置支持(application 標籤的配置)
  • 主 Activity 的配置

3.2.2.1 實現原理

  • 自動生成模塊自己的 Application 類
  • 自動讀取 Module 的 AndroidManifest 文件並修改成可以打包成 App 的配置
  • 在打包的時候動態更改 SourceSet,使打包的時候使用生成的文件進行打包
  • 在打包的時候動態更改支持的 Plugin 類型('com.android.application'或是'com.android.library')

3.2.2.2 修改模塊 build.gradle 的配置

將以下配置添加到模塊目錄下的 build.gradle 文件中

modular {  
        // 模塊包名
        packageName = "com.youzan.ebizcore.plugin.demoa"

        app {
            // 單模塊打包開關
                asApp = true
                // 運行的 App 的名稱
                appName = "Module A"
                // 入口 Activity
                launchActivity = "com.youzan.ebizcore.plugin.demoa.ModuleAActivity"

                // 配置只在單模塊打包時需要引入的依賴
                requires {
                    require "com.squareup.picasso:picasso:2.3.2"
                }
            }
        }

3.2.2.3 生成單模塊運行需要的環境

運行 modular 的 createApp Task,就會自動生成需要的類(以 module_a 爲例)

    自動生成的文件目錄結構:
    ./module_a
        --src
        ----main
        ------app # 自動生成 app 目錄
        --------java # 自動生成 Application 類
        --------res # 自動生成資源
        --------AndroidManifest.xml # 自動生成 Manifest 文件

3.2.2.4 執行單模塊打包並安裝的 Task

運行 modular 的 runAsApp Task,模塊就會被單獨達成一個 apk 包,並安裝到你的手機上,如果模塊有上下文依賴(比如登錄)的話可以額外提供依賴,加到模塊的 app 的 requires 中。 這裏的打包執行是在 build 目錄下生成了一個打包腳本,並調用 Gradle 的 API 執行腳本來實現打包安裝的。

3.2.3 模塊 API 管理

模塊 API 層提供的接口和數據結構代碼是可以直接在模塊內部被引用到的,方便開發,但是在暴露給外部的模塊時候的時候是需要打包成 aar 上傳到 Maven 來提供的,Modular-Plugin 分別針對這兩個步驟提供了兩個 Task,方便開發者快速進行開發和發佈。

3.2.3.1 在 build.gralde 中添加相關配置

    modular {
            packageName = "com.youzan.ebizcore.plugin.demoa"
            // 模塊 API 支持相關參數
            api {
                // 是否需要提供 API 支持的開關(會影響到是否可以運行自動生成代碼的 Task)
                hasApi = true
                // 對外提供的 API Service 類名
                apiService = "ModuleAService"
                // API 層的依賴
                requires {
                    require "com.google.code.gson:gson:2.8.2"
                }
            }
        }

3.2.3.2 生成 API 打包需要的文件

運行 modular 的 createApi Task,就會自動生成需要的類(以 module_b 爲例)

    ./module_b
          --src
          ----main
          ------service # 自動生成 service 目錄,用來存放對外接口和數據對象
          --------java # 自動生成 Application 類
          --------AndroidManifest.xml # 自動生成 Manifest 文件,爲了單獨打成 aar 包

3.2.4 模塊發佈

發佈功能內部使用了 'maven-publish' 插件來進行依賴的上傳,開發者只關心上報的配置就好

3.2.4.1 在 build.gralde 中添加發布配置

    modular{
            // 模塊發佈需要的參數
            publish {
                // 是否打開模塊發佈
                active = true
                // 上報地址,支持本地路徑和遠程 Mave 倉庫地址
                repo = "../release"
                groupId = "com.youzan.ebizmobile.demo"
                artifactId = "modular-a"
                // 上報的業務模塊 aar 包的版本號
                moduleVersion = "0.1.4"
                // 上報的 API 層 aar 包的版本號
                apiVersion = "0.1.5"
                // Maven 登錄名和密碼,可以從 local.properties 中取
                userName = ""
                password = ""
            }
        }

3.2.4.2 執行發佈的 Task

運行 modular 的 uploadModule Task,Module-Plugin 會執行打包上傳的任務,執行順序是這樣的: 1. 首先打包並上傳 Module 的 API 模塊(SourceSet 只包含 API 的類) 2. 將 Module API 的代碼從模塊的 SourceSet 中去除,並添加剛纔上報的 API 模塊的 Maven 依賴到 Module 的 dependencies 中

3.3 Modular-support

3.3.1 基礎組件抽象

以圖片組件爲例,一般業務模塊中使用到的圖片相關的功能有:圖片加載、圖片選擇等,可以把這些功能抽象成接口

    interface IImageLoadSupport {
        fun <IMAGE : ImageView> loadImage(imageView: IMAGE?, imgUrl: String)
        fun <IMAGE : ImageView> loadImage(imageView: IMAGE?, @DrawableRes drawableId: Int)
        fun <IMAGE : ImageView> loadImage(imageView: IMAGE?, imgUrl: String, callback: ImageLoadCallback<IMAGE>)
        fun imagePicker(activity: Activity?, selectedImgUris: List<Uri>)
        fun onImagePickerResult(requestCode: Int, resultCode: Int, intent: Intent?): List<String>?
    }

3.3.2 基礎組件的實現

基礎組件的實現可以在 App 中進行註冊,如果需要單模塊組件中使用 Support 相關功能,可以提供一套默認實現,在但模塊運行時引入,在全局有一個 Support 註冊中心,以 Map 的形式維護運行中的 Support 對象:

    fun <SUPPORT : Any, SUPPORTIMPL : SUPPORT> registerProvider(supportCls: Class<SUPPORT>, provider: SupportProvider<SUPPORTIMPL>) {
        synchronized(Lock) {
            supportsProviderMap[supportCls] = provider
            if (supportsMap.containsKey(supportCls)) {
                supportsMap.remove(supportCls)
            }
        }
    }

四、規劃

開發到現在,這邊的三個組件已經能夠基本完成我們對於組件化核心需求,但是,也是有一些方向可以進一步優化整套方案的使用:

  • Modular-Support 組件引入依賴注入的方式實現 API 的調用,使用方可以不再需要關心實例對象的獲取
  • Modular-Support 組件可以提供給 Weex、RN、H5、Flutter 業務一些原生的功能
  • Modular-Plugin 能夠進一步壓縮打包時間,並且讓開發中的依賴配置更加靈活
  • Modular-Plugin 繼續優化管理和依賴打包的功能,提高效率

五、總結

組件化的道路千萬條,項目的架構也是在不斷得調整優化,來達到提升團隊開發效率,保證項目穩定性的目的。以上的這些想法和在實際項目中的一些方案,希望能給正在進行模塊化探索的同學提供一些靈感。

原文地址:https://tech.youzan.com/you-zan-yi-dong-androidzu-jian-hua-fang-an/

本文在開源項目:https://github.com/xieyuliang/Android-P7-share/blob/master/(點擊此處藍色字體可以查看)
中已收錄,裏面包含不同方向的自學編程路線、面試題集合/面經、及系列技術文章等,資源持續更新中...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章