一.什麼是模塊化 什麼是模塊化呢?有一種定義是:模塊化是一種處理複雜系統分解為更好的可管理模塊的方式。由此可見,模塊化思路下構成的複雜系統是由各個可管理的子模塊構成的,每個子模塊之前相互獨立,並通過某種特定的方式進行通信。在工業上面,有模塊化汽車的概念,也有模塊化手機的概念,各個模塊根據一定的標準進 ...
一.什麼是模塊化
什麼是模塊化呢?有一種定義是:模塊化是一種處理複雜系統分解為更好的可管理模塊的方式。由此可見,模塊化思路下構成的複雜系統是由各個可管理的子模塊構成的,每個子模塊之前相互獨立,並通過某種特定的方式進行通信。
在工業上面,有模塊化汽車的概念,也有模塊化手機的概念,各個模塊根據一定的標準進行生產,生產之後可以直接進行各個模塊的組裝,某個模塊出現問題之後,可以單獨對這個模塊進行替換。舉個例子,同樣一款汽車,有各中配置不同的版本,比如發動機不同。這些發動機都按照一定的標準生產,但是發送的輸出和能耗並不同。重要的是其介面標準一樣。從可替換這一點來講,和軟體開發中的可插拔是異曲同工的。
Android 開發中有兩個比較相似的概念:組件化和模塊化,這裡需要進行區分的。
組件化:指的是單一的功能組件,如地圖組件、支付組件、路由組件(Router)等等;
模塊化:獨立的業務模塊,模塊相對於組件來講粒度更大。
模塊化的好處是顯而易見的。
• 多團隊並行開發測試;
• 模塊間解耦、重用;
• 可單獨編譯打包某一模塊,提升開發效率。
Android 插件化 ——指將一個程式劃分為不同的部分,比如一般 App的皮膚樣式就可以看成一個插件
Android 組件化 ——這個概念實際跟上面相差不那麼明顯,組件和插件較大的區別就是:組件是指通用及復用性較高的構件,比如圖片緩存就可以看成一個組件被多個 App共用
插件的方式只有三種:1,apk安裝,2,apk不安裝,3,dex包
二.模塊Debug和Release處理
對於模塊化項目,每個單獨的 Business Module 都可以單獨編譯成 APK。在開發階段需要單獨打包編譯,項目發佈的時候又需要它作為項目的一個 Module 來整體編譯打包。簡單的說就是開發時是 Application,發佈時是 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 :
1 <manifest xmlns:android="http://schemas.android.com/apk/res/android" 2 package="com.dajiazhongyi.dajia.pedumodule"> 3 4 <uses-permission android:name="android.permission.INTERNET" /> 5 <uses-permission android:name="android.permission.READ_PHONE_STATE" /> 6 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 7 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> 8 <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> 9 10 <application 11 ... 12 > 13 14 <activity 15 android:name="com.dajiazhongyi.dajia.loginmodule.ui.DaJiaLauncher" 16 android:exported="true" 17 android:screenOrientation="portrait"> 18 <intent-filter> 19 <action android:name="android.intent.action.MAIN" /> 20 <category android:name="android.intent.category.LAUNCHER" /> 21 </intent-filter> 22 <intent-filter> 23 <action android:name="android.intent.action.VIEW" /> 24 25 <category android:name="android.intent.category.DEFAULT" /> 26 <category android:name="android.intent.category.BROWSABLE" /> 27 28 <data android:scheme="dajia" /> 29 </intent-filter> 30 </activity> 31 32 <activity 33 android:name=".ui.MainActivity" 34 android:screenOrientation="portrait"/> 35 36 </application> 37 38 </manifest>
realease 模式下的 AndroidManifest.xml :
1 <manifest xmlns:android="http://schemas.android.com/apk/res/android" 2 package="com.dajiazhongyi.dajia.pedumodule"> 3 4 <application 5 android:allowBackup="true" 6 android:supportsRtl="true"> 7 8 <activity 9 android:name="com.dajiazhongyi.dajia.pedumodule.ui.PEducationListActivity" 10 android:screenOrientation="portrait"/> 11 12 <activity 13 android:name="com.dajiazhongyi.dajia.pedumodule.ui.syslib.SystemEduDetailListActivity" 14 android:screenOrientation="portrait"/> 15 16 <activity 17 android:name="com.dajiazhongyi.dajia.pedumodule.ui.syslib.SystemEduListActivity" 18 android:screenOrientation="portrait"/> 19 20 </application> 21 22 </manifest>
三.模塊化分層設計
合理的模塊化分層設計是非常重要的,就像一個房子一樣,合理的框架設計是成功的保證。
模塊化分層設計需要達到以下幾個目標:
-
模塊職責明確;
-
模塊代碼邊界清晰;
-
模塊通信
四.模塊職責明確
根據職責進行分層設計是合理有效的,以下是在項目實踐中採用的分層設計。
①.SDK
SDK層包括的內容如圖所示,需要強調的是並不是所有的第三方Libraries都放到SDK,必須是通用的基礎級別的。
②.組件庫
我們將各個業務模塊公用的組件整合到組件庫中,組件庫並不一定是一個module,它也可以是多個module,實際使用的時候更多的被業務模塊依賴。
③.BaseCore
這是最重要的一個層級,APP核心的部分就是它,BaseCore可以用通用的定義以下幾個部分:
CoreAccount: APP賬號管理,賬號登錄、註銷、Profile信息獲取等;
CoreNetwork: 以Retrofit2為例,CoreNetwork並不提供業務模塊的API,只是提供基礎的網路狀態管理、網路錯誤管理;
CoreStorage: 處理SQLite、Preferences;
CoreCommunication:模塊之間的通信主要有三種:事件通知、頁面跳轉(Activity、Service)、介面調用。模塊通信是最重要的層次,後面會重點講
此外,這個層次是最容易代碼越界的層次,隨著業務的不斷複雜,業務模塊中的代碼是極有可能下沉到BaseCore的,從而導致Core層代碼越來越冗餘。清晰合理的代碼邊界規範是重要的。
④業務模塊
業務模塊的拆分粒度需要把控,太小的粒度並不是很合理。其中App(Release)是最終發佈出去的版本,它是對其他模塊1…N 的整合。各個業務模塊在debug’階段,可以獨立打包成apk進行調試,在release階段,則作為APP的module被引用。各個業務模塊之間不進行相互調用,它們之間的通信通過BaseCore層來實現。
五.代碼邊界
合理的代碼邊界約定可以保證層次的清晰、避免架構變得冗餘,雖然沒法完全保證,畢竟定期的重構是無法避免的。
①各個業務模塊之間無依賴關係,模塊之間頁面的跳轉通過ARouter等頁面路由協議進行;
②模塊之間的事件通信採用EventBus,並依賴於BaseCore層的事件Manager進行管理;
③模塊之間的功能暴露全部通過介面,介面需要下沉到BaseCore層,介面使用前必須先註冊,調用方式形如下,後續文章會詳細介紹:
ServiceManager.regist(PluginService.class); ServiceManager.get(PluginService.class).execute();
④組件庫組件必須提供個性化定製,方便業務模塊使用;
⑤合理控制各組件和各業務模塊的拆分粒度,太小的公有模塊不足以構成單獨組件或者模塊的,我們先放到類似於 CommonModule 的組件中,在後期不斷的重構迭代中視情況進行進一步的拆分;
⑥上層的公有業務或者功能模塊可以逐步下放到下層,下放過程中按照層次職責歸類下放;
⑦各個模塊之間的橫向依賴關係,比如在使用PluginService2之前,需要先註冊PluginService1,這種依賴管理後續會詳細介紹
六.模塊通信
模塊通信需要解決三大問題:
-
頁面跳轉
-
事件通知
-
介面調用
頁面跳轉
這裡介紹一款頁面路由神器:ARouter https://github.com/alibaba/ARouter
本著能用、夠用、好用的原則,這款神器支持以下功能:
- 支持直接解析標準URL進行跳轉,並自動註入參數到目標頁面中
- 支持多模塊工程使用
- 支持添加多個攔截器,自定義攔截順序
- 支持依賴註入,可單獨作為依賴註入框架使用
- 支持InstantRun
- 支持MultiDex(Google方案)
- 映射關係按組分類、多級管理,按需初始化
- 支持用戶指定全局降級與局部降級策略
- 頁面、攔截器、服務等組件均自動註冊到框架
- 支持多種方式配置轉場動畫
- 支持獲取Fragment
- 完全支持Kotlin以及混編(配置見文末 其他#5)
其調用方式如下:
1. 添加註解 @Route(path = "/test/activity") public class YourActivity extend Activity { ... } 2. 初始化SDK if (isDebug()) { // 這兩行必須寫在init之前,否則這些配置在init過程中將無效 ARouter.openLog(); // 列印日誌 ARouter.openDebug(); // 開啟調試模式(如果在InstantRun模式下運行,必須開啟調試模式!線上版本需要關閉,否則有安全風險) } ARouter.init(mApplication); // 儘可能早,推薦在Application中初始化 3. 發起路由操作 // 1\. 應用內簡單的跳轉(通過URL跳轉在'進階用法'中) ARouter.getInstance().build("/test/activity").navigation(); // 2\. 跳轉並攜帶參數 ARouter.getInstance().build("/test/1") .withLong("key1", 666L) .withString("key3", "888") .withObject("key4", new Test("Jack", "Rose")) .navigation();
實際應用中,在BaseCore中實現一個RouterManager,管理路由初始化,跳轉等事宜:
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(); // 開啟調試模式(如果在InstantRun模式下運行,必須開啟調試模式!線上版本需要關閉,否則有安全風險) } 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該庫的詳細介紹
由於篇幅原因,事件通知、介面調用將在後續文章中介紹!!
其他問題
資源名衝突
對於多個 Bussines Module 中資源名衝突的問題,可以通過在 build.gradle 定義首碼的方式解決:
defaultConfig { ... resourcePrefix "module_name_" ... }
而對於 Module 中有些資源不想被外部訪問的,我們可以創建 res/values/public.xml,添加到 public.xml 中的 resource 則可被外部訪問,未添加的則視為私有:
<resources> <public name="module1_str" type="string"/> </resources>