最近羣裏的朋友們在討論組件化方案,我看了大家分享的文章,感覺受益匪淺,現在分享給大家,希望對大家的學習和工作有所幫助。
一、概述
目前有贊移動端的主要工作內容是在“有贊微商城”和“有贊零售”兩條公司主要的業務線,隨着有贊 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 模塊。它就像一個大儲物櫃,每個人都把一些其他人可能用到的東西一股腦兒塞進去。 這麼個塞法,會有兩個問題:
- 冗餘:比如一些工具類,很多時候,當你找不到需要的工具類的時候,你可能會塞一個新的進去
- 維護成本高:所有公用的業務邏輯的實現都在 Common 中,對一些公用業務邏輯的影響面無法掌控
2.2.1 Common 裏面都有什麼?
- 工具類
- 公用的 UI 組件
- 多個業務模塊都公用的業務類
- 基礎組件的封裝類(圖片庫、網絡庫、Webview)
- 封裝的一些基類(BaseActivity,BaseFragment 什麼的)
2.2.2 解決的思路
- 將公用的業務模塊向上抽離到業務模塊中(所謂業務模塊的服務化)
- 將基礎組件抽象到一個獨立的組件中
- 將一些基礎類下沉到不包含業務邏輯的底層核心庫中
2.3 業務模塊服務化
“服務化”這個詞,在服務端的開發中經常被提到,簡單來說,就是根據業務劃分爲多個模塊,模塊之間的交互以互相提供服務的方式來完成。 而客戶端隨着業務模塊的增多,也必然存在業務模塊之間存在業務依賴的情況,而 Android 端依賴的方式有:
- A 模塊直接依賴 B 模塊,直接調用 B 模塊的代碼邏輯
- 將 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 容器庫等等,這也帶來了一些問題:
- Common 太重
- 業務模塊跟基礎組件強耦合,在開發一些跨團隊的組件過程中,如果碰到使用的基礎庫不同的時候,需要比較多的時間來做封裝
- 升級基礎組件或替換依賴的成本比較高,一些 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/(點擊此處藍色字體可以查看)
中已收錄,裏面包含不同方向的自學編程路線、面試題集合/面經、及系列技術文章等,資源持續更新中...