反思 | 事件總線的侷限性,及組件化開發流程中通訊機制的設計與實現

反思 系列博客是個人一種新學習方式的嘗試,該系列起源和目錄請參考 這裏html

背景

諸如EventBus\RxBus\LiveDataBus的事件總線庫在業內正遭濫用。java

誠然,事件總線看起來 小而美 ,但隨着業務複雜度上升,事件的發送和訂閱處處分佈,這個優點反而成爲了負擔,所以,筆者不建議在任何量級的項目中使用事件總線庫。更多緣由讀者可參考 這篇文章android

更合理的方案是什麼呢?在量級較小的項目中,開發者應該經過 依賴注入Callback進行不一樣層級的依次傳遞,以保證 層級間的依賴關係足夠清晰git

而對於體量逐漸增大的項目而言,項目的模塊化、組件化、插件化改造被提上日程,各團隊負責不一樣的業務線,將業務分割成組件,並基於組件自己進行開發,因而咱們有了新的訴求,即 組件與組件保證是隔離的,同層級的組件間不該該持有其它組件中類的引用。github

須要注意的是,即便項目組件化,組件間也仍有通訊的場景,但這並不是使用事件總線的藉口——對大致量的項目而言,EventBus\RxBus\LiveDataBus這種事件總線庫太侷限了,其能力已徹底知足不了項目架構的需求,所以,一個適用於組件化開發的 通訊組件 的需求迫在眉睫。數據庫

本文將對組件化開發流程中 通訊組件 的設計理念與實現方式進行完整的敘述。這裏的 通訊組件 並不是特指某個已有的工具庫(好比ARouterWMRouter等),事實上,它們都是組件化開發流程的實踐之一。json

本文結構以下:數組

1、組件間通訊的基本實現

一、Android原生通訊機制

對於組件間通訊,最經典的場景當屬頁面跳轉,對於Android而言,Activity之間相互隔離,原生API對頁面跳轉提供了兩種實現方式:緩存

第一種方式是經常使用的 顯式意圖,經過 startActivitystartActivityForResult,這種方案簡單且實用,但在組件化開發流程中,組件間未持有其它組件中Activity.class的引用,所以沒法支持組件間的跳轉。性能優化

第二種方式則是相對冷僻的 隱式意圖,這種方式支持組件間以及跨進程通訊,好比,開發者能夠經過隱式意圖喚起系統的呼叫頁面:

// 喚起撥號頁面
private void call() {
    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_CALL);
    intent.setData(Uri.parse("tel:" + 119));
    startActivity(intent);
}
複製代碼

因爲代碼中不存在類依賴的關係,隱式意圖更適合組件間通訊,但其缺陷也很明顯:

  • 1.隱式意圖需將Activity對應的配置規則和參數以action等標籤的形式,集中聲明在Manifest中,不利於參數的管理,且擴展性不佳,進而致使團隊協做困難;
  • 2.開發者對路由控制能力不強,因爲整個路由跳轉行爲都由系統控制,所以,當路由出現異常時,沒法進行自定義補救,好比跳轉一個錯誤頁面(相似H5的404)。

如今看來,原生API對組件間頁面跳轉能力的提供,確實還略有不足,但這依然不是真正的問題所在。

能真正引爆這些定時炸彈的,只有 業務需求 自己。

二、導火索

即便Google推出了Navigation架構組件,不少開發者依然對這種單ActivityFragment的開發模式不買帳——無緣無故增長項目複雜度毫無心義

不管如何,一個簡單的計算器app也無必要引入複雜的工程架構,以及組件/插件化的開發流程。

所以,與其熱火朝天討論某個新框架流行與否,讀者更想看到它究竟是解決了什麼問題。

那就是業務的 爆炸性增加

隨着微信、支付寶等一衆大型和中型應用規模逐漸擴大,即便是原生的跳起色制也沒法知足組件化開發的需求,好比,首頁的若干個Tab對應的不一樣Fragment身處不一樣組件,這時Fragment之間的通訊該如何保障?

同時,隨着業務粒度的愈發細分,甚至單個Fragment中的View都來自五湖四海(好比商品詳情頁面, 視頻預覽商品評論 的控件分別由不一樣業務組件提供); 更深刻思索一下,若商品介紹一欄是由WebView提供的——涉及到H5和原生的交互,咱們又該如何定義H5與原生間通訊的接口?

因而可知,Activity自身的通訊機制確實已經不夠用了。

三、組件間通訊的基本實現

對於多元化的通訊需求而言,首先最重要的是將通訊協議進行統一,不管是Activity間跳轉,仍是FragmentView之間的通訊,亦或是H5與原生的交互,咱們都經過相似httpurl的形式定義:

// 跳轉 用戶模塊 - 登陸頁面
String loginUrl = "route://com.example.route/user/activity/login"
// 跳轉 用戶模塊 - 註冊頁面
String registUrl = "route://com.example.route/user/activity/register"

// 跳轉 商品模塊 - 詳情頁面, id爲商品的id
String detailUrl = "route://com.example.route/buy/activity/detail?id=xxxxx"    
複製代碼

定義好了以後,對於組件間頁面跳轉,能夠以下操做:

Router.route(detailUrl);  // 在用戶模塊,發起商品模塊中頁面的跳轉
複製代碼

應用接收到這樣自定義、且支持攜帶參數的url,通訊庫內部解析後統一分發,進行對應頁面的跳轉,這樣咱們就實現了最基礎的通訊功能。

四、降級策略與攔截器機制

接下來咱們針對 隱式意圖錯誤處理能力不足 這點進行深刻性討論。

在組件化開發流程中,開發者一般在當前的組件的Demo上進行開發,雖然模塊自身是可運行的,可是當涉及到其它組件的通訊,問題隨之而來。

和完整的工程相比,Demo上未持有其它組件中Activity的聲明,直接經過 隱式意圖 發起通訊會致使系統拋出異常。

那麼,咱們但願當通訊發生錯誤時,能夠針對不一樣的環境提供不一樣的降級策略,以保證開發者和用戶的體驗,好比:

  • Demo工程的開發流程中,當嘗試跳轉其它組件時,得到一個「該url不在當前組件工程中」的提示;
  • 在集成了全部組件的主工程中,在遇到不合法的url時,則爲用戶跳轉一個通用的404頁面。

若有可能,通訊庫在路由的過程當中,能提供限速、屏蔽等 靈活簡單 控制的可能性,那麼就更好了。

所以,以ARouter爲首的絕大多數組件通訊庫都提供了這種能力,實現方式也使用了很是經典的 攔截器 機制,經過 遞歸 將通訊事件向下分發,在須要處理的層級中進行攔截處理。

五、潑冷水時間?

本小節筆者將以ARouter爲例,闡述頁面間路由庫的一些侷限性,以及致使這些侷限性的緣由。

毫無疑問,ARouter提供了足夠強大的頁面間路由跳轉能力,它也確實攬括了業內絕大多數開發者的青睞,在開源之初,做者對其的定義就是Android平臺上的 頁面路由框架

這也變相致使自身對UI層級的跳轉能力很強,但對數據通訊的支持很薄弱。

什麼是對數據通訊的支持呢?讀者知道,除了可見的UI交互,數據的交互也很是頻繁,好比經過組件間通訊,向用戶組件獲取當前用戶信息、向訂單組件獲取某個訂單數據等等。

ARouter並不支持這些嗎?實際上並不是如此,ARouter自身提供了IProvider接口實現組件間服務的管理,並提供服務的自動註冊和依賴注入。

但遺憾的是,因爲ARouter自身設計緣由,其初始化只針對當前進程,這也致使了其路由表的自動註冊和攔截器相關機制都是單進程的。

而在目前國內多進程、插件化的多元發展環境下,若想向其它進程的服務直接獲取數據,ARouter是無能爲力的,須要開發者經過AIDL等方式來本身實現。

六、洗白

那麼致使這些侷限性的緣由,是由於ARouter這類頁面路由庫自身設計的不足嗎,並不是徹底如此,從技術角度而言,爲ARouter添加進程間通訊的支持是可行的。

大而全的框架每每也是摻雜了各類私貨的大雜燴,看似 功能強大 ,實則 臃腫不堪 —— 筆者更喜歡相似Retrofit的設計,將網絡請求的功能 收斂,並將 反序列化返回類型網絡請求擴展 等相關功能經過ConverterAdapterInterceptor的方式抽象出來,交給開發者選擇性依賴後,再自行組裝,Retrofit自身則毫不多幹涉一分一釐。

一樣,做爲 頁面路由框架ARouter目前的設計已知足現有須要。對於進程間通訊,ARouter能夠在IProvider的實現中,經過聲明AIDL進行通訊,最終將結果交還給ARouter去分發。這也正符合了其開源時所提倡的口號:簡單夠用

如今咱們知道,對於業務量級不大,尚以 頁面跳轉 爲主要通訊手段的應用而言,ARouter這類 頁面路由框架 已足夠使用;可是,對於更爲複雜的項目而言,組件間 數據獲取 更加頻繁,做爲設計者,如何保證靈活性的同時,提供更便捷數據通訊的可能呢?

2、更高維度的支持

從更高維度的視角來看,不管是UI層級的 頁面跳轉,仍是業務層級的 數據獲取,均可將其抽象爲一種 通訊:

一、通訊和通訊結果的定義

對此,咱們能夠對通訊協議進行以下的定義:

// 跳轉 用戶模塊 - 登陸頁面
String loginUrl = "route://com.example.route/user/activity/login"

// 獲取 用戶模塊 - 用戶數據
String getUserName = "route://com.example.route/user/service/getUserName"
複製代碼

咱們能夠像http請求同樣,對頁面跳轉通訊的結果進行以下結構的定義:

// 跳轉頁面的返回值
{
  "code" : "0000",      // 跳轉失敗,能夠定義一個錯誤碼,好比 "4000"
  "msg"  : "success",
  "data" : null
}
複製代碼

而對於數據獲取的定義,則能夠充分利用data字段:

// 獲取用戶信息
{
  "code" : "0000",
  "msg"  : "success",
  "data" : {
    "userName": "James Moriarty",
    "token": "xxxxxxx"
  }
}
複製代碼

這樣,不管是哪一種通訊,咱們都將通訊的結果抽象爲了Result,並在代碼中進行對應的處理:

class Result {
   @NonNull String code;
   @NonNull String msg;
   @Nullable Object data;
}

// 根據不一樣種類的通訊行爲,分別處理result
Result result =  Router.route(url); // url能夠是跳轉頁面,也能夠是獲取數據
複製代碼

如今咱們提供了基本的 UI通訊數據通訊 的支持,並將Result返回,可是目前的實現仍是沒法知足全部的場景——服務間的通訊並不是都是同步的。

二、異步通訊的支持

對於異步的通訊,咱們一般理解爲 網絡請求 ,實際上,網絡請求只是 數據異步通訊 的一部分,除此以外還有 數據庫操做文件的讀寫 等等。

難道只有 數據通訊 纔有異步的場景嗎?固然不是, UI通訊 中的異步場景一樣很是多,最簡單的例子就是startActivityForResult,咱們但願將登陸的行爲交給通訊庫,通訊庫異步跳轉登陸頁面,登陸成功後,返回以下定義的登陸結果:

{
  "code" : "0000",    // 登陸成功,也可對登陸失敗、取消定義不一樣code
  "msg"  : "success",
  "data" : {          // 返回用戶登陸信息
    "userName": "James Moriarty",
  }
}
複製代碼

在咱們的組件中,就能夠針對異步行爲進行以下通訊:

Callback<Result> callback =  Router.routeAsync(loginUrl);
// 執行異步通訊
callback.excute(result -> {
    // 登陸頁面登陸結果(或網絡請求結果)返回後,進行處理
});
複製代碼

這樣,不管是網絡請求,仍是異步UI登陸,咱們都將通訊的結果,抽象爲一個回調函數,將具體的實現內置在通訊庫中,其它組件的開發者無需關注實現的細節:

對於UI通訊而言,如何實現成這樣的API? 舉例來講,咱們能夠將ActivityonActivityResult()委託給一個不可見的Fragment處理,感興趣的讀者可參考Glide或者ViewModel的源碼。

三、多進程的支持

本小節部份內容節選自 @Spiny這篇文章

目前,由於自己是JVM級別的單例模式,所以咱們Router並不支持跨進程通訊。

上文咱們也一樣提到了,想進行跨進程通訊也很簡單,只須要在接收到須要跨進程通訊的url時,本身實現跨進程的調用便可。

既然如今咱們的Router已經脫離了相似ARouter這種 頁面路由框架 的範疇,將UI和業務都在更高維度進行了抽象,那麼,可否提供針對Router自己提供更強大的支持呢,好比跨進程通訊?

其實解決的方法也並不複雜。原來的路由系統還能夠繼續使用,咱們能夠把整套架構想象成互聯網,如今多個進程有多個Router,咱們只須要把多個Router鏈接到一塊兒,那麼整個路由系統仍是能夠正常運行的。因此咱們把原有的Router稱之爲本地路由LocalRouter

如今,咱們須要提供一個IPS、DNS供應商,那就建立一個進程,該進程的做用就是註冊路由,連接路由,轉發報文,咱們稱之爲廣域路由WideRouter

咱們先來看下路由鏈接架構圖:

如圖所示,豎直方向上,每一列,表明一個進程,經過虛線隔開,分別有 Process WideRouterProcess MainProcess A、···、Process N 這些進程。淺黃色的表明 WideRouter,深黃色 的表明 WideRouter 的守護 Service。淺藍色 的表明每一個進程的 LocalRouter,深藍色 的表明每一個 LocalRouter 的守護 Service

WideRouter 經過 AIDL 與每一個進程 LocalRouter 的守護 Service 綁定到一塊兒,每一個 LocalRouter 也是經過 AIDLWideRouter 的守護 Service 綁定到一塊兒,這樣,就達到了全部路由都是雙向互連的目的。

除了AIDL以外,市場上的通訊庫還有各類各樣跨進程通訊的實現方案,例如BroadcastReceiver、Socket、ContentProvider、Binder等等,有興趣的讀者能夠查看文末的參考連接,分別對比它們不一樣的實現方式。

3、更多元化的設計

目前,咱們已經完成了組件間通訊機制核心功能的實現。接下來咱們針對其它部分的功能,針對不一樣開源框架中的不一樣實現方式,進行簡單的討論。

一、組件的自動註冊

不一樣的組件各自向外暴露不一樣的功能,咱們須要將url和對應的邏輯進行綁定,以保證Router可以在接收到對應通訊的url時,做出對應的響應,這個流程咱們稱之爲組件的註冊。

舉例來講,在完整的項目工程中,咱們對全部組件的url進行註冊;而在組件自身的demo中,咱們對demo自身所須要的組件進行註冊。

那麼,對於高度組件化的項目而言,組件的粒度切分的很是細,這時在代碼中手動對組件一一註冊成爲了一個苦力活,所以,是否有必要設計一個技術方案,保證在應用啓動時,通訊庫可以對應用依賴的全部組件進行自動註冊呢?

1.1 不實現自動註冊的理由

首先咱們先討論,通訊庫不實現自動註冊的理由。

不提供自動註冊是一種偷懶嗎?筆者認爲不徹底是,手動註冊的好處在於,首先,開發者對註冊的組件老是已知的——這最簡單且直接地提供了組件動態化可插拔的能力,且不易出錯。

其次,手動註冊的方式,可以更靈活對應用的啓動性能優化進行保障,並不是全部組件都須要在應用啓動時進行當即註冊,當組件不少時,組件的註冊成本是否會影響App啓動的速度?這些問題都是須要去考量的。

1.2 APT實現自動註冊

而對於自動註冊,最大的問題在於如何找到全部組件中url的映射關係,而後對其自動註冊處理,而若是在運行期處理則有可能會大量地運用 反射,所以這種方案並不是首選。

對此,以ARouter爲表明的通訊庫使用到了 註解處理器AnnotationProcessor),經過在編譯期對項目進行掃描處理,解析註解,找出全部組件中對應的映射關係,而後存入並生成對應的映射文件類;在運行時,對這些組件的映射文件類進行一一註冊,從而完成整個項目的自動註冊。

表面來看,註解處理器 已經知足了咱們的需求,實際上還有一個隱藏的問題,那就是編譯時註解的特性只在源碼編譯時生效,並不能針對aar文件中的註解進行掃描,所以,咱們還須要保證APP在啓動時能找到全部的映射文件類,不然註冊根本無從談起。

ARouter曾經的實現方案是第一次啓動對全部dex文件進行讀取,遍歷每一個entry查找指定包內的全部類名,而後反射獲取指定的類對象,統一進行註冊,雖然初次效率並非很是高,但最後會進行本地緩存,以保證以後啓動註冊的效率。

1.3 編譯期字節碼修改註冊

有沒有更高效的註冊方式呢?

CC 組件通訊庫提供了另一種 編譯期修改字節碼 的實現方案,大體思路是:在編譯時,掃描全部類,將符合條件的類收集起來,並經過修改字節碼生成註冊代碼到指定的管理類中,從而實現編譯時自動註冊的功能,不用再關心項目中有哪些組件類了。不會增長新的class,不須要反射,運行時直接調用組件的構造方法。

對這種方案感興趣的讀者能夠參考這篇文章.

因而可知,即便是組件的註冊流程,各個庫的維護者都作出了各類各樣的實踐,而只有明白了每種方案的設計理念,才能對庫自己的適用場景有更清晰的認知。

二、依賴注入,從Square到Google?

ARouter在頁面的跳轉上提供了一個不一樣於其它通訊庫的功能,那就是可以將發起頁面跳轉時傳入的參數,經過依賴注入的方式自動注入到對應的Activity中。

這篇文章中闡述了該功能是如何實現的,頗有趣的是,在該功能最初的實現方案中,是運行期經過反射拿到ActivityThread實例,最終在Activity實例化的時候,經過反射把Intent預先存好的參數值寫入到須要自動裝配的字段中實現的。

這種方案的缺點很明顯,除了反射帶來的性能影響外,甚至可能致使用戶的代碼出現NPE,所以這種實現方式後來被新的方案所代替。

新的方案依然是咱們的老朋友AnnotationProcessor,在編譯期間,其爲Activity生成一個對應的注入輔助類,運行時經過輔助類對Activity中的字段進行賦值。

這也是Square最初推出的依賴注入庫dagger,被Google後來居上的dagger2代替的緣由。

還有另一個問題,爲何其它通訊庫沒有像ARouter同樣提供這樣一個依賴注入的功能呢,是由於作不到嗎?

並不是如此,本文中咱們將UI與業務之間的通訊,統一爲更高維度的抽象,所以,若是設計一個新的功能,新功能也應是對 通訊 總體的概念服務,而非部分場景。

小結

本文針對組件化開發流程中核心的 通訊機制 進行了系統性的描述。

對於組件化而言,其目的在於在 業務模塊間的解耦,而事件總線除了能給開發者帶來開發上暫時的便利,以及 貌似解耦 的假象以外,更多埋下了組件間依賴關係 混亂的種子,並不是長久之計——更合理的方案是針對性引入適合自身項目、且更全面的組件間通訊庫。

篇幅所限,不少優秀的開源項目中的功能和設計未能一一闡述,有興趣的讀者能夠從下文的連接中進行選擇性的參考。

參考 & 感謝

細心的讀者可以發現,關於 參考&感謝 一節,筆者着墨愈來愈多,緣由無他,筆者 從不認爲 一篇文章就可以講一個知識體系講解的面面俱到,本文亦如是。

所以,讀者應該有選擇性查看其它優質內容的權利,甚至是爲其增長一些簡潔的介紹(由於標題大多都很類似),而不是文章末尾甩一堆https開頭的連接不知所云。

這也是對這些內容創做者的尊重,若是你喜歡本文,也一樣但願你可以喜歡下面這些文章。

一、開源最佳實踐:Android平臺頁面路由框架ARouter @劉志龍

對於ARouter的創做流程和設計理念,沒有比做者本人更有發言權的了,這篇文章從理論到實踐都講解的很是清晰、流暢且天然,對於想要深刻學習ARouter的讀者不要錯過。

二、Android架構思考:模塊化、多進程 @Spiny

相比較ARouter, ModularizationArchitecture 這個通訊庫及其做者彷佛更低調,但從文章中能夠得知,做者本人對組件化的理解很是深刻,尤爲是將進程間通訊機制的實現,比喻爲互聯網,很是易於理解,所以直接將部分原文放在了 多進程的支持 一節,再次感謝!

三、Android組件化之(路由 vs 組件總線) @luckybilly

這篇文章是CC的做者的原創文章,針對 路由組件總線 進行了深刻的對比,很是深刻,推薦。

四、多個維度對比一些有表明性的開源android組件化開發方案 @luckybilly

五、一種更高效的組件自動註冊方案(android組件化開發)

luckybilly的另兩篇好文,前者針對市面上一衆主流的通訊庫進行了不一樣角度的對比,後者針對組件自動註冊的不一樣實現進行了深刻的對比,強烈推薦!

六、WMRouter:美團外賣Android開源路由框架

美團開源的WMRouter介紹文章,有興趣的讀者可做爲引伸閱讀。


關於我

Hello,我是 卻把清梅嗅 ,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人 博客 或者 GitHub

若是您以爲文章還差了那麼點東西,也請經過 關注 督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章
相關標籤/搜索