記得第一次實施項目組件化時,遇到的最大困擾就是,組件之間的通信問題。例如:
- 怎么從這個組件跳轉到另一個組件的頁面;
- 組件之間怎么傳遞數據;
- 怎么獲取其他組件的數據或服務;
- 組件怎么通知其他組件響應某個事件;
1. 頁面跳轉統一采用路由
在Android中,頁面跳轉都是通過startActivity來實現的。但是我們組件化之后,上層的業務組件之間是不能相互依賴的,也就是說現在無法通過startActivity來進行頁面跳轉了。
組件化之后,所有頁面跳轉都必須采用路由來實現。現在已經有很多成熟的路由框架了,具體什么是路由、路由的作用都講的很清楚,我這里不再贅述了,比較成熟的有:
- 美團的WMRouter: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. 小結
組件之間通信是組件化開發首先要解決的問題,我們必須先解決該問題,后面才能實施下去。在資源緊張、時間緊迫的情況下,可以借鑒成熟的方案,沒必要重新造輪子。我的方案,前期實施比較容易,也很容易理解,但是問題其實還很多。
系列文章
Android組件化開發實踐(一):為什么要進行組件化開發?
Android組件化開發實踐(二):組件化架構設計
Android組件化開發實踐(三):組件開發規范
Android組件化開發實踐(四):組件間通信問題
Android組件化開發實踐(五):組件生命周期管理
Android組件化開發實踐(六):老項目實施組件化
Android組件化開發實踐(七):開發常見問題及解決方案
Android組件化開發實踐(八):組件生命周期如何實現自動注冊管理
Android組件化開發實踐(九):自定義Gradle插件
Android組件化開發實踐(十):通過Gradle插件統一規范