今天來回味下組件化和模塊化,這2種說法時一回事,當然還是有區別的,下面再詳細說,其實很簡單,只是設計范圍的不同,也都是約定俗成的東西。為了方便我下面都說組件化了
到現在組件化真的不是什么新鮮東西了,大公司都用的滾瓜爛熟,龍飛鳳舞了,也就是現在部分中型項目和小項目在組件化的路上努力。所以同志們,組件化沒玩過的,不熟悉的趕緊搞起來,說一點,你不會組件化,發布影子工程那么對你來說就是個噩夢。從本質上來講任何技術進步都是在現實需求的逼迫下抓耳撓腮,耗盡無數頭發才想出來的。哈哈,這里說個笑話罷了。所以呢組件化這個東西出來這么久了,頁發展了這么久了,用的人越來越多,那肯定是對我們顯示開發大有裨益的,下伙伴們不會,不熟悉抓緊啦,要不面試問你你怎么回答呢!
下面來正式說說組件化
組件化這個東西其實并不復雜,他就是種思路,本質上是一種 app 架構思路,說穿了很簡單的,難在組件化改造的時候,真正寫起代碼會出現不少棘手的問題,當然這些坑基本前人都趟完了,這里我主要時記錄下,要是你看到熟悉的部分,請不要罵我啊,畢竟都是前輩們的東西啊。
這里補充一下,組件化時一種 app 架構,他的發展也是沿著正常的技術發展脈絡來的,也是為了以追求高復用,高可維護性的目的的代碼封裝,區別是組件化是對整個 app 的再次封裝。
廢話了這么多,那么什么是組件化呢,各位看官想不要著急,在詳細說組件化之前,我們要搞懂2個概念,就是上面說的組件和模塊。
首先組件和模塊都不是官方規定的,都是這些技術發展下來大家約定俗成的概念,其實很簡單,一說就明白
模塊:android 中的模塊就是業務模塊,單指業務,是按照業務對 app 進行拆分,比如說訂單我們搞成一個模塊,個人中心我們搞成一個模塊,視頻,音頻這些都搞成模塊,在app中的體現就是 一個個module,module 的中文意思也是模塊,這不準這就是 google 對我們的暗示呢。模塊化的目的時為了搭積木,隨便拿幾個模塊module 出來就可以誰誰便便的上線一個 app,你還別說現在影子 app 的需求很旺盛,你去看看大公司的項目那個不是一堆影子工程,頭條還搞出一個頭條視頻的馬甲呢,這其實就是把視頻 module 拿出來,加上一個啟動頁。這樣的例子是比比皆是的,要不說不會組件化影子工程對你就是噩夢呢,哈哈,到時候維護那是想也別想了,代碼你要搞多少份啊。
組件:這個一樣簡單啊,說穿了就是我們平時干的事,對功能的封裝,這就是組件,一個功能就是一個組件,IO,數據庫,網絡等等這些功能都是組件,這么說你就明白了吧。既然這樣那為毛線我們還要搞出來這個一個組件的概念,當然了任何事都是有其意義的,因為組件對功能代碼的封裝有個很高了明確的要求:一處封裝,處處使用。要我們把維護性,復用性,擴展性,性能做到極致,因為這樣才能真正做到一處封裝,處處使用。當然組件的范圍現在也是覆蓋的很廣的,app 中的一切都是組件,基本上我們分為:基礎功能組件,通用UI組件,基礎業務組件。
以上我談了下我自己對于模塊化,組件化的理解,是目前開發中對于模塊和組件的理解。在模塊化和組件化的發展中概念也是有些調整變化的,大家只要看現在時什么樣子就好了,深入學習的話有興趣可以看看組件化,模塊化的發展歷程。
我認為 Android 模塊化探索與實踐 對于模塊化,組件化概念的解釋時最優秀的。
組件化和模塊化在現在看是一回事了,如果把一個項目看成是袋中的組合的話,那么模塊就是體積最大的哪些袋子,組件就是體積小的袋子,大的袋子是最直接可被外接觀測和接觸的袋子,大的袋子也是用小的袋子組成的,一個不太恰當的比喻吧,模塊和組件就是這樣的關系,是我們對業務和功能拆分,封裝的理解。
好了正式開始介紹了組件化啦
組件化在工程表現上就是我們把 app 按照其業務的不同,劃分為不同的 module模塊,把各種功能封裝成一個個 library,module 之間時嚴格禁止橫向依賴的,要不怎么單獨使用呢,我不能為了用一個 module,把相關的module 都帶上吧,要是這么 module 還有依賴的module 呢,這樣談復用性就是扯淡了。
主 app 就是我們常說的殼工程依賴這些 module,library 由需求的 module 依賴,但是要考慮library 版本的問題,隨著業務和功能的擴展,library 的數量也是巨大的,微信在組件化拆分時據說拆分出80多個 module,可見 library 也是少不了的。
module 和 library 多數時候我們時提供arr 和 jar 來給殼工程引用的,arr 和 jar 在編譯時是不會再編譯的,只會檢查版本,保留一個最新的版本,既提高了 app 的編譯速度,頁提供一種資源沖突解決方式。
下面我方一些圖來描述一下組件化,大伙仔細看看,圖比文字可生動多了
項目如何組件化:
組件化核心:router##
我們在抽象 module 時,module 之間是沒有相互依賴的,是嚴格解耦的,為了達到我們復用的目的。module 之間不能相互依賴,就沒法調用別的 module 的代碼了,那么面對業務之間的頁面相互調起,相互通信這些常見的需求我們該怎么辦,沒錯就是大伙在上面的圖里面看見的東西 router。
router 是我們統一制定的模塊間通訊協議,router 中我們主要是處理一下幾個問題:
- 模塊之間頁面跳轉
- 模塊之間數據傳遞
- 模塊初始化處理
router 這東西有現成的,你也可以自己封裝。使用的思路都是把 router 作為一個組件,所有的業務 module 都依賴這個 router 組件,當然殼app 也是,然后我們把需要的模塊間頁面跳轉,數據傳遞,初始化都注冊到 router 中,這里面就體現到我們定義的統一,通用的模塊通訊協議的重要性了,router 維護多個集合保存這里關系,然后我們通過router 就可以實現模塊間的通訊了。
router 的封裝還是挺麻煩的,要寫好了不容易,現在用的比較多的有:
- 阿里的 ARouter
- 最早出現的 ActivityRouter
- spiny同學的router這是我的最愛,目前不維護了,思路很棒,并且考慮到了進程化的問題,可惜沒有使用 APT 注解技術
- 練手的 router
上面我介紹了幾個 router 路由,基本上不論時自己寫還是用現成的,router 基本上都是上面這幾個的樣子了,當然了現在好的 router 還是要使用 APT注解技術來動態去 router 注冊模塊方法,自己寫代碼去注冊的話使用很使用,有些問題不好處理,比如 router 的靜態實例要是被回收了,你再 new 一個出來,那么模塊注冊的方法怎么辦,寫起來太麻煩,還不如 APT 注解來的方便,擴展性也好。這里有個ToyBricks_Android項目模塊化解決方案 可以解決 APT不能掃描 arr 包的問題。
最后說一下,module 間的通訊其實可以分成3種:
- 頁面調起
- 某種事件的通知
- 直接調用某些模塊的業務方法
頁面調起現在的 router 都可以很好的完成這個任務。
某些事件的通知,比如我切換城市了,通知某些頁面去顯示或是刷新數據,這個根據業務來說影響的范圍會很廣的,會影響多個業務的,因為 module 的復用性,我們在 module 中是不能確定會具體影響哪些業務module 的,那么這種場景使用 eventbus/廣播比較合適了。
直接調用默寫模塊的業務方法,這屬性業務模塊間在業務上的強耦合了,這個碰到產品這么設計你也沒辦法,一般碰到這樣的場景也是會保證相關的業務module 都是會加載的,所以呢在定義 router 靈活一些,可以做到調用指定module 的某些方法
找到另一個說法,我很喜歡,和我的理念也很接近
出自:Android 架構設計:MVC、MVP、MVVM和組件化
所謂的組件化,通俗理解就是將一個工程分成各個模塊,各個模塊之間相互解耦,可以獨立開發并編譯成一個獨立的 APP 進行調試,然后又可以將各個模塊組合起來整體構成一個完整的 APP。它的好處是當工程比較大的時候,便于各個開發者之間分工協作、同步開發;被分割出來的模塊又可以在項目之間共享,從而達到復用的目的。組件化有諸多好處,尤其適用于比較大型的項目。
各個模塊之間如何進行數據共享和數據通信?我們可以把需要共享的數據劃分成一個單獨的模塊來放置公共數據。各個模塊之間的數據通信,我們可以使用阿里的 ARouter 進行頁面的跳轉,使用封裝之后的 RxJava 作為 EventBus 進行全局的數據通信。
router 我不想說太多,也說不好,這部分大伙看我最后的鏈接吧,或是看看上面4個路由也可以,不論如何實現,router 是為了給 module 模塊搭建一個通訊的中間平臺,目的就是這樣。仔細的大家多看吧,這里我也是看別人的。
我再逼逼一下,module 和 library 我們盡量不要提供源代碼的方式提供依賴,這不符合我們復用的目的,到時候你發布幾個影子功能或是別的 app,那么你使用源代碼依賴方式的 module 和 library 你怎么提供維護,所以盡量使用 arr 和 jar 的方式。我們可以把一些定位相近library 打包成一個 module 也是不錯的。
我們一定要熟悉gradle的使用,在組件化中我們會大量的使用 gradle 提供各種資源加載的配置和環境配置
組件化最核心的目的就是代碼的高可復用和高可維護和高可擴展性能,其他的優點都是屬于連帶性質的,我們要先把握住核心點學習,其他的都不是主要,有時間再看
組件化碰到的問題
1. 子模塊單獨編譯測試
在做組件化開發時,我們測試 module 庫都是把 module 單獨達成 apk 文件,在發布module時 提供 library 供外界依賴,這都是通過配置 module 的 gradle 的編譯模式實現的
首先在子模塊build.gradle中定義常量,來標示模塊目前是否處于開發模式
def isDebug = true
在子模塊的build.gradle中進行模式配置。debug模式下編譯成獨立app,release模式下編譯成library。
if (isDebug.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
兩種模式下模塊AndroidManifest.xml文件是有差別的。作為獨立運行的app,有自己的Application,要加Launcher的入口intent,作為library不需要。這個問題很好解決,寫兩個不同的AndroidManifest.xml即可,并在gradle中進行配置。
在 gradle 腳本中配置
android {
sourceSets {
main {
if(isDebug.toBoolean()) {
manifest.srcFile 'src/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/release/AndroidManifest.xml'
}
}
}
}
2. sdk和第三方庫的版本一致性
不同module依賴sdk版本不一致,會因兼容性問題導致編譯問題。
不同module引用了同一個第三方庫的不同版本,并且這個庫沒有做到向前兼容,就有可能出現方法找不到、參數不對應等問題。
所以有必要統一整個project的依賴版本。
在最外層build.gradle中定義的常量能被整個project的build.gradle文件引用,統一的版本定義可以放在這里。
ext {
android_compileSdkVersion = 25
android_buildToolsVersion = '25.0.2'
android_minSdkVersion = 21
android_targetSdkVersion = 25
lib_appcompat = 'com.android.support:appcompat-v7:25.1.1'
lib_picasso = 'com.squareup.picasso:picasso:2.5.2'
lib_gson = 'com.google.code.gson:gson:2.6.1'
}
我沒試過 arr資源的 module 是否還可以使用這種方式
3. 資源id沖突
android 中 module的資源文件最后都是會合并到主項目中的,資源文件的 id 最終和 moudle 是的 id 時不一樣的,所以這就會出現資源重名的問題,解決這個問題,我們的做法就是module 資源加一個統一的前綴
andorid{
...
buildTypes{
...
}
resourcePrefix "moudle_prefix"
}
但是注意 res 文件夾下的文件可以用 gradle 腳本加前綴,但是圖片資源不行,圖片資源我們還是需要在命名時自己添加前綴
4. application初始化的問題
子模塊作為application時,有一些初始化的工作需要在Application.onCreate時進行。而作為library時,調不到這個onCreate。所以自己寫一個靜態方法,供主工程的Application調用。
public class ApplicationA extends Application {
@Override public void onCreate() {
super.onCreate();
//給底層library設置context
AppContext.init(getApplicationContext());
}
/**
* 作為library時需要初始化的內容
*/
public static void onCreateAsLibrary() {
//給FunctionBus傳入接口的實例
FunctionBus.setFunction(new FunctionA() {
@Override public String getData(String key) {
return "xixi";
}
});
}
}
主工程的Application onCreate時記得初始化子模塊。
public class MainApplication extends Application {
@Override public void onCreate() {
super.onCreate();
AppContext.init(getApplicationContext());
ApplicationA.onCreateAsLibrary();
ApplicationB.onCreateAsLibrary();
}
}
除了提供方法在殼工程里面調用,還可以結合使用了 APT 技術的 router 來做,使用注解,就不用我們自己去調用了,徹底解耦
5. library依賴問題
先說一個問題,在組件化工程模型圖中,多媒體組件和Common組件都依賴了日志組件,而A業務組件有同時依賴了多媒體組件和Common組件,這時候就會有人問,你這樣搞豈不是日志組件要被重復依賴了,而且Common組件也被每一個業務組件依賴了,這樣不出問題嗎?
其實大家完全沒有必要擔心這個問題,如果真有重復依賴的問題,在你編譯打包的時候就會報錯,如果你還是不相信的話可以反編譯下最后打包出來的APP,看看里面的代碼你就知道了。組件只是我們在代碼開發階段中為了方便叫的一個術語,在組件被打包進APP的時候是沒有這個概念的,這些組件最后都會被打包成arr包,然后被app殼工程所依賴,在構建APP的過程中Gradle會自動將重復的arr包排除,APP中也就不會存在相同的代碼了;
但是雖然組件是不會重復了,但是我們還是要考慮另一個情況,我們在build.gradle中compile的第三方庫,例如AndroidSupport庫經常會被一些開源的控件所依賴,而我們自己一定也會compile AndroidSupport庫 ,這就會造成第三方包和我們自己的包存在重復加載,解決辦法就是找出那個多出來的庫,并將多出來的庫給排除掉,而且Gradle也是支持這樣做的,分別有兩種方式:根據組件名排除或者根據包名排除,下面以排除support-v4庫為例:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") {
exlude module: 'support-v4'//根據組件名排除
exlude group: 'android.support.v4'//根據包名排除
}
}
library重復依賴的問題算是都解決了,但是我們在開發項目的時候會依賴很多開源庫,而這些庫每個組件都需要用到,要是每個組件都去依賴一遍也是很麻煩的,尤其是給這些庫升級的時候,為了方便我們統一管理第三方庫,我們將給給整個工程提供統一的依賴第三方庫的入口,前面介紹的Common庫的作用之一就是統一依賴開源庫,因為其他業務組件都依賴了Common庫,所以這些業務組件也就間接依賴了Common所依賴的開源庫。
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
//Android Support
compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion"
compile "com.android.support:design:$rootProject.supportLibraryVersion"
compile "com.android.support:percent:$rootProject.supportLibraryVersion"
//網絡請求相關
compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"
compile "com.squareup.retrofit2:retrofit-mock:$rootProject.retrofitVersion"
compile "com.github.franmontiel:PersistentCookieJar:$rootProject.cookieVersion"
//穩定的
compile "com.github.bumptech.glide:glide:$rootProject.glideVersion"
compile "com.orhanobut:logger:$rootProject.loggerVersion"
compile "org.greenrobot:eventbus:$rootProject.eventbusVersion"
compile "com.google.code.gson:gson:$rootProject.gsonVersion"
compile "com.github.chrisbanes:PhotoView:$rootProject.photoViewVersion"
compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion"
compile "com.github.GrenderG:Toasty:$rootProject.toastyVersion"
//router
compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion"
}
6. module不同業務環境使用不同代碼
我們做項目至少會有測試和線上2套環境吧,組件化讓我們開始重視 gradle,通過 gradle 配置我們可以減少很多代碼的書寫的,切換環境我們也是可以用 gradle 實現的,在不通過的環境下注冊不同的代碼文件,看下面這張圖
我們有 debug 和 release2個環境,里面放的是不同環境執行的代碼,main 里面是跟環境切換無關的代碼部分,我我們這樣設置 gradle 就可以了
android {
// ...
sourceSets {
debug {
java.srcDirs = ['src/main/java', 'src/debug/java']
}
release {
java.srcDirs = ['src/main/java', 'src/release/java']
}
}
}
此外在發布該 library 時,需要指定一些設置,如下:
android {
// ...
defaultConfig {
// ...
defaultPublishConfig 'release'
publishNonDefault true
}
}
說明:
- defaultPublishConfig 'release',默認 library 只會生產 release 下的版本,此版本將會被所有項目使用,通過defaultPublishConfig可以控制默認生產哪個版本的庫。
- publishNonDefault true,默認情況下不能生產所有版本的 library,通過設置publishNonDefault為true,可以同時生產所有版本的 library。
業務組件 module 依賴不同的基礎組件生產的 library,如下:
dependencies {
// ...
debugCompile project(path: ':baselibrary', configuration: "debug")
releaseCompile project(path: ':baselibrary', configuration: "release")
}
在使用通過這樣的配置腳本解決了多個 APK 包依賴同一組件生產的不同的 library,最終得到我們需要的開發/測試/生產 APK 包。
合并多個 module 到一個文件夾
studio 中的 module 我們在引用時都是用,項目名 + :冒號來表示的
implementation project(':basecomponents')
注意這只是表示我們要引用這個名字的 module 了,而這個 module 的地址這里我們不管
那么就可以理解為 module 的地址可以隨我們任意配置,那么在哪里配置 module 的地址呢,答案就是在 setting.gradle 文件里,我們給 ':basecomponents' 這個 module 指定他的地址就行
比如我們想新建一個名字為 components 的文件夾存放我們的組件 module ,組件我們給2個 ( basecomponents,aaa ),然后我們在 setting.gradle 里指定每個項目的文件路徑
include ':app', ':basecomponents', ':aaa'
project(':basecomponents').projectDir = new File( 'components/basecomponents' )
project(':aaa').projectDir = new File( 'components/aaa' )
一個好的組件化文檔是必須的
組件化是 android 開發步入新時代的未來,是代碼膨脹,支持快速開發的必然,一個好的組件化文檔在現今來看也是必須的了
下面貼個圖
組件化的坑
組件化是好,但是坑也是不少,不好填,尤其是 databinding,dagger,bufferkinft,這是源于 studio 編譯的問題。
studio 中 module 雖然時在代碼上獨立于殼工程的,但是在編譯時最后還是要合并到殼工程中的,要不怎么達成一個 APK 文件,要是多個 APK 文件把不成了插件化了嘛,插件化坑更多啊。合并 module 到殼工程就會產生一個根本問題,module 的 R 文件數值改變了。module 的 R文件數據不是固定的,只有殼工程的 R 文件才是常量值,時不變的,module 的 R 文件數值在把 modul 的資源合并到殼工程后才會確定下來,那么這就對依靠編譯時注解的技術造成了難題,你指定的 R 路徑最后找不到,并且據說這里面還涉及注解的 final ,不了解,看到有人這么說,所以大家在開發組件化時對于帶注解技術的框架要多注意,有坑要多看才能爬過去
組件化文章:
Android 模塊化探索與實踐 安居客的時間案例,對概念的解釋非常到位,適合初學者和概念混亂的同學看,特別是有提供詳細的 demo 哦
關于Android模塊化我有一些話不知當講不 這是我目前看到的最貼近實際的好文了,絕對值得一看的
Android組件化方案這篇文章講的最好,還設計大量 gradle 配置的思考
ToyBricks_Android項目模塊化解決方案使用 apt 注解技術解耦
一次 Android 項目組件化 介紹了不少實際 gradle 配置使用
android組件化方案,讓團隊開發更有效率借鑒 retrofit 的方式聲明
router 接口,時個思路,但是沒有達到徹底解耦,可以參考下。蘑菇街 App 的組件化之路這和講 IOS 的
優秀的組件化方案
優秀的router 路由器設計
gradle學習文章
學習 gradwle 的入門系列,翻譯自官方文檔,適合入口看,學習各種概念
- Gradle for Android 第一篇( 從 Gradle 和 AS 開始 )
- Gradle for Android 第二篇( Build.gradle入門 )
- Gradle for Android 第三篇( 依賴管理 )
- Gradle for Android 第四篇( 構建變體 )
- Gradle for Android 第五篇( 多模塊構建 )
- Gradle for Android 第六篇( 測試)
- Gradle for Android 第七篇( Groovy入門 )
gradle 各種常用使用
- Gradle 使用配置總結
- android gradle使用詳解
- 關于Gradle配置的小結
- android gradle自動化打包方案完整總結
- Android Gradle 常用使用場景實現方式的總結
- Android Gradle高級用法,動態編譯技術:Plugin Transform Javassist操作Class文件
飛雪無情的 gradle 教程
吳小龍的gradle 的教程
- Gradle for Android(一)基本配置、依賴管理
- Gradle for Android(二)全局設置、自定義BuildConfig、混淆
- Gradle for Android(三)多渠道打包、配置簽名信息
其他