美團貓眼電影Android模塊化實戰總結

1 寫這篇博客的初衷前端

首先一句話歸納:我想把這幾個月作的事情記錄下來,而且但願儘可能詳細,但願讀者讀了這篇文章可以知道項目進行模塊化,項目改業務框架可能會遇到哪些問題,具體每一個步驟都作什麼,而不是大體的瞭解。java

如今不少人都在談模塊化,網上有一大堆的博客實踐都在講這個。不少談的只是模塊與模塊之間的解耦,而且大部分講的是經過router路由進行解耦,其餘談的很少,並且不乏泛泛而談。但將一個app真正作到解耦,運行。須要解決的事情遠遠不止解耦。業務架構、進程間通訊、資源等處理、解耦方式等都須要解決。剛好對於貓眼模塊化整個過程的實施,從頭至尾,分析解決各類問題,我陸陸續續的作了幾個月。貓眼app的歷史版本是一個耦合度很高的一個工程。從這樣的一個歷史版本到最終的各個業務模塊可以獨立運行而且可以作進程間通訊,會涉及到各個方面的解耦和一些其餘東西。我今天我就以該app爲例(其餘的app進行解耦可能會遇到不一樣的問題,這點注意一下),完整的講下貓眼模塊化的整個過程。每個方面沒有照搬網絡的一些作法,而是分析對比,採用更好的設計方式。好比解耦使用serviceloader,而不是路由進行;好比架構使用更適合咱們業務的一種帶生命週期的mvp變種。我還會說下具體的花費時間和一些經驗,這樣你們之後作模塊時也心中有數。(提示一下,其實模塊化過程所涉及的東西除了文章說起的還有不少。有些未說起,是由於以前已經完成,好比網絡庫的緩存由數據庫->文本,這點讀者注意一下。若是還有遺漏的地方,能夠交流~)。android

主要內容:serviceloader解耦,mvp變種框架,模塊通訊,lib獨立運行,多端複用。sql

2 爲何作模塊化數據庫

首先要說一點:作模塊化不是爲了炫技。若是沒有業務場景需求,不建議作。
爲何要作模塊化,網上已經闡述了不少緣由了。這裏我簡單說下貓眼爲何要作:api

  • 貓眼須要快速移植到其餘app(美團,點評…)。
  • 解耦首頁,減小冷啓動時間。
  • 開發時減小build時間,代碼責任制。
  • 服務快捷替換

3 解耦到什麼程度?緩存

首先說下,模塊化到底是什麼呢?這個你們確定都耳熟能詳了:可以將不一樣的業務分離成不一樣的lib module。那麼作完模塊化,咱們的某個業務lib 具備哪些功能呢?我認爲是:bash

總結一句話:無溝通成本,快速,傻瓜式的在任何app上運行。具體就是:這個lib不耦合具體app的服務,不耦合具體app的activity。只要給我一個app(或假的app殼子),經過它的baseActivity,和他們的服務,我就能夠很是快速的將這個lib在那個app上運行。停!你可能會說這個服務是什麼東西,讓我詳細的說下吧~微信

3.1 能夠無侵入式的配置各類服務
咱們知道每一個app都會提供帳戶信息,設備信息,網絡服務,圖片加載服務,打點服務,下拉刷新樣式,錯誤狀態等。每個app的這些服務可能都不同,好比美團使用的網絡服務是okhttp,而點評使用的是長鏈接。因此咱們的業務邏輯lib不能耦合這些具體的服務。只能耦合服務抽象而來的接口。在具體app使用的時候,咱們再把app的服務提供給這個lib。那麼這些服務怎麼給呢?若是當須要服務時,我都留了一個傳參的口子,這樣我就須要把app的服務一個個塞到lib中須要的地方。這樣成本太大了。我不但願這麼麻煩,我但願的方式是直接把服務實現做爲txt文本放在app的某個文件夾,你這個lib就能給我運行。這樣我幾乎不用管lib裏面是什麼東西。你只要給我一個業務lib,我添加一個txt文本,就能運行了。網絡

3.2 lib快速便捷多端使用
說下不耦合activity。咱們知道每一個app有本身的baseAtivity,在裏面作統計,處理異常、某些庫的初始化等功能。除此以外,每一個app的actionBar也不同,每一個頁面在不用的app中manifest的schema也不同。因此在lib中的業務,若是是一個業務,咱們不能直接寫成Activity,而應該是一個view/fragment,這樣對於任何一個app,咱們直接新建一個activty,而後把lib中的頁面放到那個activity中便可。一樣,考慮的是協同合做的成本問題,我不但願在放這個頁面的時候,我須要處理不少其餘的東西,好比數據加載。我但願你給我這個業務頁面pager(實際上是一個view),我放到Activity onCreate的setContentView中便可,它就能運行。別讓我作其餘處理生命週期,數據綁定,銷燬等的事,那都是你pager內部須要作的。

3.3 demo示例
前面這兩點說的可能雲裏霧裏的。最近我寫一個貓眼問答需求,涉及5個頁面,因此作成了一個lib。那麼就結合我最近寫的lib來圖文闡述一下。

這個lib就是問答的業務lib。這不耦合具體的服務,只耦合服務接口。裏面的頁面(page包下)不是activity,而是view。那麼,這時候另一個同事想把這個lib用到貓眼app上。怎麼作呢?

在貓眼app的build.gradle下添加這個lib的依賴:

compile 'com.maoyan.android.business:movie:1.0.2.3'
複製代碼

在貓眼宿主app中添加一個lib須要的服務配置:服務實現txt文本(由於是宿主app,以前其實已經存在)。

txt裏面是:

在宿主app中建立activity,並放置lib中的頁面,填寫manifest,好比(可能有時候須要在裏面寫入actionBar的交互邏輯)

這就完成了,就運行了。所使用的各類服務,下拉刷新等都是這個app提供的。是否是快速、無需溝通寫做、傻瓜。若是咱們想測試這個app,那麼也很簡單。隨便建一個app殼子,新建activity,把lib中的頁面page放進去。而後添加所須要的服務實現txt文本(由於是測試,因此服務實現能夠自由一些,可隨意配置),就大功告成了。這種方式來修bug調ui,比啓動宿主app修改代碼節省不少時間。咱們看下我隨意寫了一個app來測試lib:

咱們能夠看到下拉刷新,狀態服務等和貓眼app中的都不同,均可以定製。若是都這麼寫,其實全部的模塊咱們均可以快速,傻瓜,可定製的作成app這種解耦程度是否是更好呢

若是感受還不錯的話,那麼咱們開始工做吧~

4 開始模塊化之旅

4.1 原項目耦合結構
開始模塊化工做,我首先得給你們呈現下以前未模塊時高度耦合的貓眼app。咱們這裏以電影詳情頁爲例,看看他的耦合狀況:

電影詳情頁是創建在一層層的基類之上,這些基類耦合了具體的網絡加載等各類服務。由於詳情頁有想看、評分、點贊等可編輯狀態,因此還耦合了greendao數據庫(之前網絡加載也耦合了這個數據庫,後來換成了retrofit+rxjava,因此替換到了這層耦合,謝天謝地)。該頁面由於須要和其餘頁面互動(好比跳轉、評分同步等),因此也同時耦合了其餘頁面的類。除此以外,還有utils,view,model等。若是想把電影詳情頁抽離出來,這些全部的耦合都要剝離。具體須要解決的問題,以下:

4.2 準備工做

4.2.1 工做量評估
首先咱們說下解耦時須要作的準備工做。由於這些工做是解耦拆分的基礎。有兩點須要作,以下所示:

首先說明一下,並非我喜歡打五顆星。確實是這部分工做量比較大~~~

4.2.2 公共資源,model,utils等的拆分
4.2.2.1 耦合狀況示例
第一點是公共資源,model,utils等的拆分。這些事情雖然不用考慮太多事情,可是很繁瑣。在作模塊化的時候,這個地方耗費了很多時間。很大一部分緣由是,以前的貓眼歷史版本代碼不夠規範,對代碼耦合這些事情不夠敏感。舉幾個例子吧:

  • 咱們以前的utils基本都寫在一個類MovieUtils裏面了。這個類就像大染缸。什麼都向裏面放。在傳入的參數方面也不夠規範,甚至MaoyanBaseFragment這種業務代碼都做爲參數傳入。致使這個東西及其難拆。
  • utils的方法不傳context。前人寫的時候圖省事,在項目中統一加了一個靜態的context,致使幾乎全部的utils都沒有傳入context,這樣的後果是這些工具方法直接以來宿主app。
  • 以前寫的common view 不夠獨立。既然想寫common view,那麼就儘可能讓這個view可以獨立,不要耦合其餘第三方庫,儘可能使用android 官方庫。

4.2.2.2 資源拆分經驗
對於資源的拆分,實際上是很是繁瑣的。尤爲是若是string, color,dimens這些資源分佈在了代碼的各個角落,一個個去拆,很是繁瑣。其實大可沒必要這麼作。由於android在build時,會進行資源的merge和shrink。res/values下的各個文件(styles.xml需注意)最後都只會把用到的放到intermediate/res/merged/…/valus.xml,無用的都會自動刪除。而且最後咱們可使用lint來自動刪除。因此這個地方不要耗費太多的時間。剛纔說了,styles.xml需注意。那麼須要注意什麼呢?這個東西是這麼寫的:

咱們在寫屬性名字的時候,必定要加上前綴限定詞。若是不加的話,你這個lib打包成aar後給其餘app使用的時候,會出現屬性名名字重複的衝突,爲何呢?由於BezelImageView這個名字根本不會出如今intermediate/res/merged/…/valus.xml裏, 因此不要覺得這是屬性的限定詞!

4.2.3 集成式vs組合式(選作)
前面說了資源utils等的拆分,那麼接下來講下第二點,基類的處理。咱們看到電影詳情頁是創建在一堆的基類之上。每一層的基類都作了一些事情。(當時這麼寫是爲了頁面的快速開發)若是咱們想將電影詳情頁獨立出來,就須要把這些基類打包成一個aar,下沉到基礎庫,供全部頁面使用。可是咱們之前的這種基類耦合了不少貓眼的東西,像下拉刷新,頁面狀態什麼的都是寫死的,而且若是我須要寫個頁面,我就須要繼承那麼一大堆的fragment。固然這種改一改也能夠移植。但對之後的代碼迭代確定是很差的(修改,添加業務)。由於它靈活性差。好比若是點評app上須要貓眼某個頁面的一部分而不是整個頁面,原來那種改起來就不是很方便。我但願的方式是這些頁面都是view,而不是fragment。而且也不是這種繼承方式,而是組合方式。即若是我想要一個帶下拉刷新的列表view,那麼我直接build出來這麼一個view,須要什麼配置就set進來,它就可以使用。這個view你能夠放到任何一個view,fragment中和其餘view進行組合。即:

這個MovieRcPagePullToRefreshStatusBlock是一個view,能夠用在任何頁面進行view的組合。

4.2.3.1 組件的插拔式,組合式設計
其實個人作法更大膽,或者更「懶」一些。我但願我這個MovieRcPagePullToRefreshStatusBlock build成功之後,放到頁面中就能顯示運行,自動加載數據了。就像小時候玩積木那樣,組件與組件都是插拔即用式的。至於這個block是怎麼加載數據的,使用者無需關係。使用者只須要拿到這個block,而後build時set進去須要的東西。放到頁面中就能夠運行了。能夠參考這個做爲示例:

咱們能夠看到這個頁面,我只是build出來了兩個view,而後放到這個page中,並無關心數據加載什麼的,數據加載是在這個block內部完成的。而後這個page就像前面說的那樣,放到某個app的activty中就能夠運行了。插拔式、傻瓜式的思想,可能我這我的比較「懶」~~

那這種架構怎麼實現呢?,接下來粗略的看看這種框架大致的實現思路吧(具體的能夠看下我寫的這一篇android 官方mvp框架優化:lifecycle-mvp,像前端那樣組合式寫頁面)。其實這個框架大致也是mvp框架的思想,不過同時解決了業務場景的一些問題,好比,生命週期,移植性,溝通成本,使用方便與否等。既然要說下實現思路,那麼從開始提及,對本身是個總結,對讀者們可能有有些許幫助。先說下mvp框架的含義:

4.2.3.2 mvp框架含義
mvp框架整體來講適用於android的場景需求。m表明model,提供數據;v即view,提供的是供presenter調用的view相關的方法;p 即presenter,提供的是頁面裏觸發動做的邏輯方法。

4.2.3.3 官方mvp框架的缺點
mvp框架網上有不少,官方也推薦了mvp框架。和通常的區別是:用contract來承載view和presenter的接口定義。用fragment來實現view接口。不過官方使用fragment來實現view,也有它的無奈。爲何說它無奈呢?對於view層的接口,使用fragment來進行實現,主要是由於fragment有生命週期。但fragment太笨重了。試想一下,我有一個頁面,裏面有四五塊內容。爲了之後的各塊內容的移動、去除、移植更方面,我但願每一塊內容都作成mvp形式,塊與塊之間不耦合。那麼官方的這個mvp框架就不適用了。由於你不可能在一個頁面寫5個fragment把。android的activity中不建議寫那麼多的fragment,fragment典型的使用場景是ViewPager。

4.2.3.4 常規變通
那麼變通一下,5塊內容的view層,再也不用fragment實現,而只是一個個普通的view,每一個view監聽事件的響應仍是在view中進行(調用各自的presenter方法)。而對於整個頁面的初始化加載或者下拉刷新加載,這5塊內容共用一個fragment,在這個fragment的onStart()和下拉刷新的監聽回調中加載5塊內容對應的presenter的方法。而後在fragment的onCreateView()中把5塊內容的view填充進來。5塊內容之間可能還須要通訊,數據交流,這些藉助presenter在fragment中進行。

4.2.3.5 帶生命週期的mvp:lifecycle-mvp
上面那麼作徹底沒有問題,而且上面那種作法也存在於咱們的項目中。但經過幾個版本的迭代,我發現了一些問題:presenter太亂,太散。fragment須要持有全部的presenter,在onStart()時load()數據。各自的view也須要持有各自的presenter。而且view和presenter之間須要互相set()。你還須要在activty或者fragment的onDetroy()方法中管理presenter。整體讓人以爲很亂。尤爲是若是你的組件須要被別人使用,或組件用須要用到其餘app時,其餘人拿到你的組件,你要關心兩個東西view和presenter,他得知道這兩個東西里面的方法,而且他須要在activty/fragment的生命週期中關聯他們並調用一些方法。嗯。這個過程確定存在的大量溝通成本~
因此纔想到了前面講的那種build方式來實例化組件,而後用pager組合組件。特色是(具體能夠看下android 官方mvp框架優化:lifecycle-mvp,像前端那樣組合式寫頁面):

  • 使用lifecycle-component這個組件提供生命週期。
  • presenter被view層內部持有,不向外暴露。
  • build建立view實例時,提供TypeFactory,用於業務的擴展。
  • 業務代碼分層。
  • 用這種mvp的變種框架改寫項目的原代/寫新業務,就可使頁面更容易移植、拓展,頁面內的模塊也能夠移動改變。固然,這種框架是創建在咱們的業務基礎之上,框架仍是須要因項目而已,沒有最好,只有更適合~

4.3 接口的抽離

前面已經闡述了模塊化的準備工做,接下來咱們須要作什麼呢?根據前面介紹的原項目耦合結構,咱們知道咱們之前的項目直接依賴了各類service的具體實現。咱們接下來要作的是把這些具體service實現用接口來剝離:

4.3.1 使用servieloader進行解耦—非顯式的調用服務實現類
4.3.1.1 官方serviceloader
從圖上能夠看到,咱們的實現類都被對應的接口所代替。但就這一步自己來講,並無太大的難度:找到之前服務調用的地方,而後換成接口調用。無非就是有些服務用的比較多,換起來繁瑣一些。但咱們如今須要考慮一個問題:服務的實現,咱們怎麼給?首先想到的是,咱們留一個參數來傳入。但這種方式會致使未來使用lib的時候,溝通成本太大:你須要告訴別人哪里哪里我須要傳入什麼類型的參數。否則你這個lib就無法運行。我不但願別人在使用你lib的時候,還須要去內部查下你的代碼是什麼,應該怎麼傳參數。我但願 別人在使用的時候,對他們來講,lib是儘可能透明的。不須要知道lib內部寫的是什麼,只須要在外部配置一個txt的文本就能夠運行lib!那應該怎麼作呢?
其實java很早就提供了這種相似的功能:在資源目錄META-INF/services中放置提供者配置文件,而後在app運行時,遇到Serviceloader.load(XxxInterface.class)時,會到META-INF/services的配置文件中尋找這個接口對應的實現類全路徑名,而後使用反射去生成一個無參的實例。
咱們大致的使用方式也是基於java提供的這種功能:

4.3.1.2 對官方serviceloader改造
4.3.1.2.1 官方serviceloader缺陷
從前面的闡述來看,java官方提供的serviceloader至少有三個地方須要改進。

  • serviceloader沒有緩存功能。由於對於服務來講,大部分咱們都須要使用單例模式,而不會頻繁的生成新的實例。
  • serviceloader使用無參的構造方法進行構建實例。這點不用多說,確定須要改進。誰的服務構建的時候不須要傳入參數呢?
  • serviceloader沒有預檢查等問題。由於在運行時,須要在配置文件中去尋找接口對應的實現類名。那麼確定會遇到接口名寫錯了,類名寫錯了,配置方式寫錯了,找不到接口實現類等,這些錯誤在編譯器是發現不了的。同時,使用serviceloader是一種非顯式的調用服務實現類方式,若是不在proguard中保護這些實現類,那麼確定會被shrink掉。除了proguard問題外,配置文件寫在資源目錄META-INF/services下對於一些手機(三星)也有兼容問題。最後,考慮servic配置文件手動註冊的缺點,serviceloader須要提供自動註冊功能。

對於上面三種狀況的處理,第一點很容易解決。提供一個緩存就能夠了,很少說。

4.3.1.2.2 serviceloader構造實例
第二點咱們是這麼解決的:咱們讓全部使用serviceloader加載服務的接口都實現Iprovider接口。Iprovider接口提供了一個init(Context context)方法。這樣全部的服務實現類都須要實現init(Context)方法,在裏面作原構造方法裏須要作的初始化邏輯。所以,咱們在調用serviceloader加載服務的時候就相似這樣:

ImageLoader  imageLoader = MovieServiceLoader.getService(context, ImageLoader.class);
複製代碼

在MovieServiceLoader內部,生成的實例會調用一下init(Context)方法。這樣咱們就解決了第二個問題。這裏可能也會有一些朋友有些疑問(好比和美團平臺的童鞋就此事討論過):爲何只傳入context參數。若是一個服務實現類還須要其餘參數怎麼辦?就咱們的服務和而言,我認爲只須要傳入context,基本上經過context可以得到android絕大部分的參數。而且對於服務來講,既然它是一種服務,按理說不會依賴你項目一個具體的一個組件。因此我認爲傳入context就夠了,而不是傳入不定格式的object參數:

MovieServiceLoader.getService(Object... params, ImageLoader.class);
複製代碼

這種方式當然可以解決全部問題。可是這種設計的思想已經違背了接口和實現的隔離概念。好比說,我想使用圖片加載服務,按理說我只須要調用一下

imageLoader = MovieServiceLoader.getService(context, ImageLoader.class);
複製代碼

就ok了,你這個具體的服務是Picasso仍是glide別讓我知道,我也不想知道。若是使用第二種方案,我難道還要知道你這個具體服務須要哪些參數,而後傳入嗎?這感受太不友好了。使用Iprovider還有一個好處,那就是咱們只須要在MovieServiceLoader倉庫的proguard中添加

就能夠了。其餘的地方在使用或新建服務接口時,都不用再考慮proguard問題了。

4.3.1.2.3 serviceloader預處理—gradle插件
第三個須要解決的問題是serviceloader的預檢查等。這個解決方法就是寫一個gradle插件。插件的大致流程是:

  • 咱們在build的某個階段拿到全部編譯後的class文件(夾)和jar包。
  • 使用javassit肯定哪些類被@autoService修飾,配置文件中若是不存在,在其添加。
  • 查看serviceConfig配置文件裏面的格式是否是正確。
  • 經過javassit來肯定serviceConfig配置文件裏面的類是否是在項目中存在,接口類是否是實現了Iprovider接口。

4.3.1.2.4 須要用到的知識:build流程,javassit,groovy
原本這裏不想說太多東西,可是考慮到這三樣不少讀者可能不熟悉。直接去網上google這三方面的東西,單就這些東西,可能還須要學上一學。那麼我仍是把個人一些經驗寫上(爲了切題,就不詳細展開了),讀者能夠參考參考,些許可以事半功倍。

由於你須要拿到編譯後的class文件和jar包,你須要知道build的大致流程,各個task的輸入輸出是什麼,是以文件夾的形式仍是jar包的形式。好比說拿全部class的時機,能夠在assembleXxx這個task時(dex task已經完成了),從dex task的輸入文件夾/jar包中拿到全部的class。同理javac task的輸入也能夠。但javac task的輸出就不能夠,由於javac task輸出的intermediate/class文件夾只包含項目中的class文件,不含有aar對應的intermediate/exploded-aar文件裏面的class文件。固然transform也是一種實現方式。transform的輸入,輸出文件路徑已經給好了,輸入的class爲全部的class。

除build流程以外,你還可能使用groovy來寫插件邏輯。不過若是你實在不想用groovy,那麼也能夠用java,二者兼容,只是groovy的不少特性像循環等就無法用了。這裏有個小經驗:寫goovy,ide不能很好提示錯誤,好比你使用了一個變量或一個方法,若是方法用錯了,變量沒定義。也不會給你提示找不到。因此最好仍是使用先寫到.java裏面,而後再移動到.groovy裏面吧。

最後你還須要知道javassit的一些知識,這個是處理class文件的工具。很強大,和java很像,大部分的使用都會落腳到ctClass的使用。因此這個類最好熟悉。這裏有個小經驗:有時候須要ctClass->class的轉化,記得使用靜態變量儲存這個class對象,否則會報 classloader屢次加載同一個路徑的異常。

ok,使用serviceloader來進行解耦的原理,改進,好處已經說完了。

4.3.2 serviceloader解耦 vs 路由方式解耦
網上有關模塊化的博客,大部分使用的是路由的方式解耦。路由的方式解耦是怎麼一回事呢?

4.3.2.1 路由方式解耦闡述
咱們看下大致的路由框架圖

那麼這個路由框架是怎麼工做的呢?這裏的action是一個服務,provider是一個map集合,盛放一個lib裏的全部(action名字:action實例)鍵值對。在宿主app中註冊各個lib的provider。這樣module A請求moduleB的服務時,經過(代碼來源):

即經過提供provider的名字,action的名字,參數名,值,到註冊的map中尋找對應的action實例,而後調用其對應的方法。核心就是使用字符串來匹配對應的實例進行解耦。

4.3.2.1.1 路由方式解耦優勢
這種方式的最大好處是,新建一個服務時,不須要寫接口,全部的都用字符串來進行標誌,進行匹配,兩個model之間不須要耦合任何東西,甚至接口聲明都不須要耦合。若是一個lib中有不少須要被外界調用的服務,而且調用的次數很少,或者我不只僅對服務解耦,那麼用這種路由的方式很好,由於不用寫接口了。

4.3.2.1.2 路由方式在服務解耦方面不適用性的討論
但爲何沒選擇這種解耦方式呢?由於這種方式,對於android總體的服務解耦來講,我仍是提出了以下的顧慮(僅表明本身的觀點,可能比較粗鄙,並非說人家的項目不夠優秀):

  • 對於大面積的解耦,確定大部分是app界別的服務進行解耦。特色是大量使用,這時候我寫幾個接口,下沉到base庫,無傷大雅。這樣我在使用的時候,serviceloader好處就突出來了:使用服務的時候,我不須要關心實現類的類名,包名是什麼,須要傳入什麼參數,調用的方法的名字是什麼。若是使用路由方式接口,我須要關心的事情就多了,若是我須要關心這麼多東西,它就不該該叫服務了。若是另外一個lib在你不知情的狀況下改了名字怎麼辦?而且在代碼移植到其餘app或獨立運行時,配置方式也不夠友好。serviceloader只須要寫個配置txt文件放在apk中便可,而且每個lib的服務寫到本身的serviceCinfig便可,不須要宿主app關心。使用路由方式,即便action能夠自動註冊,也須要在application處理一些註冊的事情。
  • 路由這種服務框架和serviceloader,本質來講,並不能進行真正意義上的模塊間的通訊。說的通俗點,路由框架能作的是:b lib能夠在不依賴a lib項目的狀況下,b能夠new 出來a中一個類的實例(或提早new好),而後調用那個實例的方法。這並不是通訊,只是可以調用其餘倉庫的方法。而通訊指的是監聽狀態,回調。serviceloader一樣也作不到真正意義上的通訊。模塊間通訊只能經過非顯式的監聽機制才能進行,好比eventbus,廣播,contentprovider等來進行。爲何要說這一點呢?由於我看到不少模塊化的博客都在說使用路由框架進行模塊間通訊。但就前面提到的這種路由框架,確實作不到真正意義上的模塊間通訊。

ok,serviceloader解耦 vs 路由方式解耦就到這裏。

4.4 解耦方面的其餘工做

4.4.1 工做評估
前面很大一個篇幅都在講使用serviceloader進行服務的解耦。那麼除了這個,還須要作什麼?這裏我先大致總結一下,再逐個闡述:

4.4.2 服務實現的抽離
第一點的後半句須要注意一下:若是你但願全部模塊都可以獨立打包運行,那麼須要把全部的服務實現也抽離出來。若是不想獨立運行,只是想進行解耦,那麼仍是留在宿主app中便可。雖說這麼一句話很輕鬆。可是抽離一個服務實現,真正實施起來卻須要花費不少的時間,由於一個服務可能耦合了不少的東西,不留神很差拆。這一點讀者們內心要先有個數。 不過能抽離就儘可能抽離吧,不僅是lib的獨立運行。對以後服務的替換也有很大的好處。好比網絡加載庫,之前使用的是retrofi+okhttp,後來升級成了retrofit+長鏈接。替換的時候只是在服務配置文件中改一句話的事情。若是打算抽離,要注意接口的定義,不要耦合具體某個庫的類,考慮要全面,設計要合理。好比INet庫,接口定義爲:

public interface INetService extends IProvider {
    <T> T create(final Class<T> service, String getdataPolicy, String cacheTime);
}
複製代碼

雖然retrofit是一個很棒的庫,但接口也沒有耦合這個庫。說不定哪一天就替換了。

4.4.3 數據庫的抽離
第二點提及來很痛苦,數據庫的抽離真的是很麻煩。不知道在哪一個版本開始,貓眼耦合了greendao。這個數據庫自己來講挺優秀的,可是架不住它太大!若是我想把一個lib給別人用,難道我這個lib還得耦合一個大的第三方數據庫?!!由於以前沒有考慮過模塊化,因此基本全部的網絡數據,敏感數據等都進行了grrendao的保存。因此解耦的時候往往看到daossion,我都是虎軀一震。網絡數據使用文件存儲且對業務代碼透明。敏感數據使用數據庫存儲,但用接口隔離,而且數據庫建議使用官方的數據庫sqlite或者room。

4.4.4 和butterKnife說再見
第三點的意思是若是你想將業務代碼獨立模塊化,那麼就得跟像butterknife框架的view注入功能說拜拜了。由於Android ADT14開始,library的R資源再也不是final類型的了,因此在library中你不能使用R.id.xx,須要使用findViewById()來代替;也不能使用switch(R.id.xx),須要使用if…else來代替。
第四點是第一點的後續工做。不存在多少工做量。

4.4.5 頁面跳轉
4.4.5.1 頁面跳轉須要作的事情
頁面跳轉也是app中須要重視的一個事情,由於它是模塊化的門戶,涉及到頁面與頁面,其餘app、i版到頁面之間的通訊問題。雖然看起來簡單,但若是設計不合理,那麼模塊化入口的代碼優雅度,crash數量,頁面降級,運營協做等方面都會受到影響。
對於頁面間的跳轉,咱們的通常作法:

一、若是這個類頁面沒有隱式跳轉功能:

  • 那麼直接在其餘頁面首先
    獲取intent(getContext(),TargetActivity.class(,而後intent添加參數。
    最後starActivity(getContext(),intent)。
  • 在目標activty的onCreate()裏面getIntent().getString(xx_key,defaultValue)等獲取參數;
  • 若是xx_key對應的value不合法或者解析錯誤,好比movieId=0,或者等於「」。那麼應該跳轉到一個其餘頁面或者跳轉失敗。

二、若是這個頁面配置了隱式跳轉功能:

  • 那麼在其餘頁面你首先得建立一個createXxxActivityIntent()的utils方法,在裏面傳入落地頁的path,參數key,參數value。
  • 在manifest中聲明。
  • 在目標activty的onCreate()裏面getIntent().getData().parseBoolean(xx_key,defaultValue)…等獲取參數
  • 若是xx_key對應的value不合法或者解析錯誤,好比movieId=0,或者等於「」。那麼應該跳轉到一個其餘頁面或者跳轉失敗。

4.4.5.2 android原生頁面跳轉存在的問題
下面說下這種使用原生頁面跳轉存在的問題~

在獲取參數的時候,須要寫一大推的intent.get(xx),若是這個頁面既含有隱式跳轉,又含有顯示跳轉,那麼確定上面那個過程都須要,這樣在onCreate()裏面就會很是的亂。要進行if else

若是想進行隱式跳轉,那麼都須要在manifest進行註冊intent-filter。一是麻煩,二是我須要在另一個地方去配置某一個activity的東西,管理不方便。

須要另外寫一個utils獲取隱式intent。

沒有降級策略,若是運營配錯了,那麼只能到錯誤頁面,而沒法進行一個補救措施,好比進入i版頁面。

開發人員或者後臺配置錯誤參數的時候,咱們須要寫兜底邏輯。每個頁面解析都須要寫一段相同的邏輯。

若是一個頁面須要登陸用戶才能夠打開的權限,那麼咱們常常會寫if(isLogin()){//跳轉頁面}else{//跳轉到登陸頁面} ,每次操做都要寫這些個相同的邏輯。

若是以爲在這方面沒那麼多要求,針對頁面間的跳轉,爲了避免耦合其餘的模塊的類,全部頁面均可以採用隱式跳起色制來進行。這基本已經能夠知足狀況了。但我這裏仍是想說下阿里推出的開源框架Arouter。其具備攔截功能,這樣跳轉失敗能夠有降級處理(好比呈現i版頁面),讓頁面具備登陸用戶可打開權限;獲取參數方式統一等。仍是挺不錯的。基本解決了上面所面臨的問題。具體就不展開了,具體能夠看開源最佳實踐:《Android平臺頁面路由框架ARouter》

4.5 模塊間/頁面間通訊
4.5.0 使用ViewModel來進行頁面間數據共享
這一段是新加的內容。我以爲放到這裏比較合適。ViewModel是google新推出的lifecycle-component中的類,官方文檔中闡述使用ViewModel能夠解決頁面旋轉等配置改變時數據保存的問題。我思考了下,以爲它在解耦頁面內數據共享的問題也能發揮做用。

舉一個我之前遇到過的例子:一個頁面作完了,pm找我作頁面的埋點。埋點須要頁面的movieId信息,可是須要埋點的那個block中並無movieId。而且我這個block層級很深。若是想拿到movieId,我須要從activity頁面層級一層層傳到我這個block中,免不了中間層級的耦合和方法的建立。當時以爲這件事真是讓人頭大。那時候多麼須要有個像事件監聽形式的eventbus那樣的東西,我只須要把數據放到bus裏面,而後這個頁面的任何一個地方都能很方便的獲取。總結一下:直白點說就是頁面block/fragment之間須要使用對方的據/view時,無需之間硬性的引用,只須要activity的context參數就能夠獲取對方的數據/view,從而進行數據交流、view訪問。而頁面的context是系統類型且是很容易獲取的,並不存在耦合。
具體使用能夠參考我以前寫的一篇文章使用ViewModel共享頁面內的數據:ActivityDataBus

4.5.1 爲何要去掉eventbus,使用廣播
若是已經到了這一步,那麼大致上一個頁面已經抽離出來了,剩下的是與其餘模塊、其餘頁面間的互動了。
前面說了serviceloader和路由方式都沒辦法作這些事情。咱們首先想到的是使用eventbus來作這些事情。使用eventbus的前提是,須要定義一些Event事件。好比:

但若是你將業務代碼各自模塊化以後,就有一個尷尬的問題擺在面前:Event事件放在哪裏?由於不少庫都須要收聽這個Event事件,因此只能將Event下沉到基礎庫。這樣致使的結果是基礎庫愈來愈大,還沒法拆分。關於這點《微信Android模塊化架構重構實踐》也提到了這件事情,而且自創性的使用了一種叫「.api」化的方式來解決這件事情。原理是在編譯期將公用接口下沉到基礎庫,供其餘module使用,而這段代碼的維護仍然放到非基礎庫中。這種base庫不會膨脹,代碼維護的責任制更明確,肯定挺不錯。惋惜最近沒有那麼多時間來寫這個gradle插件了。不知道哪一個讀者有時間和興趣能夠實現這個插件。意義仍是很大的,基礎庫的代碼不會愈來愈膨脹。eventbus除了使基礎庫膨脹以外,還有一個問題是,不能進行app間的進程通訊。咱們使用廣播來取代eventbus。android推出的LocalBroadcast實現機制簡單來講是looper-handler並維護一個全局的map。性能上和eventbus相似,使用字符串而不是Event model來匹配事件。咱們若是使用一個接口來包裝BroadcastManager,那麼咱們在app內部可使用域內廣播進行,對於模塊化後的lib,咱們可使用域外廣播來進行app間的通訊。

4.5.2 不要亂髮廣播
若是你項目中大量的使用eventbus,那麼你會看到一個類中有大量的onEventMainThread()方法,寫起來很爽,閱讀起來很痛苦。若是項目中發送這個Event的地方很是多,接收這個Event的地方也不少。在進行代碼拆分時,你都不敢輕舉妄動,生怕哪些事件沒有被接收。廣播和eventbus相似,若是項目中存在同一事件的大量發送和接收,那麼項目的可讀性和可維護性就會變得至關差。這種狀況在敏感數據的同步問題上尤其突出:

其實對於敏感數據的同步,不須要發送廣播或eventbus來進行同步。能夠藉助數據庫將想看數據本地化來完成同步。大致的思想就是咱們從網絡中獲取的數據都同步到數據庫。在進行敏感數據填充view時,採用的數據都來自數據庫。在頁面返回時,若是頁面不觸發填充敏感數據view的邏輯,那麼在onResume()手動調用,即:

那麼模塊間/頁面間通訊大致的就講完了,這裏須要作的工做很少:

4.6 lib獨立運行

4.6.1 爲何須要與宿主app進程通訊
到這一步,一個業務模塊既能夠做爲library放在宿主app中,也能夠做爲application獨立運行了。做爲library很容易理解,和文章前面的問答模塊闡述的同樣,作宿主app中添加幾個activty的殼子,而後添加上lib中的page,而後在manifest中註冊便可,即:

固然若是還須要作一些actionBar的交互,須要在宿主activty中寫入相應的邏輯。整個app的框架圖如:

當業務lib須要調試時,咱們須要讓這個lib獨立運行,就如同文章前面的問答業務模塊demo所示。那這時候就有一個問題,咱們lib獨立運行時,帳戶的數據從何而來,和app相關的地理位置,城市等等這些數據怎麼獲得?讀者可能會說這些不是服務嗎?服務的話,不該該和網絡加載,圖片加載的服務同樣使用serviceloader加載嗎?按道理講是這樣的,但帳戶等一些信息的服務實現類並非那麼容易從宿主app中抽離出來,由於那麼服務實現類須要application中進行初始化,還要考慮不少其餘東西。因此真實的帳戶信息並不那麼容易經過之前的那種方式獲取,那怎麼辦呢?最簡單的辦法是製做假數據,好比造一個我本身帳戶的信息,做爲服務實現類使用。但這樣的話,帳戶信息只能是一我的的,對帳戶信息的修改不可行,帳戶也不能退出登陸。因此還得想新的辦法。

4.6.2 與宿主app進程通訊過程
最後發現若是咱們獨立運行的lib可以監聽宿主app的帳戶,位置,城市,登陸類型,設備等信息並可以進行同步,那麼獨立運行的lib中的這些信息就都是真實信息了,而且是動態的。當宿主app退出登陸,lib中也是無登陸狀態。具體的操做是:

  • 在宿主app中,咱們提供一些contentProvider,各方法提供的內容就是宿主app真實的的帳戶等數據。當對宿主app帳戶等信息改變時,通知contentProvider的監聽者,好比:
public void onEventMainThread(LoginEvent loginEvent) {
 getContext().getContentResolver().notifyChange(Uri.parse("content://com.maoyan.android.runenv/loginsession"),null);
}
複製代碼
  • 在獨立app中,其扮演contentProvider的監聽者:
mContentResolver.registerContentObserver(Uri.parse("content://com.maoyan.android.runenv/devicesession"), false, new ContentObserver(null) {
      @Override
      public void onChange(boolean selfChange) {
          super.onChange(selfChange);
          reloadEnviroment();
      }
});
複製代碼

這樣的話,lib中帳戶等數據就和宿主app的數據保持一致了。咱們使用服務接口包一層,這樣使用方式和以前的服務使用方式就一致了。
大致的示意圖如:

當宿主app退出登陸,lib中也是無登陸狀態,咱們看下demo:

最後,按照慣例,當一個模塊要獨立運行時,須要作的事情評估:

5 最後的話

整個流程終於結束了,但願讀者看完後,對模塊化有個總體的認識,對每一步須要作什麼,耗時多少都有個大體的瞭解。進行模塊化並非爲了炫技,代表本身多厲害。若是隻是這樣,那大可沒必要這麼作。由於模塊化是一個繁瑣,枯燥,耗費時間長,你作了大量的工做,可是在表面功能上,老闆們可能看不到。還不如花一點時間,引入一個第三方庫看着花哨。很大一部分工做量是爲之前欠設計的代碼邏輯買單。我作這件事件也是爲了業務服務,由於貓眼電影須要服務的客戶端很多。所示作業務解耦,業務進行模塊化是必然的事情。經過模塊化後,能夠很方便的將代碼移植到其餘端,app內頁面的調整也變得簡單。最後的最後,在整個模塊化的過程當中,有一些經驗感悟能夠分享給你們,道理都很簡單,更重要的是落實:

6 參考資料

  • 微信Android模塊化架構重構實踐
  • 開源最佳實踐:Android平臺頁面路由框架ARouter
  • Android組件化之通訊
  • ServiceLoader
  • AutoService
  • Android-Architecture-Components
  • 關於Android業務組件化的一些思考

原創做者:陳文超,原文連接:https://www.jianshu.com/u/485dc78851d0
歡迎關注個人微信公衆號「碼農突圍」,分享Python、Java、大數據、機器學習、人工智能等技術,關注碼農技術提高•職場突圍•思惟躍遷,20萬+碼農成長充電第一站,陪有夢想的你一塊兒成長。

相關文章
相關標籤/搜索