隨着APP的不斷迭代,業務愈來愈複雜,代碼量愈來愈多,單個APP的模式已開始影響開發效率,並且原來的單模塊很難進行業務遷移。因此決定採用模塊化/組件化的思想對APP進行重構。android
組件化和模塊化是當前軟件開發中經常使用的與平臺無關的的解耦手段,被普遍應用在軟件的架構層面。這二者一般是相輔相成,經過組合的方式來使用。它們只是架構方面的一種思想,在代碼的實現層面上沒有多大區別。我的以爲差異也就如下兩點:git
在早期的Java開發中,提倡將整個項目結構按照程序的邏輯結構進行分層,好比表示數據的Dao層,表示控制器的Control層,表示View的View層,可是隨着業務的不斷迭代,發現這種分層方式有很大的弊端,代碼難以定位,且後期難以維護。隨後就出現了以業務結構劃分的模式,這種結構完全解決了以上問題。因此當前的APP基本上使用的都是這種以業務劃分的模式,但隨着App的不斷迭代,業務變得愈來愈複雜,代碼量愈來愈多,維護也變得愈來愈困難。還有一個明顯的問題,Gradle在編譯的時候花費的時間愈來愈長,這大大下降了APP的開發效率。既然單個APP難以解決這個問題,那能夠將項目進行拆分,每一個人只須要負責開發、維護本身的模塊便可。如何拆分呢?使用模塊化技術按業務邏輯將APP進行劃分,使得這些被拆分出來的模塊可單獨運行,這樣就提升了編譯速度,下降了維護成本。github
此次重構採用層次化的方式,模塊化的思想,對APP進行了完全的重構,具體的項目結構以下圖所示:數據庫
從結構上來看,APP被劃分紅5層,每層的功能具體以下:api
這是一個空的項目,其中只包含了一個Application的子類和一個IntentService的子類,主要用來對APP中使用的各類組件進行初始化,IntentService的做用是爲了提升APP冷啓動的速度,將各類組件的初始化放在後臺線程異步執行,這裏須要注意的是,對於在Applicaiton或SplashActivity中就會使用的組件,最好直接在Application中進行初始化,不然會拋出未初始化的異常。緩存
這裏的業務層被劃分爲Main模塊和其餘模塊(至於劃分幾個模塊,根據本身APP的業務,選擇合適的粒度進行劃分便可)。這裏的Main模塊主要包括:新用戶引導頁,啓動頁,主頁。具體業務方面的頁面,都放在具體的業務模塊中。微信
公共組件層主要包括APP中使用的第三方組件,這些組件基本都是如今APP的通用功能,爲上層的業務層提供支持。至於模塊劃分,雖然這些都是單獨的組件,可是每一個作爲一個Module未免有些繁瑣了,因此仍是推薦放在一個Module中。在選擇第三方的庫時,須要作必定的調研,儘可能選擇大公司,使用用戶多的SDK,同時在使用時最好封裝一下,這樣後面更換時也方便。網絡
基礎業務層主要用來統一APP的代碼結構,UI風格等,主要包含如下三個方面:架構
主要是對Activity/Fragment的封裝,提供了不需網絡請求的BaseActivity/BaseFragment和須要網絡請求的BaseProgressActivity/BaseProgressFragment, 爲頁面的代碼提供統一的結構,頁面的樣式提供統一的風格。app
主要包含各類樣式的Dialog, 自定義View等,根據APP的設計風格提供統一的樣式;
圖片操做庫ImageSet是對圖片組件庫(包括Fresco, Glide, Universer ImageLoader)的封裝,同時提供了調用系統相機/相冊選擇&裁剪照片,相似微信選擇圖片的組件,圖片上傳,圖片壓縮等功能。這個小模塊其實也可放在Common組件層,只是以爲這裏面也有一些業務相關的功能,因此就放在了這一層。
固然,基礎業務層還還包括APP設計風格中須要用到的各類動畫,樣式,顏色值,尺寸值等資源。在進行業務開發時,統一使用這些資源,爲後續修改整個APP風格提供可能。
這裏包含了一些通用的組件,包括各類經常使用的工具類,通用的UI庫,數據源的封裝(包括網絡,文件,數據庫)。這是一個APP的基本架構,裏面包含的類基本不須要改動。因此在對工具類和通用UI進行定義時,須要考慮放置的位置是否準確。
以上是整個項目使用組件化/模塊化後基本結構的詳細介紹,可是在開發過程當中仍是遇到了不少問題。
從Android工程的結構能夠看出,app模塊和新建的其餘Module的結構基本一致,最大的區別就是build.gradle的結構:
// app模塊中build.gralde的結構:
apply plugin: 'com.android.application'
// 其餘Module中build.gralde的結構:
apply plugin: 'com.android.library'
複製代碼
因此Module是否可以運行,關鍵就在於plugin的類型。將新建Module的build.gradle中的'com.android.library' 改爲 'com.android.application',同步以後選擇相應的模塊運行便可。
因此模式的切換隻須要根據條件進行判斷便可,咱們可在gradle.properties中定義一個常量,控制Module的運行模式:
gradle.properties中定義IS_MODULE:
IS_MODULE = false
複製代碼
而後在Module的build.gradle中添加條件判斷:
if (IS_MODULE.toBoolean()) {
apply plugin: 'com.android.library'
} else {
apply plugin: 'com.android.application'
}
複製代碼
這樣在進行模式切換時,只須要修改IS_MODULE的值便可。
APP在進行打包時,會將全部依賴的Module中的AndroidManifest文件進行合併,具體的合併規則參考合併多個清單文件。合併最基本的原則:只能有一個Application配置了name屬性,只能有一個Activity被配置成了主Activity。可是Module中若是不配置Applicaition中name屬性,就不能進行相應的初始化,若是不指定主Activity,APP也沒法運行。這裏可以使用兩種方案來解決:
module/build.gradle
sourceSets {
main {
if (IS_MODULE.toBoolean()) {
manifest.srcFile '../模塊名/src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile '../模塊名/src/main/AndroidManifest.xml'
}
}
}
複製代碼
若是選擇了雙AndroidManifest文件的方式,那麼在做爲Library的AndroidManifest(默認的)中組件的定義應儘可能簡單,包括主題都不要定義,這樣合併時就不多出現合併錯誤。
若是在AndroidManifest.xml中定義了元數據,如極光推送:
<meta-data
android:name="JPUSH_CHANNEL"
android:value="${JPUSH_CHANNEL}" />
<meta-data
android:name="JPUSH_APPKEY"
android:value="${JPUSH_APPKEY}" />
複製代碼
那麼除了在當前Module的build.gradle中定義JPUSH_CHANNEL和JPUSH_APPKEY:
manifestPlaceholders =[
JPUSH_PKGNAME : "com.sxu.library",
JPUSH_APPKEY : "appkey",
JPUSH_CHANNEL : "channel"
]
複製代碼
還須要在全部包含此Module的build.gradle中定義這兩個常量,不然會出現manifest合併錯誤。
在引用可切換運行模式的Module時,要根據isModule的值動態添加依賴,若是isModule爲true時,引用的模塊是以Application的形式存在,此時引入時就會報錯。
applicationId屬性在Application模式下才有效,因此在Module中設置時也須要添加isModule的條件判斷。
爲了便於對依賴和各類SDK版本的管理,最好將這些版本號定義在gradle.properties中統一管理,這樣便於後續的修改。
組件之間的通訊可以使用EventBus來實現, 可在每一個模塊中新建一個Event類,將同模塊中通訊須要的類都定義在這個Event類中。至於模塊間通訊須要的類,可定義在公共組件層的Event類中(雖然不是很合理,但暫未想到更好的方案)。
在Module的開發中,咱們可能須要引用ApplicationContext對象,但咱們沒有Context對象,沒法直接獲取到,此時可經過如下三種方式解決:
除了Common組件層外,其餘的層儘可能遵循 【同層不依賴,下層不依賴上層】的原則。
同層之間的依賴主要表如今業務層,這是不可避免的,但咱們須要避免互相引用的問題,在業務層,咱們可以使用隱式跳轉的方式或使用阿里開源的路由框架ARouter實現。
Gradle3.4中提供了新的依賴配置的關鍵字:
implementation:依賴項在編譯時對所在模塊可用,在運行時對依賴該模塊的模塊可用;
api: 依賴項在編譯時對所在模塊可用,在編譯時和運行時對依賴該模塊的模塊可用;
複製代碼
Gradle3.4中提供的兩個關鍵字至關於對是以前的compile關鍵字進行細化。使用Gradle3.4以前的版本,引入依賴項時都使用compile關鍵字,compile關鍵字容易引發屢次引入的問題。使用Gradle3.4以後的版本引入library時,對於外部不須要直接引用的library,最好使用implementation關鍵字,而對於外部須要引用的library, 可以使用api(此時就至關於compile)。
爲了不資源衝突的問題,咱們可在Module中的build.gradle配置資源名的前綴,一方面避免資源衝突,另外一方面,也便於標識資源所在的模塊:
android {
resourcePrefix "moduleName_"
}
複製代碼
Module中生成的資源Id不是final類型的,因此在onClick中不能使用switch語句塊,只能使用if...else if結構代替。
重構與版本迭代之間衝突是不可避免的問題。 一般重構的時候還須要版本迭代,此時可根據狀況進行人員分配:
重構是一項體力活,也是一項出力不討好的活,畢竟重構以後不可避免地會出現不少Bug, 若是用戶量龐大,那後果可能很嚴重,因此在重構時最好閱讀一下原來的代碼,認真梳理一下業務邏輯再進行。
在重構的過程當中對其中一個模塊嘗試使用了單Activity模式(頁面統一使用Fragment實現)。體驗感受不錯,值得一試。
模塊化/組件化是一種與技術無關的架構思想,合理的應用可大大下降項目的耦合度。爲了可以快速開發一款新的應用,現已開源了一個通用的APP框架SimpleProject,採用分層+模塊化的思想,詳見Android模塊化框架介紹。