乾貨 | 從智行 Android 項目看組件化架構實踐

一、前言

智行火車票早期以火車票業務起步,隨着整體的業務發展和擴張,先後增加了汽車票、機票和酒店模塊,逐漸打造成了一個提供出行、旅行和住宿一站式預訂服務的 OTA 平臺。

在業務擴張過程中,之前 Android 項目單一工程的架構模式慢慢暴露出一些問題,例如業務間耦合較多,整體項目編譯耗時等,漸漸無法滿足業務開發需求。

爲了解決面臨的問題,綜合主流的 Android 項目架構方案,團隊選擇了組件化架構方案對項目進行了調整和實踐,抽離出基礎組件庫、獨立的業務模塊,實現了各獨立業務的拆分和獨立運行,可以單獨進行需求開發,發版時再合併到一起編譯打包和發佈。

在組件化架構實踐過程中,團隊解決了組件化調整中遇到的一些難題,對組件化技術在 Android 項目中的應用有一定的參考價值和實踐經驗。同時根據業務需求,還實現同一個項目進行多個應用差異化適配打包的功能,便於開發和維護團隊旗下的其他應用。

二、概述

本文主要根據智行 Android 團隊在組件化架構調整中的實踐過程以及最終的實踐成果,從以下幾個方面來進行闡述:

  • 爲什麼要進行組件化架構調整
  • 組件化結構調整的實施步驟
  • 組件化調整過程中遇到的難題以及解決方案
  • 組件化架構調整的成果

2.1 組件化調整的原因和目標

如前面提到的,在調整之前,項目是單一工程的架構模式,這也是常見的 Android 項目架構模式,但是一旦項目整體業務增多,擴張出相對較爲獨立的業務模塊,這種架構就會帶來一些問題,例如:

  • 業務間代碼層面耦合太重,業務之間隔離不明確:由於各業務間代碼存在較多的耦合,經常出現某個業務線功能開發迭代影響其他業務線,出現代碼衝突,影響其他業務功能。

  • 項目整體源碼較多,編譯耗時久:各業務開發人員主要開發各自業務線需求,但是需要編譯整個項目,耗時較多,影響開發效率。

  • 多應用差異化適配方案不完善:在業務擴張過程中,還衍生出一些獨立應用,例如智行旗下的訂票助手、智行機票等應用,實際是使用同一個項目打包,更改一些主題配色和首頁入口,進行差異化的編譯打包。之前使用的多應用打包方案存在一些的問題,逐漸無法滿足實際需求。

參考技術社區的 Android 架構方案,以及結合項目實際情況和業務場景,我們選擇了組件化方案來進行架構的調整。

Android 項目組件化,最早是馮森林老師在 2016 年 MDCC 大會上的《迴歸初心,從容器化到組件化》演講中提出來的,當時該方案剛提出,實際應用到項目中的還是比較少得,畢竟一般的公司項目業務不是很複雜,項目結構也是較爲單一,沒有使用組件化的必要。

但對於此時的智行 Android 項目而言,正是組件化架構最適合實踐的項目,多個業務線,項目整體比較龐大,業務間不必要的耦合過多,因此組件化架構的調整方案也就應運而生。

在進行調整之前,團隊也定下了調整預期的目標:

1)業務解耦,使得各業務模塊可以獨立運行,同時可以組合編譯打包
2)拆分基礎組件,抽離出基礎組件 Library
3)各業務間通信和業務交叉調用的實現
4)實現多應用差異化適配打包

以上大的目標點主要是來解決之前遇到的問題,也是項目架構調整的首要目的。

2.2 組件化架構調整的整體規劃

2.2.1 基礎組件的拆分

智行 Android 項目的基礎組件主要分爲業務基礎組件和功能基礎組件,其中業務基礎組件包含登錄組件、自定義 View 組件、項目網絡層組件等,這些和業務有關聯,提供給各業務模塊的基礎組件,根據具體情況拆分成 aar 或者 library,像登錄,基礎網絡層這樣較爲穩定的組件,一般直接打包成 aar,減少編譯耗時。而像自定義 View 組件,由於隨着版本迭代會有較多變化,就直接以源碼形式抽離成 Library。

基礎組件的調整相對較爲簡單,主要就是按照功能或者業務拆分成 Library ,處理好之前的引用的地方即可,但是對於拆分出來的 Library 的質量和後續維護工作是要求相對較高的,作爲基礎的組件,是需要爲各業務模塊提供基礎的功能的,重要性是相對較高的。

基礎組件庫的編譯版本設置一般是和主工程同步的,爲了方便後續升級和維護配置,可以使用如下的方式來實現 library 使用同一份配置:

ext.libDefaultConfig = { 
    minSdkVersion 19 
    targetSdkVersion 25 
    javaCompileOptions { 
      annotationProcessorOptions { 
        includeCompileClasspath = true 
      } 
    } 
}

定義一個通用的 DefaultConfig 配置,設置統一的 SDK 版本信息和編譯選項,在 Library 的 build.gralde 文件中使用如下方式即可應用到配置:

android {
      ... 
    defaultConfig libDefaultConfig 
}

這樣既可以保證基礎組件庫的編譯配置統一,也方便後期統一修改和升級。

對於再基礎點的組件,例如 Support Library、json 庫等,絕大多數基礎組件都會使用到。爲了避免每個獨立基礎組件都去引入對應的依賴,還要儘可能得保證版本的統一,我們使用了一個空殼 Library 來一次性引入這些基礎的依賴組件。

這個 Library 叫做 BaseDependencies,然後其他的基礎組件去依賴 BaseDependencies,這樣就可以保證基礎組件對這些基礎的依賴版本做到一致,後續的升級改動位置也相對較爲集中。如此調整後的依賴如下圖所示:

當然這樣的也有一定的弊端,就是每個基礎組件都可能存在引入冗餘的依賴,對於後續可能需要提供給第三方的基礎組件,還需要進行改動纔可以獨立出來。

2.2.2 業務模塊的拆分和配置

業務模塊的調整是組件化中最重要的,這裏的模塊也是組件,對於這類組件的調整目標就是做到能夠獨立運行。業務模塊的調整主要分兩步:

1)業務模塊的拆分獨立
2)業務模塊的配置

由於之前各業務的代碼以不同包名來進行區分,各業務代碼也是比較集中的,拆分出來還是相對較爲容易的。遇到兩個業務模塊都會使用到的類的話,就將對應的類下沉到 Base Module,當然這種情況也是儘可能去避免,否則 Base Module 會越來越臃腫,如果不加以控制,那麼業務模塊就變成了一個空殼,失去了組件化原本的意義。

拆分成獨立模塊業務,彼此之間是平級的關係,無依賴關係,從而從結構層面達成了分離的目的,避免了之前一不小心就出現的類相互引用,耦合嚴重的問題。

業務模塊拆分成獨立的 Library 以後,就是對其進行配置,這也是組件化的關鍵步驟,既要使得各個業務模塊可以獨立運行,又要保證各個模塊作爲整體 App 的一部分,關鍵就在於不同場景下給每個業務模塊應用不同的插件類型。

獨立運行時,需要使用:

apply plugin: 'com.android.application'

而分獨立運行時,則需要使用:

apply plugin: 'com.android.library'

爲了方便業務模塊的在這兩種模式下的快速切換和統一調整,我們使用了以下的設置方式:

// 項目的 build.gradle 中配置模塊是否獨立運行 
def isSingleCompile = false 
ext.isSingleCompile = isSingleCompile

if (isSingleCompile) { 
    ext.COMPLIEMODE = 'com.android.application' 
} else { 
    ext.COMPLIEMODE = 'com.android.library' 
}

// 業務模塊的 build.gradle 中應用 
apply plugin: COMPLIE_MODE ```

這樣在切換時,只需要修改 isSingleCompile 的值,就可以在獨立運行和作爲模塊運行之間切換。

當業務模塊獨立運行時,還需要配置獨立的 Application 和啓動頁,以及一些特殊的資源文件,這裏同樣是根據 isSingleCompile 的值來配置 sourceSets 中的屬性:

sourceSets { 
    main { 
        manifest.srcFile MANIFEST_FILE 
        res.srcDirs = RESOURCES 
        java.srcDirs = JAVA_SOURCES 
        ... 
    } 
}

這裏配置內容不再贅述,可以參考 Android 官網的說明進行設置,主要是針對獨立運行時配置 Manifest 文件和添加入口頁面的調整。

2.2.3 業務間的通信

對於拆分成獨立部分的業務模塊而言,彼此業務出現關聯的場景還是比較多的,比如火車票推薦酒店,就需要從火車票模塊跳轉到酒店模塊。

對於這樣的業務場景,除了之前提到的將部分業務下沉到 BaseModule 以外,針對頁面間跳轉,我們採用了路由的方式。由於跳轉的場景可能是原生頁面、網頁和 React Native 頁面,我們制定了一套規則來進行通用的跳轉。

跳轉的鏈接按照以下的格式來實現:

sy://suanya.cn/xxx?url=xxx&type=1

其中的 type 則是跳轉的類型, url 參數的值就是實際的跳轉的地址。對於 網頁和 React Native 頁面,url 的值是比較容易直觀的,對於原生頁面,則引入了 ARouter 實現,對於需要傳遞的參數的場景,則採用 query parameters 的方式進行傳遞,在統一的地方進行處理,轉換成 ARouter 傳參的形式。

2.3 組件化架構調整中遇到的一些問題

2.3.1 業務模塊的 Manifest 文件維護

在之前提到,業務模塊獨立運行時需要指定 Application 和啓動頁面,Manifest 文件內容如下:

<application 
       android:name=".ModuleApplication" 
       android:label="@string/app_name">    

       <!-- 模塊單獨運行需要的 activity -->
       <activity
           android:name=".ModuleLaunchActivity">
           <intent-filter>
               <action android:name="android.intent.action.MAIN"/>
               <category android:name="android.intent.category.LAUNCHER"/>
           </intent-filter>
       </activity>
       <activity
           android:name=".ModuleHomeActivity"/>
       <!-- 模塊單獨運行需要的 activity -->

       <activity
           android:name=".activity.ModuleXXXActivity"/>
           ...
</application>    

其中的 ModuleApplication、ModuleLaunchActivity 和 ModuleHomeActivity 合併打包時,根據 sourceSets 的設置,是不會編譯進來的,需要調整 AndroidManifest 文件,最簡單的辦法就是寫兩份 AndroidManifest 文件,通過 sourceSets 中的 manifest.srcFile 來指定。但是這樣存在一個問題,例如添加一個 ModuleXXXActivity,這個在獨立和非獨立運行時都需要的 Activity,則需要在兩個 AndroidManifest 中都添加一次,這樣顯然是不夠合理的,對開發而言是不友好的。

我們通過 manifest merge 規則找到解決辦法,業務模塊只需要維護獨立運行時的一份 AndroidManifest 文件即可,在合併的 App 的 AndroidManifest 中,對 application 節點,使用 replace 操作:

<application 
       android:name=".MainApplication" 
       android:theme="@styleAppTheme" 
       tools:replace="android:theme,android:name,">

而對於只有模塊獨立運行才使用到的 activity ,則採用 remove 操作進行移除:

<activity 
    android:name="com.xx.xxx.ModuleLaunchActivity" 
    tools:node="remove"/> 
<activity 
    android:name="com.xx.xxx.ModuleHomeActivity" 
    tools:node="remove"/>

如此操作之後,就可以保證最終合併打包時,業務模塊設置的 application 被替換成 app 的,而模塊獨立運行才應用到的 activity 則被移除了,只需要維護一份模塊的 AndroidManifest 文件即可。

2.3.2 多應用差異化配置打包

在前面也提到,我們的業務場景對於同一個項目打包出不同的應用,這種需求的我們使用 ProductFlavors 進行了實現。

通過設置不同的 ProductFlavors,通過 manifestPlaceholders 來配置每個應用差異化的參數,例如接入微信的 appId,地圖的 key 等。對於每個應用使用不同的主題色和資源問題,則採用在對應的 ProductFlavors 文件夾中以同名文件、同名資源名稱的方式進行覆蓋設置,這種同名的資源最終以 ProductFlavors 文件夾中的設置爲準。

除此以外,還需要針對每個應用的簽名配置進行設置:

productFlavors.all { flavor -> 
    signingConfigs.create(flavor.name, getSigningConfigsByFlavorName(flavor.name)) 
    flavor.signingConfig signingConfigs.getByName(flavor.name) 
}

需要定義 getSigningConfigsByFlavorName 方法來根據 flavor name 獲取到對應的 signingConfigs。

三、組件化架構的實踐成果

根據之前設定的目標,組件化調整後基本都完成的預期的目標:

1)業務模塊分離,從結構層面做到了代碼隔離,減少了之前不必要的耦合
2)基礎組件的拆分,按照業務和功能拆分出基礎組件,便於後期開發和維護
3)簡單實現了業務間的通信,實現了跨模塊的多種類型的通用跳轉
4)實現同一個項目多應用差異化適配打包,支持主題適配等

整體項目進行組件化調整以後,模塊的劃分更爲清晰,結構上實現了代碼隔離,減少了耦合。業務模塊支持獨立運行和整體打包,單個模塊完整編譯耗時約 20 秒左右,合併打包完整編譯整個項目耗時約 1 分鐘,極大地提升了開發效率。

最後

針對Android程序員,我這邊給大家整理了一些資料,包括不限於高級UI、性能優化、架構師課程、NDK、混合式開發(ReactNative+Weex)微信小程序、Flutter等全方面的Android進階實踐技術;希望能幫助到大家,也節省大家在網上搜索資料的時間來學習,也可以分享動態給身邊好友一起學習!

  • Android前沿技術大綱

    Android前沿技術

     

  • 全套體系化高級架構視

  • 資料領取:點贊+加羣免費獲取 Android IOC架構設計

加羣 Android IOC架構設計領取獲取往期Android高級架構資料、源碼、筆記、視頻。高級UI、性能優化、架構師課程、混合式開發(ReactNative+Weex)全方面的Android進階實踐技術,羣內還有技術大牛一起討論交流解決問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章