為什么要組件化?
醫動力App作為公司的核心產品已經有多年歷史了,隨著版本的不斷迭代,功能越來越多,代碼量越來越大,不可避免的會產生一下問題:
- 業務越來越復雜,維護成本高;
- 業務耦合度高,代碼越來越臃腫,團隊內部多人協作開發困難;
- 編譯時間長,每修改一處代碼后都要重新編譯打包測試,導致非常耗時;
- 開發測試困難,每次修改都必須打包整個項目運行.
因此,為了提高項目的可維護性和開發效率,組件化成為了必然.
組件化的目標
首先看看老版本的醫動力的項目工程結構
項目分為Doctor和Patient,它們之間公共的代碼放在common中,common中會依賴一些第三方的庫.隨著項目的迭代,Doctor和Patient里面的代碼會越來越耦合,因為單個module中僅僅是以包名作為功能的劃分,而包之間是可以隨意調用的.
AndroidStudio/IDEA是多模塊管理的,組件化的思路就是把不同的業務模塊拆分到各個子Module中,同時這些業務Module之間不會存在直接的調用,這樣當我們移除項目中的某個業務Module的時候不會影響整體的項目運行,如下圖所示:
可以看到有「ModuleIM」,「ModuleFollow」,「ModuleDiary」三個業務模塊,它們都會引用Common模塊而獲取一些基礎庫的支持,同時這些業務模塊是可以單獨運行的.后續會將更多的模塊獨立出來,完成徹底的組件化.
如何組件化?
Android的組件化的技術點主要在兩個方面:
- 配置組件獨立運行的能力;
- 組件之間的通信
第一點是通過在module的gradle.build中切換
apply plugin: 'com.android.application'
apply plugin: 'com.android.library',
復制代碼
并設置對應模式下加載的AndroidManifest.xml和src的路徑.
第二點由于組件之間是沒有直接依賴的關系的,要想讓它們通信就必須把它們注冊在一個公共的地方,這里可以通過路由,也可以通過注冊接口.
目前組件化的方案在網上有很多,由于組件化并不涉及Android系統級別的操作,因此是比較成熟穩定的.我們知道,組件化是需要花費大量時間和精力的,很難做到把項目徹底的拆分成組件,那有沒有一種方式可以讓我們不改動原有項目(改動很小)的情況下,將新功能進行組件化開發?
因此我們選擇了使用CC來進行組件化的改造
Component Caller(CC)介紹
業界首個支持漸進式組件化改造的Android組件化開源框架
引用自CC官方的兩張圖能讓我們很快的明白什么叫做漸進式組件化?
在漸進式組件化的方案中,可以先不用解耦,只需要讓單獨運行的組件能夠調用到主App中的功能即可。思路是這樣的:
- 新業務以組件形式開發
- 新組件需要調用的主App中的業務,在對應的模塊中創建一個組件類,對外暴露對應的服務,供其它組件調用,并不需要現在就將這個模塊解耦
- 新組件通過跨App的方式調用主App中的組件
- 主App也可以通過跨App的方式調用到單獨運行的組件App中的組件
- 在同一個module中可以創建多個組件類,將來解耦時將對應的組件類移動到解耦后的module中即可
關于CC的技術實現細節可以查看其github主頁的wiki系列文章 github.com/luckybilly/…
組件化實踐
CC的集成
1.在項目的根gradle.build中加入:
buildscript {dependencies {... ...classpath 'com.billy.android:autoregister:1.4.1'}
}復制代碼
這個autoregister插件是用來在編譯期間動態掃描并修改class文件,實現組件的自動注冊,具體配置后面會提到
2.在Doctor和Patient這兩個主App的gradle.build中加入
ext.mainApp = true //設置為true,表示此module為主app module,一直以application方式編譯
apply from: '../cc-setting.gradle'
復制代碼
cc-setting.gradle中主要進行了以下幾點操作:
- 讀取local.properties文件中的配置來區分組件是否以獨立app的方式編譯;
- 添加com.billy.android:cc:1.1.0依賴;
- 自動注冊組件的配置
3.實現IComponent接口創建組件類
我們統一在Doctor和Patient中的exports包內創建App對外提供的組件,不同業務的組件放在創建在對于的類中管理
IComponent的實現也很簡單,提供一個模塊名稱name和對不同action的處理
主App的配置就這么多,接下來要新建一個組件Module
4.業務module的配置
這里需要設置module獨立運行時的applicationId,同時指定resourcePrefix資源前綴(防止不同模塊之間資源文件的沖突)
因為設置了module的獨立運行,就需要準備一份module在獨立運行模式下的AndroidManifest文件,路徑在src/main/debug下
5.業務module提供IComponent接口
和主App一樣,模塊要提供服務給其他模塊調用,需要提供實現IComponent的子類,因為業務module提供的服務會比較少且單一,我們將它放在包名下的ExportComponent下
7.主App設置需要依賴的module
dependencies {api fileTree(include: ['*.jar'], dir: 'libs')addComponent 'module_follow'addComponent 'module_diary'addComponent 'module_im'
}
復制代碼
通過addComponent方式添加依賴,其內部會根據業務module的運行模式決定是否依賴
6.設置業務module的運行模式
業務module的運行模式包括開發模式和集成模式
- 開發模式:會以App的方式運行
- 集成模式:打包在主App中
打開local.properties文件
module_follow=true
module_diary=true
module_im=false
復制代碼
設置模塊名稱等于true或者false(沒有設置則為false)
- 當等于true則該模塊會以App運行,這時候打包主App的時候是不會把該模塊打包進去
- 當等于false時則不能獨立運行,打包主App的時候會一起打包進去
運行調試
現在你可以將組件單獨運行起來了,但是由于業務組件是不包含登錄功能的,因此它是沒有用戶登錄狀態的,所以我們需要通過CC的組件調用去主App中獲取
補充一點,如果想要跨App調用首先需要打開CC的設置
CC.enableRemoteCC(true);
復制代碼
組件調用
CCResult result = CC.obtainBuilder("common").setActionName("httpInfo").addParam("type", type).build().call();String baseUrl = result.getDataItem("baseUrl");String token = result.getDataItem("token");
復制代碼
調用的方式很簡單,只需要指定模塊名稱和對應的Action,同時可以傳遞參數,以上代碼調用的是common模塊的action為httpInfo的組件.
同時支持同步和異步的調用,前一個例子是同步調用,異步調用需要傳入一個回調方法.
String callId = CC.obtainBuilder("Common").setActionName("httpInfo").addParam("type",type).build().callAsync(new IComponentCallback(){...});
復制代碼
被調用組件在處理完請求后,需要作出響應,看以下代碼:
public class HttpComponent implements IComponent {@Overridepublic String getName() {return "http";}@Overridepublic boolean onCall(CC cc) {String action = cc.getActionName();if(action.equals("action1")){CC.sendCCResult(cc.getCallId(),CCResult.success());return false;}else if(action.equals("action2")){String ccId = cc.getCallId();return true;}}
}復制代碼
注意onCall()方法的返回值,action1中返回的是false,而action2返回的是true,作用是:
- false表示立即回調結果,這里需要調用 CC.sendCCResult(cc.getCallId(),CCResult.success() 告訴調用發返回成功
- true則表示延遲回調結果,這時你可以拿著ccCallId,等到事情處理完后才調用CC.sendCCResult(cc.getCallId(),CCResult.success()
這有什么作用呢? 比如你在調用一個登錄組件去到登錄界面,只有登錄成功了才返回結果,這就需要延遲回調.
網絡請求組件化
項目中用到的網絡請求框架是okhttp+retrofit,我們希望不同module中使用不同的retrofit的Service實例,比如在Module_follow中我們會創建FollowService.java來處理當前模塊的網絡請求
在集成開發模式下我們可以通過組件調用去獲取主App中的Retrofit對象,我們只需要在主App中定一個name=http,action=getRetrofit的IComponent;
但在開發模式下,跨進程調用組件是傳輸不了Retrofit對象的,因為Android的跨進程只能傳輸Parcelable對象,這里我們可以在本module中提供一個相同名稱的IComponent,在里面去獲取主App的baseUrl和token,并創建新的Retrofit對象,這樣就可以透明的處理獲取Retrofit對象了.
為什么以上方式可行呢?因為CC在進行組件化調用的時候,會檢查當前模塊是否存在要調用的模塊,如果存在則會調用本地的,不存在才會去跨進程調用.最后我們可以把這些模擬操作抽到一個lib_mock的module里面復用.
組件化小結
組件化后帶來的一些變化:
- 編譯時間明顯縮短
- 開發人員之間可以通過模塊分工
- 可以在模塊中嘗試新的技術而不擔心影響全局(Kotlin)
CC已知局限:
開發模式不能在Android8.0及以上環境運行,開發的時候可以使用虛擬機或者低版本的手機,集成模式不影響.
引用
CC:基于總線的android組件化開發框架
Android徹底組件化方案實踐