Android 模組化探索和實踐(1):基本思路

codeGoogle發表於2018-04-11

隨著移動平臺的不斷髮展,軟體慢慢變的越來越複雜,業務繁多,體積臃腫;為了降低大型軟體複雜性和耦合度,同時也為了適應模組重用、多團隊並行開發測試等等需求,Android社群提出了兩種解決方案:模組化和外掛化。外掛化暫且按下不提,本文主要講述模組化。從基本思路上來講,模組化的實現大體上來講都是差不多的,本文將著重講述基本思路。此外,在實踐的過程中也有特別的地方:Databinding在模組化中的坑,Dagger2在模組化中的應用,頁面統一跳轉,模組化通訊方式設計,模組層級架構設計等。這些問題將在本文和後面的系列文章中一一介紹。

什麼是模組化

什麼是模組化呢?有一種定義是:模組化是一種處理複雜系統分解為更好的可管理模組的方式。由此可見,模組化思路下構成的複雜系統是由各個可管理的子模組構成的,每個子模組之前相互獨立,並通過某種特定的方式進行通訊。 在工業上面,有模組化汽車的概念,也有模組化手機的概念,各個模組根據一定的標準進行生產,生產之後可以直接進行各個模組的組裝,某個模組出現問題之後,可以單獨對這個模組進行替換。舉個例子,同樣一款汽車,有各中配置不同的版本,比如發動機不同。這些發動機都按照一定的標準生產,但是傳送的輸出和能耗並不同。重要的是其介面標準一樣。從可替換這一點來講,和軟體開發中的可插拔是異曲同工的。

Android 開發中有兩個比較相似的概念:元件化和模組化,這裡需要進行區分的。 元件化:指的是單一的功能元件,如地圖元件、支付元件、路由元件(Router)等等; 模組化:獨立的業務模組,模組相對於元件來講粒度更大。

模組化的好處是顯而易見的。 • 多團隊並行開發測試; • 模組間解耦、重用; • 可單獨編譯打包某一模組,提升開發效率。

模組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 :

<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>

複製程式碼

模組化分層設計

合理的模組化分層設計是非常重要的,就像一個房子一樣,合理的框架設計是成功的保證。 模組化分層設計需要達到以下幾個目標:

  1. 模組職責明確;
  2. 模組程式碼邊界清晰;
  3. 模組通訊

模組職責明確

根據職責進行分層設計是合理有效的,以下是在專案實踐中採用的分層設計。

SDK SDK層包括的內容如圖所示,需要強調的是並不是所有的第三方Libraries都放到SDK,必須是通用的基礎級別的。

元件庫 我們將各個業務模組公用的元件整合到元件庫中,元件庫並不一定是一個module,它也可以是多個module,實際使用的時候更多的被業務模組依賴。

BaseCore 這是最重要的一個層級,APP核心的部分就是它,BaseCore可以用通用的定義以下幾個部分:

image.png

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,這種依賴管理後續會詳細介紹

模組通訊

模組通訊需要解決三大問題:

  1. 頁面跳轉
  2. 事件通知
  3. 介面呼叫
頁面跳轉

這裡介紹一款頁面路由神器:ARouter github.com/alibaba/ARo…

本著能用、夠用、好用的原則,這款神器支援以下功能:

  1. 支援直接解析標準URL進行跳轉,並自動注入引數到目標頁面中
  2. 支援多模組工程使用
  3. 支援新增多個攔截器,自定義攔截順序
  4. 支援依賴注入,可單獨作為依賴注入框架使用
  5. 支援InstantRun
  6. 支援MultiDex(Google方案)
  7. 對映關係按組分類、多級管理,按需初始化
  8. 支援使用者指定全域性降級與區域性降級策略
  9. 頁面、攔截器、服務等元件均自動註冊到框架
  10. 支援多種方式配置轉場動畫
  11. 支援獲取Fragment
  12. 完全支援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>

複製程式碼

更多模組化實踐經驗,請關注後續文章的推出!!歡迎大家一起交流!!

原文地址: http://mp.weixin.qq.com/s/ktbIDjOUzM-gL0uEhowFBw

歡迎一起學習和交流

相信自己,沒有做不到的,只有想不到的

如果你覺得此文對您有所幫助,歡迎入群 QQ交流群 :644196190 微信公眾號:終端研發部

技術+職場

相關文章