將一個大型的項目拆分成多個Module或者新開的組件化項目,想要的預期是這些module之間是平級的關系。這樣一來就可以使得業務相對集中,每個人都可以專注在一件事上。
同時,代碼的耦合度也會隨之降低,達到高度解耦狀態,因為同級的module不存在依賴關系,在編譯上就是隔離的,這會讓組件間的依賴非常清楚,同時也具有更高的重用性,組件強調復用,模塊強調職責劃分。他們沒有非常嚴格的劃分。
達到可復用要求的模塊,那么這個模塊就是組件。每個組件的可替代性、熱插拔、獨立編譯都將可行。
代碼中心化在Android組件化中的問題體現
貌似Android的組件化是非常簡單且可行的,AS提供的module創建方式加gradle.properies 自定義屬性可讀,或者ext全局可配置的project屬性亦或kotlin dsl 中kotlin的語法糖都為我們提供了application和library的切換。
然后將代碼放在不同的倉庫位置最好是單獨git倉庫級別的管理隔離,就能達到我們想要解決的一系列問題。
然而事情并不是想象的那么簡單...
一些列的問題接踵而至,于我而言影響最深的就是應用設計時使用映射型數據庫,導致集成模式和組件模式中復用出現問題;最終使用注解配合Java特性生成代碼,雖然不完美但是依然解決了此問題。隨即閃現出了一個重要且緊急的問題,代碼中心化的問題。
這個問題是怎么出現的呢?在微信Android模塊化架構重構實踐中是這樣描述的。
然而隨著代碼繼續膨脹,一些問題開始突顯出來。首先出問題的是基礎工程libnetscene和libplugin。基礎工程一直處于不斷膨脹的狀態,同時主工程也在不斷變大。同時基礎工程存在中心化問題,許多業務Storage類被附著在一個核心類上面,久而久之這個類已經沒法看了。此外當初為了平滑切換到gradle避免結構變化太大以及太多module,我們將所有工程都對接到一個module上。缺少了編譯上的隔離,模塊間的代碼邊界出現一些劣化。雖然緊接著開發了工具來限制模塊間的錯誤依賴,但這段時間里的影響已經產生。在上面各種問題之下,許多模塊已經稱不上“獨立”了。所以當我們重新審視代碼架構時,以前良好模塊化的架構設計已經逐漸變了樣。
再看他們分析問題的原因:
翻開基礎工程的代碼,我們看到除了符合設計初衷的存儲、網絡等支持組件外,還有相當多的業務相關代碼。這些代碼是膨脹的來源。但代碼怎么來的,非要放這?一切不合理皆有背后的邏輯。
在之前的架構中,我們大量適用Event事件總線作為模塊間通信的方式,也基本是唯一的方式。使用Event作為通信的媒介,自然要有定義它的地方,好讓模塊之間都能知道Event結構是怎樣的。這時候基礎工程好像就成了存放Event的唯一選擇——Event定義被放在基礎工程中;接著,遇到某個模塊A想使用模塊B的數據結構類,怎么辦?
把類下沉到基礎工程;遇到模塊A想用模塊B的某個接口返回個數據,Event好像不太適合?那就把代碼下沉到基礎工程吧……
就這樣越來越多的代碼很“自然的”被下沉到基礎工程中。
我們再看看主工程,它膨脹的原因不一樣。分析一下基本能確定的是,首先作為主干業務一直還有需求在開發,膨脹在所難免,缺少適當的內部重構但暫時不是問題的核心。另一部分原因,則是因為模塊的生命周期設計好像已經不滿足使用需要。之前的模塊生命周期是從“Account初始化”到“Account已注銷”,所以可以看出在這時機之外肯定還有邏輯。
放在以前這不是個大問題,剛啟動還不等“Account初始化”就要執行的邏輯哪有那么多。而現在不一樣,再簡單的邏輯堆積起來也會變復雜。此時,在模塊生命周期外的邏輯基本上只能放主工程。
此外的問題,模塊邊界破壞、基礎工程中心化,都是代碼持續劣化的幫兇...
看完之后就陷入了沉思,這個問題不就是我們面臨的問題嗎?不僅是在組件化中,在很多形成依賴關系的場景中都有此類問題。
假設有user組建和分享組件,分享組件需要user組件提供數據。
具體是怎么體現的呢,我們來看一組圖:
解決方式為分享組件依賴user組件,能解決問題,假設,有一個組件A,需要引用分享組件,就必須依賴分享組件和user組件,這就一舉打破了組件編譯隔離的遠景,組件化將失去香味兒。
將user組件中的公共數據部分下沉到base組件,分享組件依賴base組件即可實現數據提供,然而當非常多的組件需要互相提供數據時,將出現中心化問題,只需要分享組件的B組件不得不依賴base組件,引入其他數據。也就造成了代碼中心化下沉失去組件化的意義。
/ 怎么解決代碼中心化問題 /
微信面對這個痛心疾首的問題時發出了“君有疾在腠理,不治將恐深” 的感慨,但也出具了非常厲害的操作-.api化。
這個操作非常高級,做法非常騰訊,但是此文檔中只提到了精髓,沒有具體的操作步驟,對我們來講依然存在挑戰。
什么是代碼中心化問題的.api方案
先看一下具體的操作過程是什么樣的。上圖3中,我們使用某種技術將user組件中需要共享數據的部分抽象成接口,利用AS對文件類型的配置將(kotlin)后拽修改為.api ,然后再創建一個同包名的module-api 組件用來讓其他組件依賴,分享組件和其他組件以及自身組件在module模式下均依賴該組件,這樣就能完美的將需要共享的數據單獨出去使用了。
SPI 方式實現
大概就是說我們可以將要共享的數據先抽象到接口中形成標準服務接口,然后在具體的實現中,然后在對應某塊中實現該接口,當服務提供者提供了接口的一種具體實現后,在jar包的META-INF/services目錄下創建一個以“接口全限定名”為命名的文件,內容為實現類的全限定名;然后利用 ServiceLoader 來加載配置文件中指定的實現,此時我們在不同組件之間通過ServiceLoader加載需要的文件了。
利用ARouter
利用ARouter在組件間傳遞數據的方式+gralde自動生成module-api組件,形成中心化問題的.api化。假設我們滿足上述的所有關系,并且構建正確,那我們怎么處理組件間的通信?
Arouter 阿里通信路由
- @Route(path = "/test/activity")
- public class YourActivity extend Activity {
- ...
- }
- 跳轉:
- ARouter.getInstance().build("/test/activity").withLong("key1", 666L).navigation()
- // 聲明接口,其他組件通過接口來調用服務
- public interface HelloService extends IProvider {
- String sayHello(String name);
- }
- // 實現接口
- @Route(path = "/yourservicegroupname/hello", name = "測試服務")
- public class HelloServiceImpl implements HelloService {
- @Override
- public String sayHello(String name) {
- return "hello, " + name;
- }
- @Override
- public void init(Context context) {
- }
- }
- //測試
- public class Test {
- @Autowired
- HelloService helloService;
- @Autowired(name = "/yourservicegroupname/hello")
- HelloService helloService2;
- HelloService helloService3;
- HelloService helloService4;
- public Test() {
- ARouter.getInstance().inject(this);
- }
- public void testService() {
- // 1. (推薦)使用依賴注入的方式發現服務,通過注解標注字段,即可使用,無需主動獲取
- // Autowired注解中標注name之后,將會使用byName的方式注入對應的字段,不設置name屬性,會默認使用byType的方式發現服務(當同一接口有多個實現的時候,必須使用byName的方式發現服務)
- helloService.sayHello("Vergil");
- helloService2.sayHello("Vergil");
- // 2. 使用依賴查找的方式發現服務,主動去發現服務并使用,下面兩種方式分別是byName和byType
- helloService3 = ARouter.getInstance().navigation(HelloService.class);
- helloService4 = (HelloService)ARouter.getInstance().build("/yourservicegroupname/hello").navigation();
- helloService3.sayHello("Vergil");
- helloService4.sayHello("Vergil");
- }
- }
假如user組件的用戶信息需要給支付組件使用,那我們怎么處理?
ARouter可以通過上面的IProvider注入服務的方式通信,或者使用EventBus這種方式。
- data class UserInfo(val uid: Int, val name: String)
- /**
- *@author kpa
- *@date 2021/7/21 2:15 下午
- *@email [email protected]
- *@description 用戶登錄、獲取信息等
- */
- interface IAccountService : IProvider {
- //獲取賬號信息 提供信息*
- fun getUserEntity(): UserInfo?
- }
- //注入服務
- @Route(path = "/user/user-service")
- class UserServiceImpl : IAccountService {
- //...
- }
在支付組件中
- IAccountService accountService = ARouter.getInstance().navigation(IAccountService.class);UserInfo bean = accountService. getUserEntity();
問題就暴露在了我們眼前,支付組件中的IAccountService和UserInfo從哪里來?
這也就是module-api 需要解決的問題,在原理方面:
- 將需要共享的數據和初始化數據的類文件設計為.api文件
- 打開AS-> Prefernces -> File Types找到kotlin(Java)選中在File name patterns 里面添加".api"(注意這個后綴隨意開心的話都可以設置成.kpa)
舉例:
- data class UserInfo(val userName: String, val uid: Int)
- interface UserService {
- fun getUserInfo(): UserInfo
- }
生成包含共享的數據和初始化數據的類文件的module-api組件
這步操作有以下實現方式。
- 自己手動創建一個module-api 組件 顯然這是不可取但是可行的
- 使用腳本語言shell 、python 等掃描指定路徑生成對應module-api
- 利用Android 編譯環境及語言groovy,編寫gradle腳本,優勢在于不用考慮何時編譯,不打破編譯環境,書寫也簡單
/ module-api 腳本 /
找到這些問題出現的原理及怎么去實現之后,從github上找到了優秀的人提供的腳本,完全符合我們的使用預期。
- def includeWithApi(String moduleName) {
- def packageName = "com/xxx/xxx"
- //先正常加載這個模塊
- include(moduleName)
- //找到這個模塊的路徑
- String originDir = project(moduleName).projectDir
- //這個是新的路徑
- String targetDir = "${originDir}-api"
- //原模塊的名字
- String originName = project(moduleName).name
- //新模塊的名字
- def sdkName = "${originName}-api"
- //這個是公共模塊的位置,我預先放了一個 新建的api.gradle 文件進去
- String apiGradle = project(":apilibrary").projectDir
- // 每次編譯刪除之前的文件
- deleteDir(targetDir)
- //復制.api文件到新的路徑
- copy() {
- from originDir
- into targetDir
- exclude '**/build/'
- exclude '**/res/'
- include '**/*.api'
- }
- //直接復制公共模塊的AndroidManifest文件到新的路徑,作為該模塊的文件
- copy() {
- from "${apiGradle}/src/main/AndroidManifest.xml"
- into "${targetDir}/src/main/"
- }
- //復制 gradle文件到新的路徑,作為該模塊的gradle
- copy() {
- from "${apiGradle}/api.gradle"
- into "${targetDir}/"
- }
- //刪除空文件夾
- deleteEmptyDir(*new* File(targetDir))
- //todo 替換成自己的包名
- //為AndroidManifest新建路徑,路徑就是在原來的包下面新建一個api包,作為AndroidManifest里面的包名
- String packagePath = "${targetDir}/src/main/java/" + packageName + "${originName}/api"
- //todo 替換成自己的包名,這里是apilibrary模塊拷貝的AndroidManifest,替換里面的包名
- //修改AndroidManifest文件包路徑
- fileReader("${targetDir}/src/main/AndroidManifest.xml", "commonlibrary", "${originName}.api")
- new File(packagePath).mkdirs()
- //重命名一下gradle
- def build = new* File(targetDir + "/api.gradle")
- if(build.exists()) {
- build.renameTo(new File(targetDir + "/build.gradle"))
- }
- // 重命名.api文件,生成正常的.java文件
- renameApiFiles(targetDir, '.api', '.java')
- //正常加載新的模塊
- include ":$sdkName"
- }
- private void deleteEmptyDir(File dir) {
- if(dir.isDirectory()) {
- File[] fs = dir.listFiles()
- if(fs != null && fs.length > 0) {
- for (int i = 0; i < fs.length; i++) {
- File tmpFile = fs[i]
- if (tmpFile.isDirectory() {
- deleteEmptyDir(tmpFile)
- }
- if (tmpFile.isDirectory() && tmpFile.listFiles().length <= 0){
- tmpFile.delete()
- }
- }
- }
- if (dir.isDirectory() && dir.listFiles().length == 0) {
- dir.delete()
- }
- }
- private void deleteDir(String targetDir) {
- FileTree targetFiles = fileTree(targetDir)
- targetFiles.exclude "*.iml"
- targetFiles.each { File file ->
- file.delete()
- }
- }
- /**
- * rename api files(java, kotlin...)
- **/
- private def renameApiFiles(root_dir, String suffix, String replace) {
- FileTree* files = fileTree(root_dir).include("**/*$suffix")
- files.each {
- File file ->
- file.renameTo(*new* File(file.absolutePath.replace(suffix, replace)))
- }
- }
- //替換AndroidManifest里面的字段*
- def fileReader(path, name, sdkName) {
- def readerString = ""
- def hasReplace = false
- file(path).withReader('UTF-8') { reader ->
- reader.eachLine {
- if (it.find(name)) {
- it = it.replace(name, sdkName)
- hasReplace = true
- }
- readerString <<= it
- readerString << '\n'
- }
- if (hasReplace) {
- file(path).withWriter('UTF-8') {
- within ->
- within.append(readerString)
- }
- }
- return readerString
- }
- }
使用
- includeWithApi ":user"
Democomponent-api地址為:
- https://github.com/kongxiaoan/component-api
原文地址:https://juejin.cn/user/2365804752418232