最近公司一個項目使用了模塊化設計,本人蔘與其中的一個小模塊開發,可是總體的設計並非我架構設計的,開發半年有餘,在此記錄下來個人想法。java
爲何須要模塊化?android
當一個App用戶量增多,業務量增加之後,就會有不少開發工程師參與同一個項目,人員增長了,原先小團隊的開發方式已經不合適了。git
原先的一份代碼,如今須要多我的來維護,每一個人的代碼質量也不相同,在進行代碼Review的時候,也是比較困難的,同時也容易會產生代碼衝突的問題。github
同時隨着業務的增多,代碼變的愈來愈複雜,每一個模塊之間的代碼耦合變得愈來愈嚴重,解耦問題急需解決,同時編譯時間也會愈來愈長。web
人員增多,每一個業務的組件各自實現一套,致使同一個App的UI風格不同,技術實現也不同,團隊技術沒法獲得沉澱。json
在剛剛開始的時候,項目架構使用的是MVP模式,這也是最近幾年很流行的一個架構方式,下面是項目的原始設計。服務器
隨着業務的增多,咱們添加了Domain的概念,Domain從Data中獲取數據,Data可能會是Net,File,Cache各類IO等,而後項目架構變成了這樣。微信
再而後隨着人員增多,各類基礎組件也變的愈來愈多,業務也很複雜,業務與業務之間還有很強的耦合,就變成了這樣的。架構
使用模塊化技術之後,架構變成了這樣。app
這裏簡單介紹下Android項目實現模塊化須要使用的技術以及技術難點。
在開始開始進行模塊化以前,須要把各個業務單獨抽取成Android Library Module,這個是Android Studio自帶一個功能,能夠把依賴較少的,做爲基本組件的抽取成一個單獨模塊。
如圖所示,我把各個模塊單獨分爲一個獨立的項目。
在主項目中使用gradle添加代碼依賴。
在把代碼抽取到各個單獨的Library Module中,會遇到各類問題。最多見的就是R文件問題,Android開發中,各個資源文件都是放在res目錄中,在編譯過程當中,會生成R.java文件。R文件中包含有各個資源文件對應的id,這個id是靜態常量,可是在Library Module中,這個id不是靜態常量,那麼在開發時候就要避開這樣的問題。
舉個常見的例子,同一個方法處理多個view的點擊事件,有時候會使用switch(view.getId())這樣的方式,而後用case R.id.btnLogin這樣進行判斷,這時候就會出現問題,由於id不是常常常量,那麼這種方式就用不了。
一樣開發時候,用的最多的一個第三方庫就是ButterKnife,ButterKnife也是不能夠用的,在使用ButterKnife的時候,須要用到註解配置一個id來找到對應view,或者綁定對應的各類事件處理,可是註解中的各個字段的賦值也是須要靜態常量,那麼就不可以使用ButterKnife了。
解決方案有下面幾種:
1.從新一個Gradle插件,生成一個R2.java文件,這個文件中各個id都是靜態常量,這樣就能夠正常使用了。
2.使用Android系統提供的最原始的方式,直接用findViewById以及setOnClickListener方式。
3.設置項目支持Databinding,而後使用Binding中的對象,可是會增長很多方法數,同時Databinding也會有編譯問題和學習成本,可是這些也是小問題,我的覺的問題不大。
上面是主流的解決方法,我的推薦的使用優先級爲 3 > 2 > 1。
當把個模塊分開之後,每一個人就能夠單獨分組對應的模塊就好了,不過會有資源衝突問題,我的建議是對各個模塊的資源名字添加前綴,好比user模塊中的登陸界面佈局爲activity_login.xml,那麼能夠寫成這樣us_activity_login.xml。這樣就能夠避免資源衝突問題。同時Gradle也提供的一個字段resourcePrefix,確保各個資源名字正確,具體用法能夠參考官方文檔。
當完成了Library module後,代碼基本上已經很清晰了,跟咱們上面的最終架構已經很類似了,有了最基本的骨架,可是仍是沒有完成,由於仍是多我的操做同一個git倉庫,各個開發小夥伴仍是須要對同一個倉庫進行各類fork和pr。
隨着對代碼的分割,可是主項目app的依賴變多了,若是修改了lib中的代碼,那麼編譯時間是很恐怖的,大概統計了一下,原先在同一個模塊的時候,編譯時間大概須要2-3min,可是分開之後大概須要5-6min,這個是絕對沒法忍受的。
上面的第一問題,能夠這樣解決,把各個子module分別使用單獨的一個git倉庫,這樣每一個人也只須要關注本身須要的git倉庫便可,主倉庫使用git submodule的方式,分別依賴各個子模塊。
可是這樣仍是沒法解決編譯時間過長的問題,咱們把各個模塊也單獨打包,每次子模塊開發完成之後,發佈到maven倉庫中,而後在主項目中使用版本進行依賴。
舉個例子,好比進行某一版本迭代,這個版本叫1.0.0,那麼各個模塊的版本也叫一樣的版本,當版本完成測試發佈後,對各個模塊打對應版本的tag,而後就很清楚的瞭解各模塊的代碼分佈。
gradle依賴以下。
可能有人會問,既然各個模塊已經分開開發,那麼若是進行開發聯調,別急,這個問題暫時保留,後面會對這個問題後面再表。
當一個大項目拆成若干小項目時候,調用的姿式發生了少量改變。我這邊總結了App各個模塊之間的數據通訊幾種方式。
頁面跳轉,好比在訂單頁面下單時候,須要判斷用戶是否登陸,若是沒有則須要跳到登陸界面。
主動獲取數據,好比在下單時候,用戶已經登陸,下單須要傳遞用戶的基本信息。
被動得到數據,好比在切換用戶的時候,有時候須要更新數據,如訂單頁面,須要把原先用戶的購物車數據給清空。
再來看下App的架構。
第一個問題,原先的方式,直接指定某個頁面的ActivityClass,而後經過intent跳轉便可,可是在新的架構中,因爲shopping模塊不直接依賴user,那麼則不能使用原始的進行跳轉,咱們解決方式使用Router路由跳轉。
第二個問題,原先的方式有個專門的業務單利,好比UserManager,直接能夠調用便可,一樣因爲依賴發生了改變,不可以進行調用。解決方案是全部的須要的操做,定義成接口放在Service中。
第三個問題,原先的方式,能夠針對事件變化提供回調接口,當我須要監聽某個事件時候,設置回調便可。
如上分析,原先方式代碼以下。
可是使用Router後,調用方式改變了。
具體的原理是什麼,很簡單的,作一個簡單的映射匹配便可,把"app://user"與UserActivity.class配對,具體的就是定義一個Map,key是對應的Router字符,value是Activity的class。在跳轉時候從map中獲取對應的ActivityClass,而後在使用原始的方式。
可能有人的會問,要向另一個頁面傳遞參數怎麼辦,沒事咱們能夠在router後面直接添加參數,若是是一個複雜的對象那麼能夠把對象序列化成json字符串,而後再從對應的頁面經過反序列化的方式,獲得對應的對象。
例如:
注: 上面的router中json字符串是須要url編碼的,否則會有問題的,這裏只是作個示例。
除了使用Router進行跳轉外,我想了一下,能夠參考Retrofit方式,直接定義跳轉Java接口,若是須要傳遞額外參數,則以函數參數的方式定義。
這個Java接口是沒有實現類的,可使用動態代理方式,而後接下來的方式,和使用Router的方式同樣。
那麼這總兩種方式有什麼優缺點呢。
Router方式:
有點:不須要高難度的技術點,使用方便,直接使用字符串定義跳轉,能夠好的日後兼容
缺點:由於使用的是字符串配置,若是字符輸入字符,則很難發現bug,同時也很難知道某個參數對應的含義
仿Retrofit方式:
由於是Java接口定義,因此能夠很簡單找到對應的跳轉方法,參數定義也很明確,能夠直接寫在接口定義處,方便查閱。
一樣由於是Java接口定義,那麼若是須要擴展參數,只能從新定義新方法,這樣會出現多個方法重載,若是在原先接口上修改,對應的原先調用方也要作響應的修改,比較麻煩。
上面是兩種實現方式,若是有相應同窗要實現模塊化,能夠根據實際狀況作出選擇。
如上分析,若是須要從某個業務中獲取數據,咱們分別須要定義接口以及實現類,然在獲取的時候在經過反射來實例化對象。
下面是簡單的代碼示例
接口定義
實現類
反射生成對象
實際調用
本示例中每次調用都是用反射生成新的對象,實際應用中可能與IoC工具結合使用,好比Dagger2.
針對上面的第三個問題,原先設計的使用方式也是能夠的,只須要把回調接口定義到對應的service接口中,而後調用方就可使用。
可是我建議可使用另一個方式——EventBus,EventBus也是利用觀察者模式,對事件進行監聽,是設置回調更優雅方式的實現。
優勢:不須要定義不少個回調接口,只須要定義事件Class,而後經過Claas的惟一性來進行事件匹配。
缺點:須要定義不少額外的類來表示事件,同時也須要關注EventBus的生命週期,在不須要使用事件時候,須要註銷事件綁定,否則容易發生內存泄漏。
上面的介紹的各個模塊之間通訊,都運涉及到映射匹配問題,在此我總結了一下,主要涉及到一下三種方式。
Map register是這樣的,全局定義一個Map,各個模塊在初始化的時候,分別在初始化的時候註冊映射關係。
下面是簡單的代碼示例,好比咱們定義一個模塊生命週期,用於初始化各個模塊。
User模塊初始化
在Application中完成初始化
使用註解的方式配置映射信息,而後生成一個相似Database同樣的文件,而後Database文件中包含一個Map字段,Map中記錄各個映射信息。
首先須要定義個Annotation。
如:
須要實現一個 Annotation Process Tool,來解析本身定義的Annotation。
代碼略,此代碼有點複雜,暫時不貼了。
編譯產生的文件,大概以下所示。
而後利用反射找到Implement_$$_Database這個類,而後從方法中找到配對。
而後在須要配置的地方添加註解便可。
調用姿式。
注意點:
有時候,在生成最終的配置文件的時候,文件的名字是固定的,好比上面的Implement_$$Database,最終的路徑是這樣的cn.mycommons.implements.database.Implement$$_Database.java,而後經過編譯到apk中或則是aar中。
可是有個問題,若是各個子模塊都使用了這樣的插件,那麼每一個子模塊的就會有這個Implement_$$_Database.class,那麼就會編譯出錯。
由於aar中包含的時候class文件,不是java文件,不能在使用APT作處理了。下面有2中解決方案。
子工程的插件生成的文件包含必定的規則,好比包含模塊名字,如User_Implement_$$_Database.java,同時修改編譯過程,把java文件也打包到aar中,主工程的插件在編譯時候,提取aar中的文件,而後合併子工程的全部的代碼,這個思路是可行的,不過技術實現起來比較麻煩。
同一的方式相似,也是生成有必定規則的的文件,或者在特意package下生成class,這些class再經過接下來的所講的Gradle Transform方式,生成一個新的Database.class文件。
這是Android Gradle編譯提供的一個接口,能夠供開發自定義一些功能,而咱們就能夠根據這個功能生成映射匹配,這種方式和APT相似,APT是運行在代碼編譯時期,並且Transform是直接掃描class,而後再生成新的class,class中包含Map映射信息。修改class文件,使用的是javassist一個第三方庫。
下面簡單講述代碼實現,後面有機會單獨寫一篇文章講解。
首先定義一個註解,這個註解用於標註一個實現類的接口。
一個測試用的接口以及實現類。
定義一個靜態方法,用於獲取某個接口的實現類。
若是不使用任何黑科技,直接使用Java技術,那麼在定義時候須要主動的往CONFIG這個map中添加配置,可是這裏咱們利用transform,直接動態的添加。
定義一個ImplementsPlugin
gradle插件。
自定義的Transform實現。
代碼省略。。地址爲https://github.com/LiushuiXiaoxia/AndroidModular/tree/master/ImplementsTransformPlugin
優勢:
Map:簡單明瞭,很容易入手,不會對編譯時間產生任何影響,不會隨着Gradle版本的升級而受影響,代碼混淆時候不會有影響,無需配置混淆文件。
APT:使用簡單,使用註解配置,代碼優雅,原理是用代碼生成的方式生成新的文件。
Transform:使用簡單,使用註解配置,代碼優雅,原理是用代碼生成的方式生成新的文件,不過生成的文件的時期和APT不一樣,會編譯時間產生少量影響。
缺點:
Map:在須要新添加映射的時候,須要手動添加,否則不會生效,代碼不優雅。
APT:在編譯時期生成文件,會編譯時間產生少量影響,同時在不一樣的Gradle的版本中可能會產生錯誤或者兼容問題。須要配置混淆設置,否則會丟失文件。技術實現複雜,較難維護。
Transform:在編譯時期生成文件,會編譯時間產生少量影響,同時在不一樣的Gradle的版本中可能會產生錯誤或者兼容問題。須要配置混淆設置,否則會丟失文件。技術實現複雜,較難維護。
從技術複雜性以及維護性來看,Map > APT = Transform
從使用複雜性以及代碼優雅性來看,Transform > APT > Map
上面介紹了不少關於模塊化的概念以及技術難題,當模塊化完成之後,再進行完成開發時候仍是會遇到很多問題。不如原先代碼在一塊兒的時候很方便的進行代碼調試。可是進行模塊化之後,直接使用的是aar依賴,不能直接修改代碼,可使用下面技巧,能夠直接進行代碼調試。
在根目錄下面建立一個module目錄以及module.gradle文件,這個目錄和文件是git ignore的,而後把對應的模塊代碼clone到裏面,根目錄的setting.gradlew apply module.gradle文件,以下所示,若是須要源碼調試,則在module中添加對應的模塊。而後在app的依賴中去掉aar依賴,同時添加項目依賴便可。當不須要源碼調試好,再修改成到原先代碼便可。
module.gradle
好比調試shopping模塊
固然還有個更具技術挑戰性方案,使用gradle插件的形式,若是發現root項目中包含的模塊化的源碼,則不適用aar依賴,直接使用源碼依賴,固然這個想法是不錯的,不過具備技術挑戰性,同時有可能隨着Gradle版本的升級,編寫的gradle插件也要作相對於的兼容風險,這是隻是簡單提示一下。
上面講到的若是要調試代碼時候,須要完整的運行的整個項目,隨着項目的增大,編譯時間可能變得很長。
咱們能夠作一個簡單的,相似與主app模塊同樣,好比我是負責user模塊的開發者,那麼我只要調試我這個模塊就好了,若是須要其餘的模塊,我能夠簡單的作一個mock,不是把其餘的模塊直接依賴過來,這樣能夠作到調試做用。等到再須要完整項目調試時候,咱們在使用上面介紹的方式,這樣能夠節省很多開發時間。
還有一種實現調試的方式,好比上面的user模塊,目錄下面的build.gradle文件是這樣的
咱們能夠在gradle.properties中設置編譯變參數isLibModule,當須要完整調試好,設置爲isLibModule=false,這樣我這個子模塊就是一個apply plugin: 'com.android.application'這樣的模塊,是能夠單獨運行的一個項目
可能有時候仍是須要單獨的運行環境,android編譯方式有2中,一種是debug,一種是release。當打包成aar的時候,使用的是release方式,咱們能夠把須要調試的代碼所有放到debug中,這樣打包的時候就不會把調試的文件發佈到aar中。不過這種實現方式,須要對Android項目的目錄有較高的認識,才能夠熟練使用。
上面介紹的各個模塊須要單獨到獨立的git倉庫,同時打包到單獨的maven倉庫,當開發完成後,這時候就須要進行打包,但這個是一個簡單和重複的事情,因此咱們須要一個工具來完成這些事情,咱們能夠利用CI系統來搞定這件事情,這裏我推薦Jenkins,主流廠商使用jenkins做爲CI服務器這個方案。
具體的步驟就是,須要對每一個模塊的git倉庫作web hook,咱們公司使用的是git lab,能夠對git的各類操做作hook,好比push,merge,tag等。
當代碼發送了變化了,咱們能夠發送事件到CI服務器,CI服務器再對各個事件作處理,好比user模塊develop分支有代碼變化,這個變化多是merge,也有多是push。咱們能夠把主項目代碼和user項目的代碼單獨clone下拉,而後編譯一下,確認是否有編譯問題,若是有編譯經過,那麼在使用相關gradle命令發佈到maven倉庫中。
無論每次編譯結果怎樣,是成功仍是失敗,咱們都應該把結果回饋給開發者,常見的方式是郵件,不過這個信息郵件方式可能很頻繁,咱們建議使用slack。
模塊化架構主要思路就是分而治之,把依賴整理清楚,減小代碼冗餘和耦合,在把代碼抽取到各自的模塊後,瞭解各個模塊的通訊方式,以及可能發生的問題,規避問題或者解決問題。最後爲了開發和調試方便,開發一些周邊工具,幫助開發更好的完成任務。
做者:流水不腐小夏
地址:http://www.jianshu.com/p/910911172243
若是你以爲此文對您有所幫助,歡迎入羣 QQ交流羣 :644196190 微信公衆號:終端研發部