知乎 Android 客戶端最先使用的是最多見的單工程 MVC 架構,全部業務邏輯都放在了主工程 Module 裏,網絡層和一些公共代碼分別被抽成了一個 Module。如今看來,當時的業務線、產品功能及研發團隊都比不上如今的體量和豐富度,遇到的問題隨時組內溝通就能夠解決。因此在知乎穩步發展的前幾年,並無遇到什麼大的問題。java
後來公司發展速度加快,拆分了多個獨立的事業部,每一個事業部有獨立的 Android 開發團隊,每一個團隊都有獨立開發、測試和部署的需求;隨着業務規模的擴大,早期的代碼耦合致使的問題也逐漸顯現出來;開發人員也愈來愈多,單工程的架構在人員協做方面也顯得愈來愈力不從心。同時考慮到對將來可能出現的多應用的支持,咱們開始了工程的組件化重構。今天咱們會在這篇文章中分享咱們組件化過程當中的一些實踐。android
咱們使用的是多工程多倉庫的方案,即每一個組件都有本身的獨立倉庫,都可獨立於主工程單獨運行;主工程經過 aar 依賴各個組件,自身則逐漸被拆成一個殼的狀態,不包含業務邏輯代碼。通過一年多的不斷迭代,如今是這個樣子:git
它包含 4 個層次: 主工程:除了一些全局配置和主 Activity 以外,不包含任何業務代碼。 業務組件:最上層的業務,每一個組件表示一條完整的業務線,彼此之間互相獨立。 基礎組件:支撐上層業務組件運行的基礎業務服務。 基礎 SDK:徹底業務無關的基礎代碼。 各層次職責清晰獨立,能夠很方便的進行拆解和組合;因爲都有本身的版本,業務線能夠獨立發版,隨時升級、回滾。github
組件化的第一步就是對要拆出去的組件進行解耦,常看法耦方式有如下幾種:api
(1) 公用代碼處理: 基礎業務邏輯分別拆成基礎組件 自身邏輯完整、用於完成某一特定功能、不含業務邏輯的一組代碼,獨立成 SDK 代碼量很小不足以拆分紅單獨拆分的代碼和資源,咱們統一放在一個專門創建的 common 組件中,而且嚴格限制 common 組件的增加。隨着組件化的逐漸進行,common 應該逐漸變小而不是增大。 碰巧被共同使用的一些代碼和資源片斷,一般它們被複用只是由於被開發人員搜索到而直接使用了,不少時候某個資源已經被 A 業務聲明瞭前綴,可是因爲沒有隔離,仍然會不可避免的被他人在 B 業務中強行復用,這時候若是 A 業務方要進行一些修改,B 業務就會受到影響 —— 這種狀況咱們容許直接複製bash
(2) 初始化:有些組件有在應用啓動時初始化服務的需求,並且不少服務仍是有依賴關係的,最初咱們爲每一個組件都添加了一個 init() 方法,可是並不能解決依賴順序問題,須要每一個組件都在 app 工程中按順序添加初始化代碼才能正常運行,這使得不熟悉整套組件業務的人很難創建起一個能夠獨立運行的組件 app。所以咱們開發了一套多線程初始化框架,每一個組件只要新建若干個啓動 Task 類,並在 Task 中聲明依賴關係便可:網絡
這樣就解決了組件在主工程中堆積初始化代碼的問題,在簡化了代碼的同時還有加快啓動速度的功效。多線程
(3) 路由:界面間使用 Url 進行跳轉,不但實現瞭解耦,也統一了各端的頁面打開方式。咱們實現了一套靈活小巧的路由框架 ZRouter,它支持多組件、路由攔截、AB Test 、參數正則匹配、降級策略、任意參數傳遞以及自定義跳轉等功能,能夠自定義路由的各個階段,徹底知足了咱們的業務需求。架構
(4) 接口:除了頁面間的跳轉,不一樣業務之間不可避免的會有一些調用,爲了不組件的直接通訊,一般都是使用接口依賴的方式。咱們實現了一個 Interface Provider 來支持接口通訊,它能夠經過運行時在動態註冊一個接口,同時也實現了對於 ServiceLoader 的支持。只要一方組件將通訊接口暴露出來,使用方就能夠直接使用接口進行調用。app
動態註冊接口
Provider.register(AbcInterface.class,new AbcInterfaceImpl())
複製代碼
獲取實例並調用
Provider.get(AbcInterface.class).doSomething()
複製代碼
(5) EventBus:這個自沒必要說,雖說濫用是一個問題,可是有些場景下,使用事件仍是最爲方便簡單的方式
(6) 組件 API 模塊:上面提到的接口和事件以及一些跨組件使用的 Model 放到哪裏好呢?若是直接將這些類下沉到一個公共組件中,因爲業務的頻繁更新,這個公共組件可能會更新得十分頻繁,開發也十分的不方便,因此使用公共組件是行不通的,因而咱們採起了另外一種方式——組件 API :爲每一個有對外暴露需求的組件添加一個 API 模塊,API 模塊中只包含對外暴露的 Model 和組件通訊用的 Interface 與 Event。有須要引用這些類的組件只要依賴 API 便可。
一個典型的組件工程結構是這個樣子:
以上圖爲例,它包含三個模塊:
有了解耦的方法,剩下的就是採起行動拆分組件了,拆組件是一個很頭疼的問題,它很是考慮一我的的細心與耐心,因爲沒法準確知道有哪些代碼要被拆走,也不能直觀的知曉依賴關係,移動變得很是的困難且容易出錯,一旦不能一次性拆分紅功,處處都是編譯錯誤,便只能靠人肉一點一點的挪。 工欲善其事,必先利其器。爲了解決這個問題,咱們開發了一個輔助工具 RefactorMan: 它能夠遞歸的解析出工程中全部源碼的引用和被引用狀況,同時會根據預設規則自動分析出全部不合理的依賴,在開發人員根據提示解決了不合理依賴以後,便可將組件一鍵移出,大大減小了拆組件的工做量。咱們在組件化初期曾經走過一些彎路,最初拆出的八個組件工程的的部分源碼經歷了幾回的反覆移動才得出最優解,而有了 RefactorMan,咱們能夠面對反覆的拆分和組合組件有恃無恐 Bonus :因爲能夠分析和移動資源,因此額外得到了清理無用資源的功能
單獨運行組件 app 並不能完整的覆蓋全部的 case,尤爲是在給 QA 測試的時候,仍是須要編譯完整的主工程包的,因此咱們須要一個直接編譯完整包的方案: 最初咱們的實現方式只針對組件,比較簡單: 首先在 setting.gradle 中動態引入組件 module:
def allComponents = ["base", "account" ... "template" ...]
allComponents.forEach({ name ->
if (shouldUseSource(name)) {
// 動態引入外部模塊
include ":${name}"
project(":${name}").projectDir = getComponentDir(name);
}
})
複製代碼
而後在 app/build.gradle 中切換依賴,須要將全部被間接依賴的組件所有 exclude 以防止同時依賴了一個組件的 module 和 aar:
allComponents.forEach({ name ->
if (shouldUseSource(name)) {
implementation(project(":${name}")) { exclude group: COMPONENT_GROUP }
} else {
implementation("${COMPONENT_GROUP}:${name}:${versions[name]}") { exclude group: COMPONENT_GROUP }
}
})
複製代碼
因爲全部組件的 group 都是同樣的,因此這樣作並無什麼問題,可是後來一些基礎 SDK 也出現了這種需求,這時候就須要一種通用的源碼依賴方案,所以作了一下修改,直接使用 gradle 提供的依賴替換功能,只須要修改 setting.gradle 便可:
// ... 忽略讀取配置代碼 ...
configs.forEach { artifact, prj ->
include ":${prj.name}"
project(":${prj.name}").projectDir = new File(prj.dir)
}
gradle.allprojects { project ->
if (project == project.rootProject) {
return
}
project.configurations.all {
resolutionStrategy.dependencySubstitution {
configs.forEach { artifact, prj ->
// 在這裏進行替換
substitute module(artifact) with project(":${prj.name}")
}
}
}
}
複製代碼
而 build.gradle 的依賴寫法與普通的工程徹底同樣。 普通狀態下的的主工程:
源碼引用 template 組件後的主工程:
這樣咱們就能夠像以前在單工程中同樣寫代碼了。 得益於源碼引用,咱們直接在提交組件代碼的時候,CI 會自動聯合主工程編譯出完整包,QA 會根據完整包進行測試,在測試經過後便可自動發佈到公司的倉庫,並經過內部的集成平臺集成到主工程。
小 tip :工程 .idea/vcs.xml 中定義了當前工程關聯的 Git 倉庫,能夠在聯合編譯的同時經過修改 vcs.xml 來把組件目錄也關聯到主工程 Git 配置中,在開發過程當中就可使用 Android Studio 的內置 Git 功能了。
咱們當前的組件,絕大部分是一個組件一個倉庫的,對於通常的組件來講,並無什麼問題,可是對於有的業務線,自己規模比較大,包含了若干個子業務,好比知乎大學,電子書、live 和私家課等子業務,這些子業務自己功能獨立,可是共享整個業務線的基礎代碼,同時大業務線也有會一些彙總全部子業務的頁面,它們的關係是這個樣子:
這幾個業務若是都要拆分出去獨立成組件,而後抽離公共部分紅爲也成爲一個業務線基礎組件,這時候會面臨一個很大的問題:因爲幾條業務線都屬於同一個主業務線,作活動或者上新 Feature 的時候,這幾個組件常常會發生聯動,須要先更新 base 再更新其餘業務線,提交 mr 也要同時提多個倉庫,出現頻繁的連鎖更新;而若是不拆的話,業務線代碼自己就已經很龐大,即便是單獨編譯組件 app 也會很慢,而且隨着時間的推移,各個業務線的代碼邊界會像組件化以前的主工程同樣逐漸劣化,耦合會愈來愈嚴重。
因此如今需求變成了這個樣子: 對外保持只有一個組件:有聯動需求的時候,組件仍然只發布一次更新 各個子業務仍舊保持互相獨立和隔離,能夠獨立運行 咱們曾經試圖使用 sourceSets 的方式將不一樣的業務代碼放到不一樣的文件夾,可是 sourceSets 的問題在於,它並不能限制各個 sourceSet 之間互相引用,base 模塊甚至能夠直接引用最上層的代碼,雖然能夠在編譯期進行檢查,可是總有一些後知後覺的意味,而且使用 sourceSets 想讓各個模塊單獨跑起來配置也比較麻煩。而 Android Studio 的 module 自然具備隔離的優點。因此咱們的解決方案是在組件工程中使用多 Module 結構:
各個子業務線分別拆成同一個工程中不一樣的 Module:它們共同依賴 base ,同時各個業務線互相不依賴,這些子業務又在一個主 Module 中聚集起來,正如上面圖片所示那樣
對於外界來講只有一個 main 組件,若是直接經過 ./gradlew :main:uploadArchives 來發布,那麼就只能把 main Module 的代碼發佈上去,其餘 Module 的代碼是沒法發佈的,因此咱們須要在發佈的時候將全部的代碼合併到 main 中去。這時候只能使用添加 sourceSet 的方式,而一旦使用了 sourceSet,代碼就再也不隔離了。因此咱們使用了一個動態的策略:編譯時使用 sourceSet 依賴,其餘時候使用 module 依賴,這樣能夠同時擁有二者的優點。
也就是說:表面看起來,這是一個普通的多模塊的工程,可是實際上,他們的關係是動態的:寫代碼時是七個葫蘆娃,編譯時是葫蘆小金剛:
如何作到呢,能夠簡單的判斷當前啓動的 Task,通常咱們只在 assemble、install、upload 的時候使用合體操做,而其餘時候使用普通的 project 依賴,示例代碼以下:
boolean useSource = gradle.startParameter.taskNames.any {
it.contains("assemble") || it.contains("install") || it.contains("upload"))
}
subProject.forEach { subProject ->
if (useSource) {
android.sourceSets.main {
java.srcDirs += file("../${subProject}/src/main/java")
res.srcDirs += file("../${subProject}/src/main/res")
}
} else {
dependencies { implementation project(":$subProject") }
}
}
複製代碼
其餘資源例如 resources、assets、aidl、renderscript、jni、jniLibs、shaders 以及 aar 和 jar 文件,它們都是多文件的,可使用與上面相似的方法添加。
可是 manifest 不一樣,一個 module 中只有一個 AndroidManifest.xml ,因此須要有一個方法將子業務的 manifest 合併。咱們使用了官方提供的 ManifestMerger 實現了 manifest 的合併,這裏再也不展開合併的具體代碼,有興趣的同窗能夠本身去看源碼。
將上面代碼封裝了一個方法 using
,主 module 就能夠這樣引用子 module 了:
dependencies {
using "base"
using "sub1"
using "sub2"
using 'sub3'
using 'sub4'
}
複製代碼
因爲每一個子業務組件都是獨立的,仍然能夠單獨配置獨立編譯獨立運行,因爲每一個業務的代碼量相對整個業務線來講大大減小了,因此獲得了更快的編譯速度。
最近兩年不少公司都開始了 App 的組件化,組件化的基礎思想都是相通的,可是並無一個放之四海而皆準的通用解決方案,各個公司在組件化的過程當中都會根據自身的狀況不斷的調整方案,適合自身發展的,纔是最好的。一些組件化初期看起來不起眼的問題,可能進行到後期纔會慢慢顯現出來,這時候就要及時調整方案。知乎的組件化也是在不斷的變更中逐漸完善的,而且之後確定也會隨着業務和代碼的變更不斷的進行優化,這會是一個持續的過程,後續咱們也會持續分享一些組件化遇到的問題和解決方案。 以上就是咱們在組件化過程當中的一部分實踐,因爲本人的水平有限,若有錯誤和疏漏,歡迎各位同窗指正。
Peter Porker,2016 年加入知乎,現爲知乎 Android 基礎架構團隊負責人,有着豐富的 Android 工程化,組件化經驗,設計並主導了知乎的 Android 組件化拆分工做。