WMRouter是一款Android路由框架,基於組件化的設計思路,功能靈活,使用也比較簡單。html
WMRouter最初用於解決美團外賣C端App在業務演進過程當中的實際問題,以後逐步推廣到了美團其餘App,所以咱們決定將其開源,但願更多技術同行一塊兒開發,應用到更普遍的場景裏去。Github項目地址與使用文檔詳見 github.com/meituan/WMR…。java
本文先簡單介紹WMRouter的功能和適用場景,而後詳細介紹WMRouter的發展背景和過程。android
WMRouter主要提供URI分發、ServiceLoader兩大功能。git
URI分發功能可用於多工程之間的頁面跳轉、動態下發URI連接的跳轉等場景,特色以下:github
基於SPI (Service Provider Interfaces) 的設計思想,WMRouter提供了ServiceLoader模塊,相似Java中的java.util.ServiceLoader
,但功能更加完善。經過ServiceLoader能夠在一個App的多個模塊之間經過接口調用代碼,實現模塊解耦,便於實現組件化、模塊間通訊,以及和依賴注入相似的功能等。其特色以下:瀏覽器
其餘特性:性能優化
WMRouter適用但不限於如下場景:微信
Native+H5混合開發模式,須要進行頁面之間的互相跳轉,或進行靈活的運營跳轉連接下發。能夠利用WMRouter統一頁面跳轉邏輯,根據不一樣的協議(HTTP、HTTPS、用於Native頁面的自定義協議)跳轉對應頁面,且在跳轉過程當中可使用UriInterceptor對跳轉連接進行修改,例如跳轉H5頁面時在URL中加參數。網絡
統一管理來自App外部的URI跳轉。來自App外部的URI跳轉,若是使用Android原生的Manifest配置,會直接啓動匹配的Activity,而不少時候但願先正常啓動App打開首頁,完成常規初始化流程(例如登陸、定位等)後再跳轉目標頁面。此時可使用統一的Activity接收全部外部URI跳轉,到首頁時再用WMRouter啓動目標頁面。架構
頁面跳轉有複雜判斷邏輯的場景。例如多個頁面都須要先登陸、先定位後才容許打開,若是使用常規方案,這些頁面都須要處理相同的業務邏輯;而利用WMRouter,只須要開發好UriInterceptor並配置到各個頁面便可。
多工程、組件化、平臺化開發。多工程開發要求各個工程之間能互相通訊,也可能遇到和外賣App相似的代碼複用、依賴注入、編譯等問題,這些問題均可以利用WMRouter的URI分發和ServiceLoader模塊解決。
對業務埋點需求較強的場景。頁面跳轉做爲最多見的業務邏輯之一,經常須要埋點。給每一個頁面配置好URI,使用WMRouter統一進行頁面跳轉,並在全局的OnCompleteListener中埋點便可。
對App可用性要求較高的場景。一方面,能夠對頁面跳轉失敗進行埋點監控上報,及時發現線上問題;另外一方面,頁面跳轉時能夠執行判斷邏輯,發現異常(例如服務端異常、客戶端崩潰等)則自動打開降級後的頁面,保證關鍵功能的正常工做,或給用戶友好的提示。
頁面A/B測試、動態配置等場景。在WMRouter提供的接口基礎上進行少許開發配置,就能夠實現:根據下發的A/B測試策略跳轉不一樣的頁面實現;根據不一樣的須要動態下發一組路由表,相同的URI跳轉到不一樣的一組頁面(實現方面能夠自定義UriInterceptor,對匹配的URI返回301的UriResult使跳轉重定向)。
下面開始介紹WMRouter的發展背景和過程。爲了方便後文的理解,咱們先簡單瞭解和回顧幾個基本概念。
根據維基百科的解釋,路由(routing)能夠理解成在互聯的網絡經過特定的協議把信息從源地址傳輸到目的地址的過程。一個典型的例子就是在互聯網中,路由器能夠根據IP協議將數據發送到特定的計算機。
URI(Uniform Resource Identifier,統一資源標識符)是一個用於標識某一互聯網資源名稱的字符串。URI的組成以下圖所示。
一些常見的URI舉例以下,包括平時常常用到的網址、IP地址、FTP地址、文件、打電話、發郵件的協議等。
在Android中也提供了android.net.Uri
工具類用於處理URI,Android中URI經常使用的幾個部分主要是scheme、host、path和query。
在Android中的Intent跳轉,分爲顯式跳轉和隱式跳轉兩種。
顯式跳轉即指定ComponentName(類名)的Intent跳轉,通常經過Bundle傳參,示例代碼以下:
Intent intent = new Intent(context, TestActivity.class);
intent.putExtra("param", "value")
startActivity(intent);
複製代碼
隱式跳轉即不指定ComponentName的Intent跳轉,經過IntentFilter找到匹配的組件,IntentFilter支持action、category和data的匹配,其中data就是URI。例以下面的代碼,會啓動系統默認的瀏覽器打開網頁:
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.meituan.com"))
startActivity(intent);
複製代碼
Activity經過Manifest配置IntentFilter,例以下面的配置能夠匹配全部形如demo_scheme://demo_host/***
的URI。
<activity android:name=".app.UriProxyActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="demo_scheme" android:host="demo_host"/>
</intent-filter>
</activity>
複製代碼
在美團外賣C端早期開發過程當中,產品但願經過後臺下發URI控制客戶端跳轉指定頁面,從而實現靈活的運營配置。外賣App採用了Native+H5的混合開發模式,Native頁面定義了專用的URI,而H5頁面則使用HTTP/HTTPS連接在專門的WebView容器中加載,兩種連接的跳轉邏輯不一樣,實現起來比較繁瑣。
Native頁面的URI跳轉最開始使用的是Android原生的IntentFilter,經過隱式跳轉啓動,可是這種方式存在靈活性差、功能缺失、Bug多等問題。例如:
從外部(瀏覽器、微信等)跳轉外賣的URI時,系統會直接打開相應的Activity,而沒有通過歡迎頁的正常啓動流程,一些代碼邏輯可能沒有執行,例如定位邏輯。
有不少頁面在打開前須要確保用戶先登陸或先定位,每一個頁面都寫一遍判斷登陸、定位的邏輯很是麻煩,提升了開發維護成本。
運營人員可能會配錯URI,頁面跳轉失敗,有些跳轉的地方沒有作try-catch處理,會產生Crash;有些地方雖然加了try-catch,但跳轉失敗後沒有任何響應,用戶體驗差;跳轉失敗沒有監控,不能及時發現和解決線上業務異常。
爲了解決上述問題,咱們但願有一個Android的URI分發組件,能夠根據URI中不一樣的scheme、host、path,進行不一樣的處理,同時可以在頁面跳轉過程當中進行更靈活的干預。調研發現,現有的一些Android路由組件主要都是在解決多工程之間解耦的問題,而URI每每只支持經過path分發,頁面跳轉的配置也不夠靈活,難以知足實際須要。因而咱們決定自行設計實現。
下圖展現了WMRouter中URI分發機制的核心設計思路。借鑑網絡請求的機制,WMRouter中的每次URI跳轉視爲發起一個UriRequest;URI跳轉請求被WMRouter逐層分發給一系列的UriHandler進行處理;每一個UriHandler處理以前能夠被UriInterceptor攔截,並插入一些特殊操做。
常見的頁面跳轉來源以下:
對於來自App內部和Web容器的跳轉,咱們把全部跳轉代碼統一改爲調用WMRouter處理,而來自外部和Push通知的跳轉則所有使用一個獨立的中轉Activity接收,再調用WMRouter處理。
UriRequest中包含Context、URI和Fields,其中Fields爲HashMap<String, Object>,能夠經過Key存聽任意數據。簡單起見,UriRequest類同時承擔了Response的功能,跳轉請求的結果,也會被保存到Fields中。Fields能夠根據須要自定義,其中一些常見字段舉例以下:
每次URI跳轉請求會有一個ResultCode(相似HTTP請求的ResponseCode),表示跳轉結果,也存放在Fields中。常見Code以下,用戶也能夠自定義Code:
總結來講,UriRequest用於實現一次URI跳轉中全部組件之間的通訊功能。
UriHandler用於處理URI跳轉請求,能夠嵌套從而逐層分發和處理請求。UriHandler是異步結構,接收到UriRequest後處理(例如跳轉Activity等),若是處理完成,則調用callback.onComplete()
並傳入ResultCode;若是沒有處理,則調用callback.onNext()
繼續分發。下面的示例代碼展現了一個只處理HTTP連接的UriHandler的實現:
public interface UriCallback {
/** * 處理完成,繼續後續流程。 */
void onNext();
/** * 處理完成,終止分發流程。 * * @param resultCode 結果 */
void onComplete(int resultCode);
}
public class DemoUriHandler extends UriHandler {
public void handle(@NonNull final UriRequest request, @NonNull final UriCallback callback) {
Uri uri = request.getUri();
// 處理HTTP連接
if ("http".equalsIgnoreCase(uri.getScheme())) {
try {
// 調用系統瀏覽器
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(uri);
request.getContext().startActivity(intent);
// 跳轉成功
callback.onComplete(UriResult.CODE_SUCCESS);
} catch (Exception e) {
// 跳轉失敗
callback.onComplete(UriResult.CODE_ERROR);
}
} else {
// 非HTTP連接不處理,繼續分發
callback.onNext();
}
}
// ...
}
複製代碼
UriInterceptor爲攔截器,不作最終的URI跳轉操做,但能夠在最終的跳轉前進行各類同步/異步操做,常見操做舉例以下:
每一個UriHandler均可以添加若干UriInterceptor。在UriHandler基類中,handle()方法先調用抽象方法shouldHandle()
判斷是否要處理UriRequest,若是須要處理,則逐個執行Interceptor,最後再調用handleInternal()
方法進行跳轉操做。
public abstract class UriHandler {
// ChainedInterceptor將多個UriInterceptor合併成一個
protected ChainedInterceptor mInterceptor;
public UriHandler addInterceptor(@NonNull UriInterceptor interceptor) {
if (interceptor != null) {
if (mInterceptor == null) {
mInterceptor = new ChainedInterceptor();
}
mInterceptor.addInterceptor(interceptor);
}
return this;
}
public void handle(@NonNull final UriRequest request, @NonNull final UriCallback callback) {
if (shouldHandle(request)) {
if (mInterceptor != null) {
mInterceptor.intercept(request, new UriCallback() {
@Override
public void onNext() {
handleInternal(request, callback);
}
@Override
public void onComplete(int result) {
callback.onComplete(result);
}
});
} else {
handleInternal(request, callback);
}
} else {
callback.onNext();
}
}
/** * 是否要處理給定的uri。在Interceptor以前調用。 */
protected abstract boolean shouldHandle(@NonNull UriRequest request);
/** * 處理uri。在Interceptor以後調用。 */
protected abstract void handleInternal(@NonNull UriRequest request, @NonNull UriCallback callback);
}
複製代碼
在外賣C端App中的URI分發示意以下圖。全部URI跳轉都會分發到RootUriHandler,而後根據不一樣的scheme分發到不一樣的子Handler。例如waimai協議分發到WmUriHandler,而後進一步根據不一樣的path分發到子Handler,從而啓動相應的Activity;HTTP/HTTPS協議分發到HttpHandler,啓動WebView容器;對於其餘類型URI(tel、mailto等),前面的幾個Handler都沒法處理,則會分發到StartUriHandler,嘗試使用Android原生的隱式跳轉啓動系統應用。
每一個UriHandler均可以根據實際須要實現降級策略,也能夠不做處理繼續分發給其餘UriHandler。RootUriHandler中提供了一個全局的分發完成事件監聽器,當UriHandler處理失敗返回異常ResultCode或全部子UriHandler都沒有處理時,執行全局降級策略。
隨着外賣C端業務的演進,團隊成員擴充了數倍,商超生鮮等垂直品類的拆分,以及異地研發團隊的創建,客戶端的平臺化被提上日程。關於外賣平臺化更詳細的內容,可參考團隊以前已經發布的文章 美團外賣Android平臺化架構演進實踐。
爲了知足實際開發須要,在長時間的探索後,逐步造成了如圖所示的三層工程結構。
原有的單個工程拆分紅多個工程,就不可避免的涉及到多工程之間的耦合問題,主要包括通訊問題、複用問題、依賴注入、編譯問題,下面詳細介紹。
當原先的一個工程拆分到各個業務庫後,業務庫之間的頁面須要進行通訊,最主要的場景就是頁面跳轉並經過Intent傳遞參數。
原先的頁面跳轉使用顯式跳轉,Activity之間存在強引用,當Activity被拆分到不一樣的業務庫,業務庫不能直接互相依賴,所以須要進行解耦。
利用WMRouter的URI分發機制,恰好能夠很容易的解決這個問題。將將全部業務庫的Activity註冊到WMRouter,各個業務庫之間就能夠進行頁面跳轉了。
此時WMRouter已經承載了兩項功能:
waimai://*
)wm_router://page/*
)因爲後臺下發的URI是和產品、運營、H五、iOS等各端統一制定的協議,支持的頁面、格式、參數等都不能隨意改動,而內部頁面跳轉使用的URI,則須要根據實際開發須要進行配置,兩套URI協議不能兼容,所以使用了不一樣的scheme。
業務庫之間常常須要複用代碼。一些通用代碼邏輯能夠下沉到平臺層從而複用,例如業務無關的通用View組件;而有些代碼不適合下沉到平臺層,例如業務庫A要使用業務庫B中的某個界面模塊,而這個界面模塊和業務庫B的耦合很緊密。具體到外賣實際業務場景中,商家頁在商家休息時會展現推薦商家列表,其樣式和首頁相同(如圖),而兩個頁面不在一個工程中,商家頁但願能直接從首頁業務庫中獲取商家列表的實現。
爲了解決上述問題,咱們調研瞭解到Java中SPI (Service Provider Interfaces) 的設計思想與java.util.ServiceLoader
工具類,還學習到美團平臺爲了解決相似問題而開發的ServiceLoader組件。
考慮到實際需求差別,咱們從新開發了本身的ServiceLoader實現。相比Java中的實現,WMRouter的實現借鑑了美團平臺的設計思路,不只支持經過接口獲取全部實現類,還支持經過接口和惟一的Key獲取特定的實現類。另外WMRouter的實現還支持直接加載實現類的Class、用Factory和Provider建立對象、單例管理、方法調用等功能。在Gradle插件的實現思路上,借鑑了美團平臺的ServiceLoader並作了性能優化,給平臺提出了改進建議。
業務庫之間代碼複用的需求示意如圖,業務庫A須要複用業務庫B中的ServiceImpl但又不能直接引用,所以經過WMRouter加載:
URI跳轉和ServiceLoader看起來彷佛沒有關聯,但通訊和複用需求的本質均可以理解成路由,頁面經過URI分發跳轉時的協議是Activity+URI,在這裏ServiceLoader的協議是Interface+Key。
爲了兼容外賣獨立App和美團App外賣頻道的兩端差別,平臺層的一些接口要在兩個主工程分別實現,並注入到底層。常規Java代碼注入的方式寫起來很繁瑣,而使用WMRouter的ServiceLoader功能能夠更簡單的實現和依賴注入相似的效果。
對於WMRouter來講,全部依賴它的工程(包括主工程、業務庫、平臺庫)都是同樣的,任何一個庫想要調用其餘庫中的代碼,均可以經過WMRouter路由轉發。前面的通訊和複用問題,是同級的業務庫之間經過WMRouter調用,而依賴注入則是底層庫經過WMRouter調用上層庫,其本質和實現都是相同的。
ServiceLoader模塊在加載實現類時提供了單例管理功能,可用於管理一些全局的Service/Manager,例如用戶帳戶管理類UserManager
。
因爲歷史緣由,主工程做爲一個沒有業務邏輯的殼工程,對業務庫卻有較多依賴,特別是對業務庫的初始化配置繁瑣,和各業務庫耦合緊密。另外一方面,WMRouter跳轉的頁面、加載的實現類,須要在Application初始化時註冊到WMRouter中,也會增長主工程和業務庫的耦合。開發過程當中各業務庫頻繁更新,常常出現沒法編譯的問題,嚴重影響開發。
爲了解決這個問題,一方面WMRouter增長了註解支持,在Activity類、ServiceLoader實現類上配置註解,就能夠在編譯期間自動生成代碼註冊到WMRouter中。
// 沒有註解時,須要在Application初始化時代碼註冊各個頁面,耦合嚴重
register("/home", HomeActivity.class);
register("/order", OrderListActivity.class);
register("/shop", ShopActivity.class)
register("/account", MyAccountActivity.class);
register("/address", MyAddressActivity.class);
// ...
複製代碼
// 增長註解後,只須要在各個Activity上經過註解配置便可
@RouterUri(path = "/shop")
public class ShopActivity extends BaseActivity {
}
複製代碼
另外一方面,ServiceLoader還支持指定接口加載全部實現類,主工程能夠經過統一接口,加載各個業務庫中全部實現類並進行初始化,最終實現和業務庫的完全隔離。
開發過程當中,各個業務庫能夠像插件同樣按需集成到主工程,能大幅減小不能編譯的問題,同時因爲編譯時能夠跳過不須要的業務庫,編譯速度也能獲得提升。
在WMRouter解決了外賣C端App的各類問題後,發現公司內甚至公司外的其餘App也遇到了類似的問題和需求,因而決定對WMRouter進行推廣和開源。
因爲WMRouter是一個開放式組件化框架,UriRequest能夠存聽任意數據,UriHandler、UriInterceptor能夠徹底自定義,不一樣的UriHandler能夠任意組合,具備很大的靈活性。但過於靈活容易致使易用性的降低,即便對於最常規最簡單的應用,也須要複雜的配置才能完成功能。
爲了在靈活性與易用性之間平衡,一方面,WMRouter對包結構進行了合理的劃分,核心接口和實現類提供基礎通用能力,儘量保留最大的靈活性;另外一方面,封裝了一系列通用實現類,並組合成一套默認實現,從而知足絕大多數常規使用場景,儘量下降其餘App的接入成本,方便推廣。
目前業界已有的一些Android路由框架,不能知足外賣C端App在開發過程當中的實際須要,所以咱們開發了WMRouter路由框架。借鑑網絡請求的思想,設計了基於UriRequest、UriHandler、UriInterceptor的URI分發機制,在保證功能靈活強大的同時,又儘量的下降了使用難度;另外一方面,借鑑SPI的設計思想、Java和美團平臺的ServiceLoader實現,開發了本身的ServiceLoader模塊,解決外賣平臺化過程當中的四個問題(通訊問題、複用問題、依賴注入、編譯問題)。在通過了近一年的不斷迭代完善後,WMRouter已經成爲美團多個App中的核心基礎組件之一。
子健,美團高級工程師,2015年加入美團,前後負責外賣客戶端首頁、商家容器、評價等業務模塊的開發維護,以及平臺化、性能優化等技術工做。
淵博,美團高級工程師,2016年加入美團,目前做爲外賣商家端Android App主力開發,主要負責商家端和蜜蜂端業務技術需求開發。
雲馳,美團高級工程師,2016年加入美團,目前負責外賣客戶端搜索、IM等業務庫,及外賣多端統一工做。
美團外賣誠招Android、iOS、FE高級/資深工程師和技術專家,Base北京、上海、成都,歡迎有興趣的同窗投遞簡歷到wukai05@meituan.com。