Android項目模塊化/組件化開發(非原創)

文章大綱

一、項目模塊化初步介紹
二、項目模塊化的兩種模式與比較
三、大型項目模塊化的演進
四、項目模塊化總結
五、參考文章

 

一、項目模塊化初步介紹

1. 前言

在Android開發中,隨着項目的不斷擴展,項目會變得越來越龐大,而隨之帶來的便是項目維護成本與開發成本的增加!每次調試時,不得不運行整個項目;每當有新成員加入團隊時,需要更多的時間去了解龐大的項目。。。而爲了解決這些問題,團隊通常會將項目模塊化,以此來降低項目的複雜度和耦合度,讓團隊可以並行開發與測試,讓團隊成員更加專注於自己所負責的功能模塊開發。。。
對於一些大廠如BAT或者美團等這些大型互聯網公司,都會自己造輪子,實現項目模塊化。而對於中小型公司,限於成本因素,一般都是選用這些大廠造的優秀的輪子來進行項目模塊化。

2. 模塊化需要做什麼

首先,在開始項目模塊化之前,我們必須要明確模塊化需要做些什麼?這就等於寫書之前必須得有個總綱,否則越寫到後面,越是混亂。以下是我認爲在模塊化時需要注意的幾個問題:
(1)如何拆分項目
(2)模塊之間的通信
(3)模塊內的代碼隔離
(4)模塊在調試與發佈模式之間的切換

3. 如何拆分項目

 

如上圖所示,我將項目大概劃分爲五層:
宿主層
不做具體的項目功能實現,只負責集成業務模塊,組裝成一個完整的APP
業務模塊層
將項目的每個大功能模塊拆分成的一個一個單獨的module
基礎業務組件層
此層最大的作用是爲了複用,例如首頁模塊與新盤模塊中都有樓盤搜索這個功能,且UI顯示相似,這時在兩個模塊中都實現樓盤搜索就顯得繁瑣了,像這種與業務有關聯且需要多處使用的情況,我們完全可以將其抽離出來作爲基礎業務組件
功能組件層
項目中常用的功能庫,如圖片加載、網絡請求等
底層SDK
從公司項目中長期積累出來的底層類庫
以上是大多數項目模塊化時的拆分方式,每個人也可以根據項目的實際情況進行調整。

4. 模塊之間的通信

4.1 常用的通信方式
當項目被拆分成多個模塊後,模塊之間的良好的通信是我們必須考慮的問題。ARouter本身也提供一套通信機制,但是一般很難滿足我們所有的需求,所以我們會容易想到的常用的幾種通信方式:EvenBus、協議通信、廣播或者是將通信的部分下沉到公共組件庫。對於這幾種方式,在一些大廠的技術文章中都有提到一些他們的看法,下面我簡單總結一下:
EventBus: 我們非常熟悉的事件總線型的通信框架,非常靈活,採用註解方式實現,但是難以追溯事件,微信、餓了麼認爲這是個極大的缺點,不是很推薦,但是美團覺得只要自身控制的好就行(自己設計了一套基於LiveData的簡易事件總線通信框架)。
協議通信: 通信雙發必須得都知曉協議,且協議需要放在一個公共部分保存。雖然解耦能力強,但是協議一旦變化,通訊雙方的同步會變的複雜,不方便。
廣播: 安卓的四大組件之一,常見的通信方式,但是相對EventBus來說,過重。
下沉到公共組件庫: 這是在模塊化中常見的做法,不斷的將各種方法、數據模型等公共部分下成到公共組件庫,這樣一來,公共組件庫會變的越來越龐大,越來越中心化,違背了項目模塊化的初衷。最後,越來越難以維護,不得不在重新拆分公共組件庫。

4.2 如何對外暴露接口
解決了通信手段的問題,我們就得考慮另一個問題,爲其他模塊提供的接口+數據結構我們應該放在哪裏?下沉到公共模塊嗎?或者另外新建一個module用來維護這些接口+數據結構?但是這樣一來,成本就有些大了,也不方便。

在微信的模塊化文章中提出了一個解決方法,將你要暴露的接口+數據結構甚至其他想要暴露的文件都.api化,即將你要暴露的文件的後綴名改爲api,然後通過特定的方法將api後綴的文件拷貝出來,在module外部重新組成一個新的module(也可稱爲SDK),而想要使用的模塊只需要調用這個SDK即可。當然,拷貝文件和組件SDK是完全自動化的,並非手工,這樣才能節省成本。

由於微信的模塊化文章中沒有涉及到.api化的具體實現,所以根據這種思路,我使用了其他方法來實現要達到的效果。具體思路如下:

創建一個名爲_exports的文件夾,需要對外暴露的文件都放在裏面

 

將_exports文件夾打包成jar

/**
 * 創建 jar 包
 */
task makeExportJar(type: Jar) {
    baseName = "hpauth-exports"
    version = "1.0.0"
    extension = "jar"
    // java文件編譯後的所在位置
    from('build/intermediates/classes/debug/')
    // kotlin文件編譯後的所在位置
    from('build/tmp/kotlin-classes/debug/')
    include('com/homeprint/module/auth/_exports/**')
    // jar包導出路徑
    destinationDir = file('build/_exports')
}

將jar發佈到本地maven倉庫(發佈到本地僅僅針對個人開發的時候;團隊開發時,大家使用各自的電腦,無法訪問到你本地的maven倉庫,所以這時需要在局域網中建立一個maven倉庫,詳情請查看《Android:超詳細的本地搭建maven私服以及使用Nexus3.x搭建maven私服的講解》)

artifacts {
    archives makeExportJar
}

uploadArchives {
    repositories {
        mavenDeployer {
            // 本地的maven倉庫路徑
            repository(url: uri("../repo"))
            pom.project {
                groupId 'com.homeprint.module'
                artifactId 'auth-exports'
                version '1.0.0'
            }
        }
    }
}

在需要的模塊調用jar

compileOnly 'com.homeprint.module:auth-exports:1.0.0@jar'

注: 此處必須使用compileOnly來調用,compileOnly是provided的替代方法,provided將被google廢棄。此處使用compileOnly代表,jar包只在編譯時有效,不參與打包。如果使用api或者implementation,因爲我們只是將文件拷貝出來成爲一個單獨的SDK,並未修改包名和文件名,當將多個模塊集成爲一個app時,會拋出異常,提示jar包中的文件已存在。

5. 組件的生命週期管理

在組件化開發時,每個組件都應該有自己獨立的生命週期,這個生命週期類似於組件自己的Application,在這個生命週期中,組件可以做一些類庫的初始化等工作,否則如果每個組件都將這些工作集中到殼工程的Applicaiton中實現的話,會顯得殼工程的Application太過中心化,並且一旦需要修改,會很麻煩,且容易產生衝突。

基於上述原因,我們可以自己搭建一個簡易的組件生命週期管理器,主要分爲兩步:

構建組件的生命週期模型,構建的模型持有整個app的Application引用,同時提供三個基礎方法:生命週期創建、生命週期停止以及生命週期的優先級設置。

public abstract class BizLifeCycle {
    private Application app;
    // 優先級,priority 越大,優先級越高
    @IntRange(from = 0)
    private int priority = 0;

    public BizLifeCycle(@NonNull Application application) {
        this.app = application;
    }

    public Application getApp() {
        return app;
    }

    /**
     * 獲取優先級,priority 越大,優先級越高
     */
    public int getPriority() {
        return priority;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    public abstract void onCreate();

    public abstract void onTerminate();
}

在每個組件中只要實現上述模型即可。
有了生命週期模型,我們還需要一個管理器,用於管理這些組件的生命週期模型,在這個管理器中,我們同樣需要實現三個基礎方法:生命週期模型的註冊,生命週期模型的反註冊以及執行已存在的生命週期模型。

public class BizLifeCycleManager {
    private static ArrayList<BizLifeCycle> sPinLifeCycleList;

    /**
     * 註冊組件的生命週期
     */
    public static <T extends BizLifeCycle> void register(@NonNull T lifeCycle) {
        if (sPinLifeCycleList == null) {
            sPinLifeCycleList = new ArrayList();
        }
        if (!sPinLifeCycleList.contains(lifeCycle)) {
            sPinLifeCycleList.add(lifeCycle);
        }
    }

    /**
     * 執行組件生命週期
     */
    public static void execute() {
        if (sPinLifeCycleList != null && !sPinLifeCycleList.isEmpty()) {
            // 冒泡算法排序,按優先級從高到低重新排列組件生命週期
            BizLifeCycle temp = null;
            for (int i = 0, len = sPinLifeCycleList.size() - 1; i < len; i++) {
                for (int j = 0; j < len - i; j++) {
                    if (sPinLifeCycleList.get(j).getPriority() < sPinLifeCycleList.get(j + 1).getPriority()) {
                        temp = sPinLifeCycleList.get(j);
                        sPinLifeCycleList.set(j, temp);
                        sPinLifeCycleList.set(j + 1, temp);
                    }
                }
            }
            for (BizLifeCycle lifeCycle : sPinLifeCycleList) {
                lifeCycle.onCreate();
            }
        }
    }

    /**
     * 解除組件生命週期
     */
    public static <T extends BizLifeCycle> void unregister(@NonNull T lifeCycle) {
        if (sPinLifeCycleList != null) {
            if (sPinLifeCycleList.contains(lifeCycle)) {
                lifeCycle.onTerminate();
                sPinLifeCycleList.remove(lifeCycle);
            }
        }
    }

    /**
     * 清除所有的組件生命週期
     */
    public static void clear() {
        if (sPinLifeCycleList != null) {
            if (!sPinLifeCycleList.isEmpty()) {
                for (BizLifeCycle lifeCycle : sPinLifeCycleList) {
                    lifeCycle.onTerminate();
                }
            }
            sPinLifeCycleList.clear();
        }
    }
}

調用示例如下,直接在殼工程的Application中使用,因爲生命週期模型中都設置有優先級,所以在設置了優先級的情況下,可以不必在意register的順序。如此一來,將生命週期註冊後,需要更改每個組件件的一些初始化工作,可以直接在組件的生命週期中修改,而不需要變更殼工程的Application。

override fun onCreate() {
        super.onCreate()
        BizLifeCycleManager.register(CommonBizLifeCycle(this))
        BizLifeCycleManager.register(HttpBizLifeCycle(this))
        BizLifeCycleManager.register(RouterBizLifeCycle(this))
        BizLifeCycleManager.register(ThirdBizLifeCycle(this))
        BizLifeCycleManager.execute()
    }

6. 模塊在調試與發佈模式之間的切換

項目開發時,一般提供調試環境與正式發佈環境。在不同的環境中,app的有些功能是不需要用到的,或者是有所不同的。另外,在模塊化開發時,有些業務模塊在調試時,可以作爲單獨的app運行調試,不必每次都編譯所有的模塊,極大的加快編譯速度,節省時間成本。基於,以上種種原因,我們就必須對項目的調試與正式環境做不同的部署配置,然而如果全靠每次手動修改,當模塊量達到數十時,則會非常麻煩,且容易出錯。所以我們需要儘可能的用代碼做好配置。

首先,來看如何配置module在app與library之間的切換,實現module在調試時作爲app單獨運行調試。看示例代碼,這也是比較常見的方式:

在gradle.properties中,配置字段isAuthModule,控制auth模塊是作爲一個模塊還是一個app

# true: 作爲一個模塊 
# false: 作爲一個app
isAuthModule=true

在auth模塊的build.gradle文件中作如下配置:

// 獲取 gradle.properties 文件中的配置字段值
def isApp = !isAuthModule.toBoolean()

// 判斷是否爲 app,選擇加載不同的插件
if (isApp) {
   apply plugin: 'com.android.application'
} else {
   apply plugin: 'com.android.library'
}
......

android {
   defaultConfig {
        // library 是沒有 applicationId 的,只有爲 app 時,纔有
         if (isApp) {
             applicationId "com.homeprint.module.auth"
         }
         ......
   }
   
   sourceSets {
          main {
              // 作爲app與模塊時的AndroidManifest.xml會有所不同,在不同狀態時選擇不同的AndroidManifest.xml
              if (isApp) {
                  manifest.srcFile 'src/main/AndroidManifest.xml'
              } else {
                  // 記得在 main 文件夾下創建 module 文件夾,添加AndroidManifest.xml
                  manifest.srcFile 'src/main/module/AndroidManifest.xml'
          }
   }
}

這樣只要我們更改一下,gradle.properties文件中的配置字段,就可以自由實現module在模塊與app之間的切換。下面我們再來看下,如何實現app在調試與發佈環境時,加載不同的模塊,看以下示例:

假如有兩個模塊,lib_debug 與 lib_release,lib_debug 是只有在調試環境才需要使用,
lib_release 是只有在正式環境才需要使用,以下提供兩種方式實現

方式一:
 if(mode_debug){
     implementation project(':lib_debug')
 }else{
     implementation project(':lib_release')
 }


方式二:
 debugImplementation project(':lib_debug')
 releaseImplementation project(':lib_release')

以上兩種方式,方式一類似於上述的模塊在 app 與 library 之間的切換,方式二是使用 gradle 提供的方法實現

7. maven私服的講解與項目上傳與引用

https://blog.csdn.net/liyi1009365545/article/details/84766956

二、項目模塊化的兩種模式與比較

目前項目模塊化大體可以分爲兩種模式,分別是submodule和multi-project。根據字面意思,我們就可以很容易理解這兩種模式,下面就讓我們來具體瞭解一下這兩種模式!

1. submodule模式

 

如上圖所示,項目中只有一個project工程,在project中構建多個module組件,每個module都有自己的git倉庫,非常直觀,這也是我們最常見的模塊化架構。

優點
架構直觀,可以讓加入開發的新成員比較快速的理解項目的構建
團隊協作靈活,在項目開發階段(特別是起始不穩定的階段),有更多的module依賴選擇,例如直接依賴project,或者通過aar/jar依賴,或者是maven依賴,可以更加快速的進行開發調試

缺點
整個project的git分支會很複雜
團隊協作的時候,大家都是在同一個app模塊中做測試自己開發的模塊,比較容易產生衝突
因爲所有的module都在一個project中,每個人都可以修改他人負責的module,不是很安全

2. multi-project模式

 

如上圖所示,這種模式是將每個功能模塊都拆分成一個單獨的功能project,每個功能project都至少包含自己測試使用的app模塊和功能模塊這兩個module。此外,還有一個專門的project用來作爲app殼工程,組合所有的功能模塊。這樣每個project都有自己的單獨的git倉庫和單獨的測試使用的app模塊。

優點
每個project有自己單獨的git倉庫,減少項目的git複雜度
每個project有自己的app模塊用於測試,避免影響他人
開發成員只需要負責自己的project,不需要關注其他的功能模塊,更加專注
開發成員只能關注自己負責的功能模塊,無法修改到其他人負責的功能模塊,更加安全
更加解耦

缺點
對於新加入的開發成員不是很友好,不能直觀的瞭解項目的構建
需要在項目開發前達成一些規範協議,否則在協作時容易產生衝突,比如資源文件的命名,如果在兩個module中出現命名一樣的資源文件,則會報錯
因爲每個功能都是單獨的project,所以開發調式時,只能使用aar/jar或者maven來依賴需要的module,不如submodule模式靈活,在項目初期不穩定時的開發成本要高於submodule模式
會增大項目的體積

3. 建議

(1)在個人開發或者小團隊開發時,沒有必要使用multi-project模式,成本太高
(2)開發小項目時,submodule模式足以,如果項目後期變的越來越大,可以在轉multi-project模式
(3)在項目前期不穩定時,如果要使用multi-project模式架構,對於基礎組件與公共組件部分儘可能的要考慮完善,否則每次更改都需要重新發布aar/jar或者上傳到maven倉庫中,成本較高(畢竟Android Studio的運行速度有時確實不盡人意)

三、大型項目模塊化的演進

1. 美團外賣Android平臺化架構演進實踐

https://tech.meituan.com/2018/03/16/meituan-food-delivery-android-architecture-evolution.html

2. 微信Android模塊化架構重構實踐

https://mp.weixin.qq.com/s/6Q818XA5FaHd7jJMFBG60w

四、項目模塊化總結

今天我們講的主題是基於項目模塊化來說的,模塊化是什麼大家肯定都是知道了的。我們說,做模塊化其實跟項目重構很像,都是從這幾個點來做的,只是側重點不同。分別是:刪除、組織、降級、解耦。

模塊化重構

 

刪除:刪除不必要的文件,儘可能減小工程體積。這裏有一組數據,是我統計我們餓了麼的一款 APP 在模塊化前後一些文件的數量。
可以看到,.java文件從1677個減少到了1543個。其實這不是重點,重點是下面的drawable,這裏drawable只包含圖片、和xml佈局,當經過模塊化重構後文件數從 693 減少到 538 個。圖片資源減少接近 200 個,apk 的大小也會隨之降低。

 

而組織呢,指的是:按照有意義的標準將代碼分組。這其實也是java的包所存在的目的之一。
但是隨着項目的不斷迭代,需求很緊的情況下是很難有時間去真正規範的將類分組的。看到圖中,我們之前的結構很亂,就是因爲項目快速迭代和人員更替的過程中,不免會有這樣的現象。所以這也是模塊化重構時所作的一件大事。

 

接下來就是我們經常說的內聚和耦合了,降級。我們之前有一個類叫:Navigator,它是負責幾乎所有Activity直接跳轉的。就是我們會把所有的startActivity()的跳轉放到這個類裏面去寫。之前少的時候還好,結果等我看到這個類的時候,這個類已經有 200 多個方法了,全是Activity跳轉的方法。

而我們在做模塊化重構時的做法就是,首先觀察自己的項目,這是重構很重要的一步,就是要結合自身。把這個類拆分成了三大部分,我們有兩塊業務是會頻繁跳轉的但這兩個業務跳轉的頁面又都是在自身的模塊內,分別是用戶模塊和商戶模塊。因此將這兩個模塊中分別建立兩個用於模塊自己內部的跳轉叫UserNavigator和ShopNavigator,而模塊間的跳轉或一些小模塊內部的則使用Router去做。

之後解耦,也是今天的重點,如何優雅移除模塊間的耦合。
到目前爲止,我們已經能夠做到讓所有不包含業務狀態接口的模塊的增刪,不需要改動任何一行代碼。
具體到一個示例就是這樣:

 

或者,也可以是這樣:

 

這兩個段代碼的區別就是一個是手動管理Debug的狀態,另一個是交給Gradle的編譯任務去控制,原理上是一樣的。
而這麼做是如何實現的呢,其思路就是:一個模塊就是一個功能,你想要讓你的 apk 具備這個功能,就添加這個模塊一起編譯就可以了。這纔是我們說的真正的組件化,模塊之間零耦合,增減模塊零改動。
例如圖中:debug這個模塊,肯定不會用在正式的生產環境;而相反的tinker這個模塊,熱補丁肯定也不會用於調試階段。所以我在開發時就可以不使用這個模塊相關的代碼。
另外再舉個使用的例子:我有一個訂單模塊,訂單模塊需要播放鈴聲,比如大家在飯店經常聽到“您有新的餓了麼訂單,請及時處理”。但我在開發訂單模塊的時候,如果我已經確定鈴聲播放是沒有問題的,那我可以選擇開發階段不打鈴聲的包,直到發佈到線上了再去加上鈴聲的包。那我沒有添加這個鈴聲模塊的時候,我就默認不具備播放鈴聲的功能,但完全不影響其他的訂單模塊的業務功能,而這個鈴聲模塊的增刪,是不需要修改任何代碼的。
聽到這裏相信大家都很好奇這是怎麼實現的。接下來就跟大家講講內部的原理。

鐵金庫解耦
所有的核心功能都來自我們自己寫的一個庫:IronBank。取《自冰與火之歌》中的【鐵金庫】,叫鐵金庫不容拖欠。
鐵金庫的內部實現,其實是使用了 APT 註解處理器,去在編譯時解析註解生成一個類,讓這個類去生成跨模塊的對象。鐵金庫使用了與後端 SOA 設計思路類似的方式:將模塊之間的主動依賴倒置,變爲功能的提供與使用。

 

例如圖上左邊有一個對外提供媒體功能的服務提供者,他告知IronBank我提供媒體服務:“嘿,老鐵,我這有個媒體服務,你那邊有誰要用的時候可以用我的。”
到了另一邊,如果此刻有模塊說是,我需要媒體服務:“老鐵,你那有沒有媒體服務,我這邊需要播一個鈴聲啊!”。
“有的,給你。”
IronBank就會將之前服務提供者提供給他的媒體對象交給服務使用者。

五、參考文章

    1. https://blog.csdn.net/liyi1009365545/article/details/84032509
    2. https://blog.csdn.net/liyi1009365545/article/details/84766956
    3. https://blog.csdn.net/liyi1009365545/article/details/85853027
    4. https://tech.meituan.com/2018/03/16/meituan-food-delivery-android-architecture-evolution.html
    5. https://mp.weixin.qq.com/s/6Q818XA5FaHd7jJMFBG60w
    6. https://xiaozhuanlan.com/topic/3629451870
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章