近期公司有組件化的打算,因此對市面上的方案進行了調研,目前已經整理出一套作爲項目組件化的方案,這裏分享一波,當然組件化是沒法一步到位的,中間肯定少不了踩坑優化,所以本篇也會持續更新。
那麼我們先說說組件化是幹嘛的吧,組件化就是將單模塊的項目拆成多個,並且每個模塊可以單獨運行。
WTF!!!這麼簡單?
對概念就是這麼簡單,但當我們去做的時候就會發現幾個問題
- 模塊如何單獨運行
- 拆成獨立模塊後初始化問題(組合運行和獨立運行的時候怎麼初始化)
- 跨模塊方法調用(如何啓動Activity、跨模塊獲取數據)
- 模塊獨立運行時跨模塊方法調用
那麼要想組件化就需要解決上面這幾個問題,所以接下來就是圍繞這幾個問題展開討論,不過在這之前我們先看看整體架構有個大概的認識。
組件化架構圖
從組件的劃分上分爲四層,從上往下依次爲
-
App殼工程:負責管理各個業務組件和打包APK,沒有具體的業務功能。
-
業務組件層:根據不同的業務構成獨立的業務組件。
-
功能組件層:對上層提供基礎功能服務不包含業務,如地圖、拍照、日誌等。
-
組件基礎設施:Base類、第三方Sdk、View等一些通用代碼。
這裏單獨說下業務組件和功能組件,一個典型的業務組件工程結構是這個樣子:
以上圖爲例,它包含三個模塊(兩個Library和一個Application):
- jd :組件代碼,它包含了這個組件所有業務代碼並實現了jd-api的接口。
- jd_api:組件的接口模塊,專門用於與其他組件通信,只包含 Model、Interface 和 Event,不存在任何業務和邏輯代碼。
- jd_app 模塊:用於獨立運行 app,它直接依賴組件模塊,只要添加一些簡單的配置,即可實現組件獨立運行。
你可能會問爲什麼要有個jd_api模塊,其實和接口隔離是一個意思,jd_api模塊存放着jd模塊需要對外暴露的接口,jd模塊去實現這些接口,當別的模塊想要調用jd模塊方法的時候拿到的是jd_api模塊的接口對象,從而隔離jd模塊,只不過這些接口是裝在一個獨立的library中,之所以這樣也是因爲業務模塊粒度太大,包含的代碼量較多,如果將接口放在業務模塊內,既不利於隔離不同實現,還會因爲獲取接口實現類增加很多冗餘的判斷代碼,所以將接口單獨作爲一個library模塊,具體實現類的話根據具體業務場景依賴對應的業務模塊。
以jd模塊爲例,他需要依賴jd_api並實現它的接口
dependencies {
...
implementation project(':component-jd:jd_api')
...
}
而獨立運行的jd_app模塊則需要依賴接口模塊jd_api和業務具體實現模塊jd
dependencies {
...
runtimeOnly project(':component-jd:jd')//runtimeOnly可以防止我們在寫代碼的時候直接引用到jd模塊的類
implementation project(':component-jd:jd_api')
...
}
如果哪天對於jd的業務有新的實現,我們只需要修改runtimeOnly project(':component-jd:jd')
依賴即可,至於怎麼拿到接口實現類是通過Arouter這個框架去獲取的,後面會說。
對於功能模塊來說,同樣也需要用接口隔離,但與業務模塊不同的是功能模塊本身相對獨立沒有業務邏輯,所以不需要單獨爲接口創建一個library,直接把對外暴露的接口定義在功能模塊內即可,外部只需通過工廠拿到具體實現類進行操作。
以支付功能模塊爲例:
在支付模塊內有一個接口IPay進行隔離,RandomPay爲接口具體實現類,業務模塊要想調用支付模塊的方法只需通過PayFactory拿到IPay實現類操作即可。
模塊如何單獨運行
模塊要想單獨運行只需要新建一個Application殼工程用來作爲獨立運行的入口,模塊本身永遠是library,然後殼工程依賴模塊即可,那麼一個模塊的目錄將變成如下這樣:
projectRoot
+--app
+--component_module1(文件夾)
| +--module1(業務模塊library)
| +--module1_api(業務組件的接口模塊,專門用於與其他組件通信library)
| +--module1_app (獨立運行的殼工程Application)
app模塊是全量編譯的application模塊入口,module1是業務library模塊,module1_api是業務組件的接口library模塊,module1_app是用來獨立啓動 module1的application模塊。
對於獨立運行的module1_app模塊只需依賴業務接口模塊和業務模塊
dependencies {
...
runtimeOnly project(':module1')
implementation project(':module1_api')
...
}
對於全量編譯的app模塊則根據所需業務依賴對應的業務接口模塊和業務模塊
dependencies {
...
runtimeOnly project(':module1')
implementation project(':module1_api')
runtimeOnly project(':module2')
implementation project(':module2_api')
...
}
由於有專門用於單獨啓動的module1_app模塊的存在,業務的 library模塊只需要按自己是library模塊這一種情況開發即可,而爲了讓業務模塊單獨啓動所需要的配置、初始化工作都可以放到module1_app模塊裏,並且不用擔心這些代碼被打包到最終Release的App中。
拆成獨立模塊後初始化問題
初始化的邏輯我們可以細分爲兩類
- 通用的初始化邏輯
- 每個模塊個性化的初始化邏輯
對於通用的初始化邏輯可以寫在Base模塊的Application中
public class BaseApplication extends Application {
private static Application sApplication;
@Override
public void onCreate() {
super.onCreate();
sApplication = this;
initARouter(this);
}
public void initARouter(Application application) {
if (BuildConfig.DEBUG) { // 這兩行必須寫在init之前,否則這些配置在init過程中將無效
ARouter.openLog(); // 打印日誌
ARouter.openDebug(); // 開啓調試模式(如果在InstantRun模式下運行,必須開啓調試模式!線上版本需要關閉,否則有安全風險)
ARouter.printStackTrace(); // 打印日誌的時候打印線程堆棧
}
ARouter.init(application); // 儘可能早,推薦在Application中初始化
}
public static Application getApplication() {
return sApplication;
}
}
無論是組合運行還是獨立運行的殼app的Application都繼承這個BaseApplication完成通用邏輯的初始化。
對於個性化的初始化邏輯則放在模塊內部,在獨立運行的時候沒有問題可以讓module1_app的Application繼承我們業務模塊提供的Application完成初始化,但組合運行的時候由於系統只會創建一個Application就是app的,又因爲我們不允許app模塊直接調用業務模塊的方法,需要通過module_api去調用,而業務模塊又沒法在app的Application創建前將初始化服務註冊,導致app的Application#onCreate()方法中獲取不到業務模塊的初始化服務實現類無法初始化,其實可以通過APT在編譯期獲取到需要初始化的類然後在BaseApplication裏面加入初始化這些類的邏輯,但我們這裏選用了一個騷方法解決這個問題,使用contentProvider來初始化。
每個業務模塊自己聲明一個ContentProvider用來初始化當前模塊自己個性化的東西,如果對ContentProvider初始化順序還有要求可以通過initOrder屬性來控制(值越大,越先初始化),詳情請見Android 多個 ContentProvider 初始化順序。
public class JDInitProvider extends ContentProvider {
@Override
public boolean onCreate() {
Log.i("zhuliyuan","JD初始化"+getContext());
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}
有一點需要注意的是ContentProvider的authorities屬性不能重複,爲了模塊組合運行和獨立運行都ok,所以我們用包名作爲前綴避免重複。
<provider
android:name=".JDInitProvider"
android:authorities="${applicationId}.JDInitProvider"
android:exported="false"
android:multiprocess="true"
android:initOrder="200"/>
跨模塊方法調用
跨模塊方法調用可以分爲兩類
- startActivity啓動頁面
- 模塊間方法調用
先說startActivity,由於我們項目中已經集成了Arouter所以我就直接把它作爲了啓動頁面的路由,並且Arouter本身也支持組件化,對於Arouter Api可以查看官方文檔這裏不贅述,唯一需要規範下的是對於頁面的跳轉我們需要進行一道封裝,原因是因爲通過url方式的路由在ide中沒法提示,那麼當我們要啓動其他人維護的頁面的時候並不能在ide上提示出對應的參數類型和數量導致溝通成本增大,並且容易產生bug。
以jd_app模塊啓動jd模塊Activity爲例:
首先我們在jd_api模塊中定義出對外暴露的路由方法
public class JDRouter {
public interface Path {
String JD_ACTIVITY = "/jd/activity";
}
public interface Params {
}
public static void toJDActivity() {
ARouter.getInstance().build(Path.JD_ACTIVITY).navigation();
}
}
jd模塊的JDActivity添加路由標記
@Route(path = JDRouter.Path.JD_ACTIVITY)
public class JDActivity extends BaseActivity {
@Autowired(name = ResidentRouter.Path.SERVICE_PAY_RESULT)
PayResultService service;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.jd_activity_main);
}
}
jd_app模塊則通過jd_api的方法啓動JDActivity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void jump(View view) {
JDRouter.toJDActivity();
}
}
經過JDRouter封裝一層後ide則可以給我們提示出對應的方法和參數,能有效的避免因爲溝通問題產生的bug。
接下來在說說模塊間方法調用,具體點可細分爲上層模塊調用下層模塊和同層模塊間調用,但不管是哪種調用都是需要用接口隔離的,調用者需要拿到接口的實現類去執行對應邏輯,而獲取接口實現類這個過程也是通過Arouter實現的。
以Resident業務模塊爲例:
resident_api模塊需要聲明對外暴露的接口和接口的路徑
public interface PayResultService extends IProvider {//對外暴露接口
int getPayResult();
}
public class ResidentRouter {
public interface Path {
String SERVICE_PAY_RESULT = "/pay/result";//接口路由路徑
}
public interface Params {
}
}
resident模塊實現該接口
@Route(path = ResidentRouter.Path.SERVICE_PAY_RESULT)
public class PayResultServiceImpl implements PayResultService {
@Override
public int getPayResult() {
return 100;
}
@Override
public void init(Context context) {
}
}
resident_app模塊依賴對外暴露的resident_api和具體實現類resident
dependencies {
...
runtimeOnly project(':component_resident:resident')
implementation project(':component_resident:resident_api')
...
}
通過ARouter提供的注入的方式拿到接口實現類,完成跨模塊方法調用
public class MainActivity extends AppCompatActivity {
@Autowired(name = ResidentRouter.Path.SERVICE_PAY_RESULT)
PayResultService service;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ARouter.getInstance().inject(this);
tv.setText("需要支付金額:" + String.valueOf(service.getPayResult()));
}
}
模塊獨立運行時跨模塊方法調用
同級模塊有依賴的情況下,組合運行沒問題,但是單獨運行的時候由於沒有對應模塊提供接口實現,那麼我們通過arouter沒法拿到具體的實現,這個時候就需要mock數據了,而mock相關的操作是爲了我們獨立運行,所以寫在獨立運行的殼工程中。以jd模塊爲例,假設jd模塊的運行需要依賴resident模塊,那麼jd_app就需要實現resident_api中jd需要的方法,以便jd模塊獨立運行的時候能夠獲取到resident的數據。
Jd_app依賴關係如下
dependencies {
...
runtimeOnly project(':component-jd:jd')
implementation project(':component-jd:jd_api')
implementation project(':component_resident:resident_api')
...
}
mock jd模塊獨立運行所需的resident_api數據
@Route(path = ResidentRouter.Path.SERVICE_PAY_RESULT)
public class MockPayResultService implements PayResultService {
@Override
public int getPayResult() {
return 100;
}
@Override
public void init(Context context) {
}
}
Tips
- 組件化後有資源衝突的可能性所以命名還得規範,比如加前綴
// Login 組件的 build.gradle
android {
resourcePrefix "login_"
// 其他配置 ...
}
如果組件配置了 resourcePrefix ,其 xml 中定義的資源沒有以 resourcePrefix 的值作爲前綴的話,在對應的 xml 中定義的資源會報紅。resourcePrefix 的值就是指定的組件中 xml 資源的前綴,不過沒法約束圖片命名需要自己注意。
- 代碼隔離Gradle 3.0 提供了新的依賴方式 runtimeOnly ,通過 runtimeOnly 方式依賴時,依賴項僅在運行時對模塊及其消費者可用,編譯期間依賴項的代碼對其消費者時完全隔離的,避免開發中直接引用到組件中類的問題
// 主項目的 build.gradle
dependencies {
// 其他依賴 ...
runtimeOnly project(':component-jd:jd')
implementation project(':component-jd:jd_api')
}
最後附上組件化Demo地址