反思 系列博客是個人一種新學習方式的嘗試,該系列起源和目錄請參考 這裏 。html
諸如EventBus\RxBus\LiveDataBus
的事件總線庫在業內正遭濫用。java
誠然,事件總線看起來 小而美 ,但隨着業務複雜度上升,事件的發送和訂閱處處分佈,這個優點反而成爲了負擔,所以,筆者不建議在任何量級的項目中使用事件總線庫。更多緣由讀者可參考 這篇文章 。android
更合理的方案是什麼呢?在量級較小的項目中,開發者應該經過 依賴注入 將Callback
進行不一樣層級的依次傳遞,以保證 層級間的依賴關係足夠清晰。git
而對於體量逐漸增大的項目而言,項目的模塊化、組件化、插件化改造被提上日程,各團隊負責不一樣的業務線,將業務分割成組件,並基於組件自己進行開發,因而咱們有了新的訴求,即 組件與組件保證是隔離的,同層級的組件間不該該持有其它組件中類的引用。github
須要注意的是,即便項目組件化,組件間也仍有通訊的場景,但這並不是使用事件總線的藉口——對大致量的項目而言,EventBus\RxBus\LiveDataBus
這種事件總線庫太侷限了,其能力已徹底知足不了項目架構的需求,所以,一個適用於組件化開發的 通訊組件 的需求迫在眉睫。數據庫
本文將對組件化開發流程中 通訊組件 的設計理念與實現方式進行完整的敘述。這裏的 通訊組件 並不是特指某個已有的工具庫(好比ARouter
、WMRouter
等),事實上,它們都是組件化開發流程的實踐之一。json
本文結構以下:數組
對於組件間通訊,最經典的場景當屬頁面跳轉,對於Android
而言,Activity
之間相互隔離,原生API
對頁面跳轉提供了兩種實現方式:緩存
第一種方式是經常使用的 顯式意圖,經過 startActivity
或 startActivityForResult
,這種方案簡單且實用,但在組件化開發流程中,組件間未持有其它組件中Activity.class
的引用,所以沒法支持組件間的跳轉。性能優化
第二種方式則是相對冷僻的 隱式意圖,這種方式支持組件間以及跨進程通訊,好比,開發者能夠經過隱式意圖喚起系統的呼叫頁面:
// 喚起撥號頁面
private void call() {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:" + 119));
startActivity(intent);
}
複製代碼
因爲代碼中不存在類依賴的關係,隱式意圖更適合組件間通訊,但其缺陷也很明顯:
Activity
對應的配置規則和參數以action
等標籤的形式,集中聲明在Manifest
中,不利於參數的管理,且擴展性不佳,進而致使團隊協做困難;如今看來,原生API
對組件間頁面跳轉能力的提供,確實還略有不足,但這依然不是真正的問題所在。
能真正引爆這些定時炸彈的,只有 業務需求 自己。
即便Google
推出了Navigation
架構組件,不少開發者依然對這種單Activity
多Fragment
的開發模式不買帳——無緣無故增長項目複雜度毫無心義。
不管如何,一個簡單的計算器app
也無必要引入複雜的工程架構,以及組件/插件化的開發流程。
所以,與其熱火朝天討論某個新框架流行與否,讀者更想看到它究竟是解決了什麼問題。
那就是業務的 爆炸性增加。
隨着微信、支付寶等一衆大型和中型應用規模逐漸擴大,即便是原生的跳起色制也沒法知足組件化開發的需求,好比,首頁的若干個Tab
對應的不一樣Fragment
身處不一樣組件,這時Fragment
之間的通訊該如何保障?
同時,隨着業務粒度的愈發細分,甚至單個Fragment
中的View
都來自五湖四海(好比商品詳情頁面, 視頻預覽 和 商品評論 的控件分別由不一樣業務組件提供); 更深刻思索一下,若商品介紹一欄是由WebView
提供的——涉及到H5
和原生的交互,咱們又該如何定義H5
與原生間通訊的接口?
因而可知,Activity
自身的通訊機制確實已經不夠用了。
對於多元化的通訊需求而言,首先最重要的是將通訊協議進行統一,不管是Activity
間跳轉,仍是Fragment
、View
之間的通訊,亦或是H5
與原生的交互,咱們都經過相似http
的url
的形式定義:
// 跳轉 用戶模塊 - 登陸頁面
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
的設計,將網絡請求的功能 收斂,並將 反序列化、返回類型、網絡請求擴展 等相關功能經過Converter
、Adapter
、Interceptor
的方式抽象出來,交給開發者選擇性依賴後,再自行組裝,Retrofit
自身則毫不多幹涉一分一釐。
一樣,做爲 頁面路由框架 ,ARouter
目前的設計已知足現有須要。對於進程間通訊,ARouter
能夠在IProvider
的實現中,經過聲明AIDL
進行通訊,最終將結果交還給ARouter
去分發。這也正符合了其開源時所提倡的口號:簡單 且 夠用。
如今咱們知道,對於業務量級不大,尚以 頁面跳轉 爲主要通訊手段的應用而言,ARouter
這類 頁面路由框架 已足夠使用;可是,對於更爲複雜的項目而言,組件間 數據獲取 更加頻繁,做爲設計者,如何保證靈活性的同時,提供更便捷數據通訊的可能呢?
從更高維度的視角來看,不管是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? 舉例來講,咱們能夠將
Activity
的onActivityResult()
委託給一個不可見的Fragment
處理,感興趣的讀者可參考Glide
或者ViewModel
的源碼。
本小節部份內容節選自 @Spiny 的 這篇文章。
目前,由於自己是JVM
級別的單例模式,所以咱們Router
並不支持跨進程通訊。
上文咱們也一樣提到了,想進行跨進程通訊也很簡單,只須要在接收到須要跨進程通訊的url
時,本身實現跨進程的調用便可。
既然如今咱們的Router
已經脫離了相似ARouter
這種 頁面路由框架 的範疇,將UI
和業務都在更高維度進行了抽象,那麼,可否提供針對Router
自己提供更強大的支持呢,好比跨進程通訊?
其實解決的方法也並不複雜。原來的路由系統還能夠繼續使用,咱們能夠把整套架構想象成互聯網,如今多個進程有多個Router
,咱們只須要把多個Router
鏈接到一塊兒,那麼整個路由系統仍是能夠正常運行的。因此咱們把原有的Router
稱之爲本地路由LocalRouter
。
如今,咱們須要提供一個IPS、DNS
供應商,那就建立一個進程,該進程的做用就是註冊路由,連接路由,轉發報文,咱們稱之爲廣域路由WideRouter
。
咱們先來看下路由鏈接架構圖:
如圖所示,豎直方向上,每一列,表明一個進程,經過虛線隔開,分別有 Process WideRouter
、Process Main
、Process A
、···、Process N
這些進程。淺黃色的表明 WideRouter
,深黃色 的表明 WideRouter
的守護 Service
。淺藍色 的表明每一個進程的 LocalRouter
,深藍色 的表明每一個 LocalRouter
的守護 Service
。
WideRouter
經過 AIDL
與每一個進程 LocalRouter
的守護 Service
綁定到一塊兒,每一個 LocalRouter
也是經過 AIDL
與 WideRouter
的守護 Service
綁定到一塊兒,這樣,就達到了全部路由都是雙向互連的目的。
除了
AIDL
以外,市場上的通訊庫還有各類各樣跨進程通訊的實現方案,例如BroadcastReceiver、Socket、ContentProvider、Binder
等等,有興趣的讀者能夠查看文末的參考連接,分別對比它們不一樣的實現方式。
目前,咱們已經完成了組件間通訊機制核心功能的實現。接下來咱們針對其它部分的功能,針對不一樣開源框架中的不一樣實現方式,進行簡單的討論。
不一樣的組件各自向外暴露不一樣的功能,咱們須要將url
和對應的邏輯進行綁定,以保證Router
可以在接收到對應通訊的url
時,做出對應的響應,這個流程咱們稱之爲組件的註冊。
舉例來講,在完整的項目工程中,咱們對全部組件的url
進行註冊;而在組件自身的demo
中,咱們對demo
自身所須要的組件進行註冊。
那麼,對於高度組件化的項目而言,組件的粒度切分的很是細,這時在代碼中手動對組件一一註冊成爲了一個苦力活,所以,是否有必要設計一個技術方案,保證在應用啓動時,通訊庫可以對應用依賴的全部組件進行自動註冊呢?
首先咱們先討論,通訊庫不實現自動註冊的理由。
不提供自動註冊是一種偷懶嗎?筆者認爲不徹底是,手動註冊的好處在於,首先,開發者對註冊的組件老是已知的——這最簡單且直接地提供了組件動態化可插拔的能力,且不易出錯。
其次,手動註冊的方式,可以更靈活對應用的啓動性能優化進行保障,並不是全部組件都須要在應用啓動時進行當即註冊,當組件不少時,組件的註冊成本是否會影響App
啓動的速度?這些問題都是須要去考量的。
而對於自動註冊,最大的問題在於如何找到全部組件中url
的映射關係,而後對其自動註冊處理,而若是在運行期處理則有可能會大量地運用 反射,所以這種方案並不是首選。
對此,以ARouter
爲表明的通訊庫使用到了 註解處理器(AnnotationProcessor
),經過在編譯期對項目進行掃描處理,解析註解,找出全部組件中對應的映射關係,而後存入並生成對應的映射文件類;在運行時,對這些組件的映射文件類進行一一註冊,從而完成整個項目的自動註冊。
表面來看,註解處理器 已經知足了咱們的需求,實際上還有一個隱藏的問題,那就是編譯時註解的特性只在源碼編譯時生效,並不能針對aar
文件中的註解進行掃描,所以,咱們還須要保證APP
在啓動時能找到全部的映射文件類,不然註冊根本無從談起。
ARouter
曾經的實現方案是第一次啓動對全部dex
文件進行讀取,遍歷每一個entry
查找指定包內的全部類名,而後反射獲取指定的類對象,統一進行註冊,雖然初次效率並非很是高,但最後會進行本地緩存,以保證以後啓動註冊的效率。
有沒有更高效的註冊方式呢?
CC 組件通訊庫提供了另一種 編譯期修改字節碼 的實現方案,大體思路是:在編譯時,掃描全部類,將符合條件的類收集起來,並經過修改字節碼生成註冊代碼到指定的管理類中,從而實現編譯時自動註冊的功能,不用再關心項目中有哪些組件類了。不會增長新的class
,不須要反射,運行時直接調用組件的構造方法。
對這種方案感興趣的讀者能夠參考這篇文章.
因而可知,即便是組件的註冊流程,各個庫的維護者都作出了各類各樣的實踐,而只有明白了每種方案的設計理念,才能對庫自己的適用場景有更清晰的認知。
ARouter
在頁面的跳轉上提供了一個不一樣於其它通訊庫的功能,那就是可以將發起頁面跳轉時傳入的參數,經過依賴注入的方式自動注入到對應的Activity
中。
這篇文章中闡述了該功能是如何實現的,頗有趣的是,在該功能最初的實現方案中,是運行期經過反射拿到ActivityThread
實例,最終在Activity
實例化的時候,經過反射把Intent
預先存好的參數值寫入到須要自動裝配的字段中實現的。
這種方案的缺點很明顯,除了反射帶來的性能影響外,甚至可能致使用戶的代碼出現NPE
,所以這種實現方式後來被新的方案所代替。
新的方案依然是咱們的老朋友AnnotationProcessor
,在編譯期間,其爲Activity
生成一個對應的注入輔助類,運行時經過輔助類對Activity
中的字段進行賦值。
這也是Square
最初推出的依賴注入庫dagger
,被Google
後來居上的dagger2
代替的緣由。
還有另一個問題,爲何其它通訊庫沒有像ARouter
同樣提供這樣一個依賴注入的功能呢,是由於作不到嗎?
並不是如此,本文中咱們將UI
與業務之間的通訊,統一爲更高維度的抽象,所以,若是設計一個新的功能,新功能也應是對 通訊 總體的概念服務,而非部分場景。
本文針對組件化開發流程中核心的 通訊機制 進行了系統性的描述。
對於組件化而言,其目的在於在 業務模塊間的解耦,而事件總線除了能給開發者帶來開發上暫時的便利,以及 貌似解耦 的假象以外,更多埋下了組件間依賴關係 混亂的種子,並不是長久之計——更合理的方案是針對性引入適合自身項目、且更全面的組件間通訊庫。
篇幅所限,不少優秀的開源項目中的功能和設計未能一一闡述,有興趣的讀者能夠從下文的連接中進行選擇性的參考。
細心的讀者可以發現,關於 參考&感謝 一節,筆者着墨愈來愈多,緣由無他,筆者 從不認爲 一篇文章就可以講一個知識體系講解的面面俱到,本文亦如是。
所以,讀者應該有選擇性查看其它優質內容的權利,甚至是爲其增長一些簡潔的介紹(由於標題大多都很類似),而不是文章末尾甩一堆
https
開頭的連接不知所云。這也是對這些內容創做者的尊重,若是你喜歡本文,也一樣但願你可以喜歡下面這些文章。
一、開源最佳實踐:Android平臺頁面路由框架ARouter @劉志龍
對於ARouter
的創做流程和設計理念,沒有比做者本人更有發言權的了,這篇文章從理論到實踐都講解的很是清晰、流暢且天然,對於想要深刻學習ARouter
的讀者不要錯過。
相比較ARouter
, ModularizationArchitecture 這個通訊庫及其做者彷佛更低調,但從文章中能夠得知,做者本人對組件化的理解很是深刻,尤爲是將進程間通訊機制的實現,比喻爲互聯網,很是易於理解,所以直接將部分原文放在了 多進程的支持 一節,再次感謝!
三、Android組件化之(路由 vs 組件總線) @luckybilly
這篇文章是CC的做者的原創文章,針對 路由 和 組件總線 進行了深刻的對比,很是深刻,推薦。
四、多個維度對比一些有表明性的開源android組件化開發方案 @luckybilly
五、一種更高效的組件自動註冊方案(android組件化開發)
luckybilly的另兩篇好文,前者針對市面上一衆主流的通訊庫進行了不一樣角度的對比,後者針對組件自動註冊的不一樣實現進行了深刻的對比,強烈推薦!
美團開源的WMRouter介紹文章,有興趣的讀者可做爲引伸閱讀。
Hello,我是 卻把清梅嗅 ,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人 博客 或者 GitHub。
若是您以爲文章還差了那麼點東西,也請經過 關注 督促我寫出更好的文章——萬一哪天我進步了呢?