Android元件化開發實踐(四):元件間通訊問題
記得第一次實施專案元件化時,遇到的最大困擾就是,元件之間的通訊問題。例如:
- 怎麼從這個元件跳轉到另一個元件的頁面;
- 元件之間怎麼傳遞資料;
- 怎麼獲取其他元件的資料或服務;
- 元件怎麼通知其他元件響應某個事件;
1. 頁面跳轉統一採用路由
在Android中,頁面跳轉都是通過 startActivity 來實現的。但是我們元件化之後,上層的業務元件之間是不能相互依賴的,也就是說現在無法通過 startActivity 來進行頁面跳轉了。
元件化之後,所有頁面跳轉都必須採用路由來實現。現在已經有很多成熟的路由框架了,具體什麼是路由、路由的作用都講的很清楚,我這裡不再贅述了,比較成熟的有:
- 美團的 WMRouter : ofollow,noindex">https://tech.meituan.com/meituan_waimai_android_open_source_routing_framework.html
- 阿里的ARouter: https://github.com/alibaba/ARouter
路由框架的核心原理都是一樣,這裡我來說說我自己的路由框架,以及這樣設計的原因何在。
1.1 路由URI格式
路由實質上都是將一個URI對映到某一個具體的介面,通過URI跳轉時,路由框架內部找到該URI對應的Activity頁面,進而實現頁面跳轉。首先我們來看一張圖,明白一個完整的URI是怎麼定義的。

圖片引用自美團
是不是特別複雜,但實際上我們並不需要這麼複雜,以我的一個路由uri為例:
hmiou://www.54jietiao.com/webview/index?title=*&url=*
這是一個開啟WebView頁面的路由地址定義,具體來說只採用了URI的幾個部分:
- scheme
這個是必須的,我定義為hmiou,這個根據專案來自定義即可。 - host
www.54jietiao.com ,通常定義為你專案的主站域名。 - path
/webview/index,也就是路徑,根據你的業務來區分即可。 - query
title=*&url=*,查詢引數
我的路由定義裡面,只採用了scheme、host、path、query這4部分,能滿足我的需求即可。
1.2 路由對映檔案
我們沒有采用註解,而是定義了一份全域性的路由對映json檔案,應用啟動時讀取配置檔案進行路由初始化。
{ "test": [ { "url": "hmiou://www.54jietiao.com/test/test1?title=*", "iclass": "Test1ViewController", "aclass": "com.hm.iou.router.demo.TestActivity1" }, { "url": "hmiou://www.54jietiao.com/test/test2", "iclass": "Test2ViewController", "aclass": "com.bwton.router.demo.MainActivity" } ], "main": [ { "url": "hmiou://www.54jietiao.com/main/index?url=*", "iclass": "MainViewController", "aclass": "com.hm.iou.router.demo.MainActivity" } ] }
每個配置項都包含“url、iclass、aclass”3個選項,url就是路由定義,iclass是對應的iOS裡面該頁面的類名,aclass是對應的Android裡面該頁面的類名,這麼做的目的是為了保持2個平臺的路由統一。
我根據功能將路由進行了分組,從上面配置檔案中可以看到有2個分組:test、main,然後每個路由url的path都以該分組名開頭,所以每個路由url至少應該包含2級路徑。
以路由“ hmiou://www.54jietiao.com/test/test1?title= 標題”為例,來看看內部是怎樣實現頁面跳轉的。
1.路由框架首先解析出這個url的scheme、host、path、query這4部分;
2.檢查scheme是否應用能支援的scheme,如果不是則不允許跳轉或跳轉失敗;
3.檢查host是否應用支援;
4.前面檢查通過後,取出path的第一級路徑,這裡為“test”,然後框架查詢路由配置表,找到“test”這個分組;
5.在“test”分組下的路由配置裡遍歷匹配,找到與當前url一致的路由配置項資料來;
6.找到對應的配置項之後,找到該url對應的aclss,這裡為“com.hm.iou.router.demo.TestActivity1”;
7.框架通過反射呼叫startActivity來進行頁面跳轉;
8.如果第1步解析出的查詢引數裡有值,則將引數放到Intent裡面傳遞過去,這裡我們會傳遞一個key為“title”,value為“標題”的資料傳遞過去,類似於intent.putExtra("title", "標題")。
9.路由表裡的查詢引數都定義成類似於title=*,這裡*只是一個佔位符,僅僅是為了便於開發人員理解,知道該路由接收一個引數,名為“title”。
這裡對路由進行分組,是因為做url匹配時,需要遍歷整個路由表,分組可以提高查詢匹配url的速度。配置檔案裡的url甚至可以設定一些自定義的正則匹配規則,你可以設定一些萬用字元,讓若干個不同的url都能跳轉到同一個頁面。
當然還有很多細節需要處理,比如:
- 支援頁面間跳轉動畫;
- 支援startActivityForResult;
- 支援設定intent的flag;
- 使用路由url進行跳轉時,查詢引數的值必須進行encode,否則會導致解析失敗;
- 通過Intent傳遞引數時,不能知道查詢引數裡的資料型別,統一定義為字串型別;
還有些功能可能實現不了,比如說頁面之間怎麼傳遞物件,Android裡可以傳遞Parceable、Serializable物件,在我這裡就不能支援。不過我並不推薦頁面間傳遞物件,這樣會帶來比較高的耦合度,同時不利於元件化開發。
1.3 動態更新路由檔案
通常安裝包裡會包含一份初始的配置檔案,但是當應用釋出之後,某個頁面出現嚴重bug,或者我們想改變某個入口點選後的跳轉目標頁面,這時可以通過動態更新路由配置檔案來實現。
新的配置檔案裡,只需要把原本配置裡的aclass、iclass替換成新的目標頁面類名即可,而不用重新發布app版本。
1.4 外部路由分發器
現在很多應用有這麼個功能:在外部第三方應用裡,或者H5網頁裡,直接通過路由url能開啟我們的應用,並跳轉到指定的目標頁面。
public class RouteDispatchActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected void onResume() { super.onResume(); Intent data = getIntent(); if(data != null && data.getData() != null) { Uri uri = data.getData(); //-------通過路由來跳轉------- 1.判斷uri是否合法; 2.判斷uri是否在白名單內; 3.判斷通過,則採用路由跳轉; 4.不通過或跳轉失敗,則僅僅開啟應用而已; } finish(); } }
在AndroidManifest.xml裡配置:
<activity android:name=".RouteDispatchActivity" android:configChanges="keyboardHidden|orientation|screenSize" android:screenOrientation="portrait" android:theme="@android:style/Theme.Translucent.NoTitleBar" > <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <data android:scheme="hmiou" android:host="www.54jietiao.com" /> </intent-filter> </activity>
該Activity沒有任何業務邏輯,它只是一個接收外部uri跳轉的入口Activity。注意這裡的 <intent-filter />配置,這樣該Activity就能響應所有 hmiou://www.54jietiao.com 這種格式的uri跳轉了。
該Activity被設定成透明的樣式,使用者感知不到,它的作用就是一個外部路由的分發器,這樣我們就能在外部應用裡跳轉到任意頁面了。這裡有個路由白名單是個什麼鬼,且繼續看下去。
1.5 路由白名單配置
前面講到可以從外部跳轉到任意指定頁面,這顯然是極度危險的操作,如果你的應用裡有錢包的話,這意味著任何應用都可以開啟你的錢包頁面進行付錢。所以對外部應用的路由跳轉,我們必須設定白名單,在白名單內的路由url,能跳轉到指定的目標頁面,不在其內的僅僅只是開啟應用進入首頁而已。
[ "hmiou://www.54jietiao.com/main/index" ]
2. 資料服務共享
像美團的WMRouter框架,主要提供了URI分發、ServiceLoader兩大功能。ServiceLoader通俗點說就是元件間服務共享、資料共享,我在路由框架裡沒有實現,而是換了個方式來實現這些需求。
2.1 維護好全域性共享資料
一般應用裡都需要使用者登入,登入之後我們會本地儲存使用者資訊,而使用者資訊可能在所有的元件都會使用。例如註冊登入元件服務裡,使用者登入後需要儲存登入資訊到本地;使用者在個人中心元件服務裡,需要讀取使用者登入資訊進行展示。
通常這類資料我稱之為全域性共享資料,我通常的做法是,將這類資料下沉到底層模組裡,所有業務元件可依賴,這樣就解決了元件之間資料共享的問題。
不要盲目的將共享資料下沉到底層元件裡,否則隨著業務的擴張,會造成難以維護的地步。一旦資料下沉之後,以後想從底層元件裡剝離出,將會是一件非常困難的事情。
2.2 採用EventBus
除了資料共享之外,還有一個是服務呼叫,例如A元件想呼叫B元件的某個操作。還是以登入為例,當用戶登入成功之後,在個人中心這個元件裡,需要及時展示使用者的個人資訊。
我引入了EventBus庫,通過EventBus傳送事件通知,其他元件接收自己感興趣的事件,通過訂閱通知的模式,來實現元件之間的通訊。
public void post(Object event)
採用EventBus有個問題,它傳送的事件必須是一個物件,但我們不可能在底層模組定義很多event class,所以我定義了一個通用的事件。
public class CommBizEvent { public String key; public String content; public CommBizEvent(String key, String content) { this.key = key; this.content = content; } }
通過key來區分事件,然後元件文件維護好這些事件名。
3. 小結
元件之間通訊是元件化開發首先要解決的問題,我們必須先解決該問題,後面才能實施下去。在資源緊張、時間緊迫的情況下,可以借鑑成熟的方案,沒必要重新造輪子。我的方案,前期實施比較容易,也很容易理解,但是問題其實還很多。