隨著移動平臺的不斷發(fā)展,軟件慢慢變的越來越復雜,業(yè)務繁多,體積臃腫;為了降低大型軟件復雜性和耦合度,同時也為了適應模塊重用、多團隊并行開發(fā)測試等等需求,Android社區(qū)提出了兩種解決方案:模塊化和插件化。插件化暫且按下不提,本文主要講述模塊化。從基本思路上來講,模塊化的實現(xiàn)大體上來講都是差不多的,本文將著重講述基本思路。此外,在實踐的過程中也有特別的地方:Databinding在模塊化中的坑,Dagger2在模塊化中的應用,頁面統(tǒng)一跳轉(zhuǎn),模塊化通信方式設計,模塊層級架構(gòu)設計等。這些問題將在本文和后面的系列文章中一一介紹。
[TOC]
什么是模塊化
什么是模塊化呢?有一種定義是:模塊化是一種處理復雜系統(tǒng)分解為更好的可管理模塊的方式。由此可見,模塊化思路下構(gòu)成的復雜系統(tǒng)是由各個可管理的子模塊構(gòu)成的,每個子模塊之前相互獨立,并通過某種特定的方式進行通信。
在工業(yè)上面,有模塊化汽車的概念,也有模塊化手機的概念,各個模塊根據(jù)一定的標準進行生產(chǎn),生產(chǎn)之后可以直接進行各個模塊的組裝,某個模塊出現(xiàn)問題之后,可以單獨對這個模塊進行替換。舉個例子,同樣一款汽車,有各中配置不同的版本,比如發(fā)動機不同。這些發(fā)動機都按照一定的標準生產(chǎn),但是發(fā)送的輸出和能耗并不同。重要的是其接口標準一樣。從可替換這一點來講,和軟件開發(fā)中的可插拔是異曲同工的。
Android 開發(fā)中有兩個比較相似的概念:組件化和模塊化,這里需要進行區(qū)分的。
組件化:指的是單一的功能組件,如地圖組件、支付組件、路由組件(Router)等等;
模塊化:獨立的業(yè)務模塊,模塊相對于組件來講粒度更大。
模塊化的好處是顯而易見的。
? 多團隊并行開發(fā)測試;
? 模塊間解耦、重用;
? 可單獨編譯打包某一模塊,提升開發(fā)效率。
模塊Debug和Release處理
對于模塊化項目,每個單獨的 Business Module 都可以單獨編譯成 APK。在開發(fā)階段需要單獨打包編譯,項目發(fā)布的時候又需要它作為項目的一個 Module 來整體編譯打包。簡單的說就是開發(fā)時是 Application,發(fā)布時是 Library。因此需要在 Business Module 的 build.gradle 中加入如下代碼:
if(isBuildModule.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
isBuildModule 在項目根目錄的 gradle.properties 中定義:
isBuildModule=false
同樣 Manifest.xml 也需要有兩套:
sourceSets {
main {
if (isBuildModule.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
}
}
}

debug 模式下的 AndroidManifest.xml :
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dajiazhongyi.dajia.pedumodule">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<application
...
>
<activity
android:name="com.dajiazhongyi.dajia.loginmodule.ui.DaJiaLauncher"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="dajia" />
</intent-filter>
</activity>
<activity
android:name=".ui.MainActivity"
android:screenOrientation="portrait"/>
</application>
</manifest>
realease 模式下的 AndroidManifest.xml :
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dajiazhongyi.dajia.pedumodule">
<application
android:allowBackup="true"
android:supportsRtl="true">
<activity
android:name="com.dajiazhongyi.dajia.pedumodule.ui.PEducationListActivity"
android:screenOrientation="portrait"/>
<activity
android:name="com.dajiazhongyi.dajia.pedumodule.ui.syslib.SystemEduDetailListActivity"
android:screenOrientation="portrait"/>
<activity
android:name="com.dajiazhongyi.dajia.pedumodule.ui.syslib.SystemEduListActivity"
android:screenOrientation="portrait"/>
</application>
</manifest>
模塊化分層設計
合理的模塊化分層設計是非常重要的,就像一個房子一樣,合理的框架設計是成功的保證。
模塊化分層設計需要達到以下幾個目標:
- 模塊職責明確;
- 模塊代碼邊界清晰;
- 模塊通信
模塊職責明確
根據(jù)職責進行分層設計是合理有效的,以下是在項目實踐中采用的分層設計。

SDK
SDK層包括的內(nèi)容如圖所示,需要強調(diào)的是并不是所有的第三方Libraries都放到SDK,必須是通用的基礎級別的。
組件庫
我們將各個業(yè)務模塊公用的組件整合到組件庫中,組件庫并不一定是一個module,它也可以是多個module,實際使用的時候更多的被業(yè)務模塊依賴。
BaseCore
這是最重要的一個層級,APP核心的部分就是它,BaseCore可以用通用的定義以下幾個部分:

CoreAccount: APP賬號管理,賬號登錄、注銷、Profile信息獲取等;
CoreNetwork: 以Retrofit2為例,CoreNetwork并不提供業(yè)務模塊的API,只是提供基礎的網(wǎng)絡狀態(tài)管理、網(wǎng)絡錯誤管理;
CoreStorage: 處理SQLite、Preferences;
CoreCommunication:模塊之間的通信主要有三種:事件通知、頁面跳轉(zhuǎn)(Activity、Service)、接口調(diào)用。模塊通信是最重要的層次,后面會重點講
此外,這個層次是最容易代碼越界的層次,隨著業(yè)務的不斷復雜,業(yè)務模塊中的代碼是極有可能下沉到BaseCore的,從而導致Core層代碼越來越冗余。清晰合理的代碼邊界規(guī)范是重要的。
業(yè)務模塊
業(yè)務模塊的拆分粒度需要把控,太小的粒度并不是很合理。其中App(Release)是最終發(fā)布出去的版本,它是對其他模塊1...N 的整合。各個業(yè)務模塊在debug'階段,可以獨立打包成apk進行調(diào)試,在release階段,則作為APP的module被引用。各個業(yè)務模塊之間不進行相互調(diào)用,它們之間的通信通過BaseCore層來實現(xiàn)。
代碼邊界
合理的代碼邊界約定可以保證層次的清晰、避免架構(gòu)變得冗余,雖然沒法完全保證,畢竟定期的重構(gòu)是無法避免的。
- 各個業(yè)務模塊之間無依賴關(guān)系,模塊之間頁面的跳轉(zhuǎn)通過ARouter等頁面路由協(xié)議進行;
- 模塊之間的事件通信采用EventBus,并依賴于BaseCore層的事件Manager進行管理;
- 模塊之間的功能暴露全部通過接口,接口需要下沉到BaseCore層,接口使用前必須先注冊,調(diào)用方式形如下,后續(xù)文章會詳細介紹:
ServiceManager.regist(PluginService.class);
ServiceManager.get(PluginService.class).execute();
- 組件庫組件必須提供個性化定制,方便業(yè)務模塊使用;
- 合理控制各組件和各業(yè)務模塊的拆分粒度,太小的公有模塊不足以構(gòu)成單獨組件或者模塊的,我們先放到類似于 CommonModule 的組件中,在后期不斷的重構(gòu)迭代中視情況進行進一步的拆分;
- 上層的公有業(yè)務或者功能模塊可以逐步下放到下層,下放過程中按照層次職責歸類下放;
- 各個模塊之間的橫向依賴關(guān)系,比如在使用PluginService2之前,需要先注冊PluginService1,這種依賴管理后續(xù)會詳細介紹
模塊通信
模塊通信需要解決三大問題:
- 頁面跳轉(zhuǎn)
- 事件通知
- 接口調(diào)用
頁面跳轉(zhuǎn)
這里介紹一款頁面路由神器:ARouter https://github.com/alibaba/ARouter
本著能用、夠用、好用的原則,這款神器支持以下功能:
- 支持直接解析標準URL進行跳轉(zhuǎn),并自動注入?yún)?shù)到目標頁面中
- 支持多模塊工程使用
- 支持添加多個攔截器,自定義攔截順序
- 支持依賴注入,可單獨作為依賴注入框架使用
- 支持InstantRun
- 支持MultiDex(Google方案)
- 映射關(guān)系按組分類、多級管理,按需初始化
- 支持用戶指定全局降級與局部降級策略
- 頁面、攔截器、服務等組件均自動注冊到框架
- 支持多種方式配置轉(zhuǎn)場動畫
- 支持獲取Fragment
- 完全支持Kotlin以及混編(配置見文末 其他#5)
其調(diào)用方式如下:
1. 添加注解
@Route(path = "/test/activity")
public class YourActivity extend Activity {
...
}
2. 初始化SDK
if (isDebug()) { // 這兩行必須寫在init之前,否則這些配置在init過程中將無效
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 開啟調(diào)試模式(如果在InstantRun模式下運行,必須開啟調(diào)試模式!線上版本需要關(guān)閉,否則有安全風險)
}
ARouter.init(mApplication); // 盡可能早,推薦在Application中初始化
3. 發(fā)起路由操作
// 1. 應用內(nèi)簡單的跳轉(zhuǎn)(通過URL跳轉(zhuǎn)在'進階用法'中)
ARouter.getInstance().build("/test/activity").navigation();
// 2. 跳轉(zhuǎn)并攜帶參數(shù)
ARouter.getInstance().build("/test/1")
.withLong("key1", 666L)
.withString("key3", "888")
.withObject("key4", new Test("Jack", "Rose"))
.navigation();
實際應用中,在BaseCore中實現(xiàn)一個RouterManager,管理路由初始化,跳轉(zhuǎn)等事宜:
public class RouterManager {
/**
* Router Path
*/
public static final String URL_WELCOME = "/loginModule/welcome";
public static final String URL_LOGIN = "/loginModule/login";
public static final String URL_MAIN_LOGIN = "/loginModule/main";
public static final String URL_MAIN_PEDU = "/peduModule/main";
...
/**
* Module application name
*/
public static final String MODULE_LOGIN = "loginmodule";
public static final String MODULE_PEDU = "pedumodule";
public static void initRouter(Application application) {
if (BuildConfig.DEBUG) {
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 開啟調(diào)試模式(如果在InstantRun模式下運行,必須開啟調(diào)試模式!線上版本需要關(guān)閉,否則有安全風險)
}
ARouter.init(application);
}
public static void gotoNewPage(Context context, String pageUrl) {
ARouter.getInstance().build(pageUrl).navigation();
}
public static void goWelcome(Context context) {
ARouter.getInstance().build(URL_WELCOME).navigation();
}
public static void goLogin(Context context) {
ARouter.getInstance().build(URL_LOGIN).navigation();
}
public static void goHome(Context context) {
String packageName = context.getApplicationInfo().packageName;
LogUtils.logD(packageName);
String suffix = packageName.substring(packageName.lastIndexOf(".") + 1);
switch (suffix) {
case MODULE_LOGIN:
ARouter.getInstance().build(URL_MAIN_LOGIN).navigation();
break;
case MODULE_PEDU:
ARouter.getInstance().build(URL_MAIN_PEDU).navigation();
break;
}
}
...
}
更多使用方法可以參考github該庫的詳細介紹
由于篇幅原因,事件通知、接口調(diào)用將在后續(xù)文章中介紹?。?/p>
其他問題
資源名沖突
對于多個 Bussines Module 中資源名沖突的問題,可以通過在 build.gradle 定義前綴的方式解決:
defaultConfig {
...
resourcePrefix "module_name_"
...
}
而對于 Module 中有些資源不想被外部訪問的,我們可以創(chuàng)建 res/values/public.xml,添加到 public.xml 中的 resource 則可被外部訪問,未添加的則視為私有:
<resources>
<public name="module1_str" type="string"/>
</resources>
更多模塊化實踐經(jīng)驗,請關(guān)注后續(xù)文章的推出?。g迎大家一起交流??!