動態路由TheRouter的設計與實踐

語言: CN / TW / HK

這篇文章是我在 2022【GIAC 全球網際網路架構大會】分享時所講內容的文字版本,修改刪減了演講時的冗餘言語,現開放給大家閱讀,希望能給買不到票參加分享的 開源實驗室 讀者帶來幫助。

大家好,今天跟大家分享的是一個開源路由TheRouter的設計。
程式碼地址: https://github.com/HuolalaTech/hll-wp-therouter-android

先來看一下目錄 我們從三點,來講述今天的主題:

  • 分別是模組化的開始,如何通過路由去實現一個模組化。
  • 然後再根據目標,去設計一個動態化的路由解決我們的問題,以及在我們的專案中,是如何實踐的。
  • 最後,今年的大環境大家應該都知道,考慮一下如何在資源有限的情況下,推動工程的重構。

這裡有三張我手機上APP的截圖,分別是:貨拉拉、今日頭條、美團

他們基本上可以代表瞭如今市面上大部分APP的一個形態,在這四五年裡,網際網路公司大幅增加,而APP的業務功能也不斷增多。

從技術角度再看一下:

這是我列出來的一個APP的通用架構,這張圖基本可以覆蓋現如今百分之八九十的 APP 架構。

  1. 首先最上層是各個業務層 比方說是像貨拉拉的搬家、拉貨、運大件這種。
  2. 接下來是各個業務模組 比如常見的像使用者賬戶體系、然後可能有一些直播、音影片、支付這樣的場景模組。
  3. 再往下就是一些功能性的元件:他們可能跟具體的業務功能相關,比如推送、IM、廣告控制元件、這樣的一系列功能元件。
  4. 最底層就是基礎設施了: 就像資料上報 異常統計等等一系列的必要基礎能力。

然後在側面還有一些貫穿整個APP的能力 像CICD 國際化 端智慧 熱修復等等。

從這張圖我們也能看出現如今的APP是越來越複雜 功能也越來越多

對於功能越來越多,越來越複雜的APP架構,我們最直接能想到的就是通過模組化,將不同的功能、不同的業務做獨立拆分,分而治之,降低整個系統的複雜度。畢竟越簡單,邏輯越少的程式碼塊,BUG就越少。

所以大型 APP 的開發,基本都會選用模組化開發,同時對於模組間解耦要求更高。
而說到模組化,我們一定需要一個路由去承載不同模組之間的通訊。路由是現如今 Android 開發中必不可少的功能,尤其是企業級APP,可以用於將原生頁面跳轉的強依賴解耦,同時減少跨團隊開發的互相依賴問題。

比如UI層級的跳轉、功能模組的聯動呼叫,這是做模組化繞不開的兩點。

實現這兩點最常用的辦法也就是:分別將我們當前的一個UI頁面與一個uri關聯,用Uri替代我們的頁面,

這個樣子在跳轉的時候就不需要強依賴UI頁面去做匹配,而只需要通過一段字串去匹配就行

那另一種就是通過介面下沉,將模組依賴改為協議依賴,這樣 我們在不同的模組之間排程的時候 只需要依賴一個最基礎的協議或者說是介面 去實現就可以了

做完模組化以後,一個APP的複雜度已經被降低很多了。
但是有一個最大的問題,我們通過模組化是沒辦法解決的。

也就是APP依賴使用者去主動的更新升級,使用者不更新,那就是永遠在用舊版本, 當年,也是為了解決這個問題,催生出了很多黑科技,比如Android的外掛化、熱修復這種黑科技,最終這些科技最終也被驗證是點歪了的技能樹。

今天我跟大家講講另一種解決辦法:

回到我們今天的主題:動態化路由

前些天我們開源了一套,在安卓上面的動態化路由叫 TheRouter 他是一整套我們實現APP動態化的設計方案。包括模組化、包括遠端路由下發、包括前面剛才我列出來的幾個依賴使用者升級而造成的一些問題,我們都是通過他來解決的。

之所以叫TheRouter 因為 The 代表了一種唯一性,我們在設計的時候就參考了全部現有的開源方案,吸取了大量優秀實現,同時補齊了各個方案的缺點。我們認為做移動端的模組化,只需要看這一個就夠了。

首先我們來看一下行業內路由的設計方案,不管是頁面跳轉,還是跨模組呼叫,基本上都是

  1. 開發階段,對要使用路由的落地頁或被呼叫方法添加註解標識。
  2. 在編譯期解析註解,生成一系列中間程式碼,等待呼叫。
  3. 應用啟動後呼叫中間程式碼完成路由的準備動作。大部分路由會額外通過 Gradle Transform,在編譯期做一次聚合,以提升執行時準備路由表的效率。
  4. 發起路由跳轉時,本質上就是一次路由表遍歷,通過uri獲取到對應的落地頁或方法物件,進行呼叫。

跨模組呼叫也是類似,在開發時做標記,編譯時生成中間程式碼,執行時通過中間程式碼呼叫跨模組方法。

TheRouter 的整體實現邏輯也是按照這個思路去做的,不過我們對於各個細節的處理,有更好的解決辦法。

這是另一個角度,跟行業路由的一些對比資料。

大家可以主要關注這幾個點:

  • 第一個點: TheRouter是完全無執行時掃描,沒有任何反射程式碼的框架。
    當然因為引用了Gsonjson解析,他裡面應該是用了反射的,但這不在我們討論的範圍內,如果你願意我們允許自定義json解析框架,你可以換成其他的解析。

  • 第二點是TheRouter對增量編譯支援非常好,APTplugin都能做到增量編譯。
    同時我們內部也有一套基於最新KSP的註解處理程式碼,KSP是kotlin專門用於處理註解做的一套實現,我們之前用的都是kapt,但是kapt只能處理Kotlin類的註解,如果是KotlinJava混合的工程,他還沒辦法處理,所以在他內部還包了一層Javaapt,碰到他解析不了的檔案,就呼叫apt去解析,所以他的處理速度是非常慢的。
    而KSP是基於語法樹分析去做的,我們知道,所有的程式碼在編譯之前,都會先經過語法樹分析,他就是在這一步順帶把分析出來的詞法返回,讓我們做一些自己的定製邏輯。所以KSP其實不僅僅可以做註解處理,還可以做一些定製的語法分析規則,類似lint那種。

  • 第三點:TheRouter應該是現如今所有路由裡,唯一一個支援AGP8的。Gradle從7.X開始,內建了編譯過程處理的相關方法,所以AGP直接在8.0刪除了相同功能的方法,這就造成大量基於TransformAPI的庫,在AGP8都沒辦法使用了。

  • 最後一點也是我們之前碰到的坑,在用tinker這類熱修復框架的時候,由於路由編譯的產物程式碼是無序的,所以每次編譯都有可能發生改變,就造成我們的補丁包非常大。TheRouter對這一點也做了特殊支援,只要你沒有新增或改動路由相關的程式碼,編譯產物程式碼就不會有任何變動。

接下來需要大家一起思考一下,一個路由 他真正需要具備的核心能力是哪些。我前面PPT列了一下,參考現在業內的一些通用的路由解決方案 它真正核心需要解決的問題就兩個點:

  • 一個是解耦UI跳轉
  • 一個是降低系統依賴

我們把這兩個目標分別拆開。
在跳轉方面,除了業界常用的通過路由字串對映頁面UI之外,我們還加入了動態引數注入。
也就是一個UI頁面需要的預設引數可以通過路由表提前宣告好,而路由表可以是遠端下發的,那這些預設引數也可以是遠端下發的,這就做到了線上預設欄位的及時更新。

另一部分,降低依賴,除了常用的SPI介面下沉,將模組功能依賴改為介面協議依賴之外,我們還提供了業務節點的hook,所有模組可以反向訂閱所需的業務節點,並在業務發生時做自己的邏輯處理。

這一個能力最常用的地方,比如我們在做隱私合規的時候,要求使用者同意隱私協議以後,才能做一些敏感API的呼叫。在以前的開發,這些呼叫都得要放到隱私彈窗所在的模組內,當用戶點同意按鈕以後,再呼叫其他模組初始化方法。這種邏輯對模組化是非常難受的,因為增大了跨模組的溝通,如果團隊特別大,不同團隊負責不同模組的時候,這種溝通就很累了,假設初始化方法需要增加一個引數,還得額外處理。哪些能力是要一啟動就呼叫的,哪些API是必須使用者同意以後才能呼叫的,都得溝通清楚。

而我們做了業務節點訂閱以後,就把這種依賴某個業務節點的功能,做成了訂閱釋出模式,你只需要宣告初始化方法依賴使用者同意隱私協議就行了,在使用者同意以後就會自動呼叫初始化方法。

另外,我們還允許客戶端建立一套基於規則引擎的觸發與響應,可以全域性動態智慧處理使用者操作。假設客戶端此刻碰到什麼意外情況,比如一個女性使用者,在夜裡十一二點打車,路上又在某些偏僻點發生異常停留,客戶端可以主動做一些我們預置的事件,比如自動報警、語音或者影片自動聯絡我們的客服。比如像今年iPhone14的新功能,有個車禍檢測,如果車翻了或者撞車了,自動幫你打救援電話。而我們這一系列規則,都可以是動態響應的。

接下來看一下路由的設計細節

TheRouter 會在編譯期根據註解生成 RouteMap__開頭的類,這些類中記錄了當前模組的所有路由資訊,也就是當前模組的路由表。

在最頂層的app模組中,通過Gradle外掛,將所有aar、原始碼中的RouteMap__開頭的類統一集中到TheRouterServiceProvideInjecter類中。

後續應用啟動後,初始化路由時只需要執行TheRouterServiceProvideInjecter類的方法,就能沒有任何反射的載入到全部的路由表了。

載入以後的路由表會被儲存到一個支援正則匹配的 Map 中,這也是TheRouter允許多個path對應同一個落地頁的原因。每當發生頁面跳轉時,通過跳轉時的path,去Map中獲取到對應的落地頁資訊,再正常呼叫startActivity()即可。

對於模組化開發中跨模組的呼叫,我們推薦採用 SOA(面向服務架構) 的設計方式,服務呼叫方與使用方完全隔離,呼叫模組外的能力不需要關注能力的提供者是誰。 ServiceProvider 的核心設計思想也是這樣的,目前服務間的呼叫協議採用介面的方式。當然,也可以相容不通過介面下沉而是直接呼叫的情況。

具體到 Android 側就是 AIDL 類似的設計,只是要比AIDL開發簡單很多:

  • 服務提供方負責提供服務,不需要關心呼叫方是誰會在何時呼叫自己。
  • 服務的使用方只關注服務本身,不需要關心這個服務是誰提供的,只需要只能服務能提供哪些能力即可。

例如上面的圖片:服務使用方需要使用錄音的服務,服務提供方則向外提供一個錄音的服務,由TheRouterServiceProvider負責撮合。

服務使用方:

無需關心,IRecordService這個介面服務是誰提供的,他只需要知道自己需要使用這樣的一個服務就行了。 注:如果沒有提供服務的提供方,TheRouter.get()可能返回null

TheRouter.get(IRecordService::class.java)?.doRecord()

服務提供方:

服務提供方需要宣告一個提供服務的方法,用@ServiceProvider註解標記。

  • 如果是 java,必須是 public static 修飾
  • 如果是 kotlin,建議寫成 top level 的函式
  • 方法名不限

``` /* * 方法名不限定,任意名字都行 * 返回值必須是服務介面名,如果是實現了服務的子類,需要加上returnType限定(例如下面程式碼) * 方法必須加上 public static 修飾,否則編譯期就會報錯 / @ServiceProvider public static IRecordService test() { return new IRecordService() { @Override public void doRecord() { String str = "執行錄製邏輯"; } }; }

// 也可以直接返回物件,然後標註這個方法的服名是什麼 @ServiceProvider(returnType = IRecordService.class) public static RecordServiceImpl test() { // xxx } ```

前面講過,TheRouter是完全面向模組化開發提供的一套解決方案。

在模組化開發時,可能每個模組都有自己需要初始化的一些程式碼。以前的做法是把這些程式碼都在Application裡宣告,但是這樣可能隨著業務變動每次都需要修改Application所在模組。TheRouter 的單模組自動初始化能力就是為了解決這樣的情況,可以只在當前模組宣告初始化方法後,將會在業務場景時自動被呼叫。

每個希望被自動初始化的方法,必須使用public static修飾,主要原因是這樣子就能通過類名直接呼叫了。另外很多初始化程式碼都需要獲取Context物件,所以我們將Context作為初始化方法的預設引數,會自動傳入Application。其他的所在類名、方法名都沒有限制,反正只要加上了 @FlowTask 註解,在編譯期都能通過 APT 獲取到。

或者隱私合規的時候,有一些功能需要同意隱私協議才能呼叫。

跨模組依賴的時候,需要另一個模組初始化以後,才能呼叫當前模組的初始化,等等業務都可以用業務節點自主訂閱的方式去解耦。

每個加了 @FlowTask 註解的方法,都會在編譯期被解析,生成一個對應的 Task 物件,這個物件包含了初始化方法的相關資訊,比如:是否非同步執行、任務名、是否依賴其他任務先執行。

當所有aar都編譯完成,生成好全部的 Task 以後,會在主 app 中通過Gradle外掛進行聚合,在這時會將所有的 Task 做一次檢查,通過構建有向無環圖來防止 Task 發生迴圈引用的情況。

每次應用啟動後,會在路由初始化時,將有向圖中的全部Task,按照依賴關係按順序載入。

可以在當前模組中,任意類中宣告一個任意方法名的方法,給方法新增上@FlowTask 的註解即可。

@FlowTask 註解引數說明:

  • taskName:當前初始化任務的任務名,必須全域性唯一,建議格式為:moduleName_taskName
  • dependsOn:參考Gradle Task,任務與任務之間可能會有依賴關係。如果當前任務需要依賴其他任務先初始化,則在這裡宣告依賴的任務名。可以同時依賴多個任務,用英文逗號分隔,空格可選,會被過濾:dependsOn = “mmkv, config, login”,預設為空,應用啟動就被呼叫
  • async:是否要在非同步執行此任務,預設false。

最後一個,APP動態響應的實現。

還是回到之前的例子:假設一個女性、夜裡12點、KTV上車、偏僻地點停車,那麼我們就可以根據這樣的一系列先決條件,交由後端的智慧大腦分析,然後下發給客戶端一個動作:比如開啟影片或語音,讓客服介入。

而把這個例子抽象一下,所有使用者的操作,比如點選、曝光、頁面跳轉等等埋點資料,都可以作為分析資料交給服務端分析,然後讓客戶端執行:跳轉頁面、彈窗、優惠券、或者其他本地方法。

這樣的一個流程做完了以後,只要我們有一個可靠的行為分析模型,我們是大概率可以預測使用者接下來的行為是要做什麼的。

當然,即便我們沒有這樣一個使用者行為分析的大腦,純客戶端的方案,也是能夠支援的,這就是離線端智慧方案了。

最後我們再來看一下前面提到的幾個 APP 的弊端,在 TheRouter 中是怎麼解決的呢?

  • 第一個:頁面Crash,我們可以通過去修改路由表,然後我們把某一些頁面的 Crash 給它降級,降級成 H5 或者說是小程式。當假設我們這個頁面沒辦法訪問的時候,我們可以讓使用者先暫時地去訪問 H5 頁面或者說小程式頁面。同樣的,如果某個頁面白屏很久,我們也可以通過降級,直接通過H5或小程式的方式相容開啟。

  • 第二個:對於一些介面欄位,老版本的相容問題,我們也是能夠去下發預設引數的方式。如果老版本它強制要求有某一個引數,那其實我們可以把這個引數給下發成一個預設引數。如果我們做了千人千面的話,那每一個使用者都可以達到不同引數不同展示的效果。

  • 第三個:新功能透傳及時性。假設我們當前有某一個直播的頁面,新版本已經有一個可以讓使用者打賞或者說是讓使用者發禮物這樣的功能了。那老版本它還沒有這樣的一個功能的話,我們可以通過點選禮物圖示後,修改落地頁把它給他提示升級彈窗。這樣的升級彈窗對使用者是影響最小的,它只在使用到這個功能的時候才需要做某一些升級。

  • 第四個意外事件處理:就是我前面講到的雲端大腦或端智慧這樣的應用場景了。

最後我們來看今天的第三部分,今年的情況大家都能感受,各種人員優化,大家都很忙,那如何將這種大的技術重構成本降到最低呢,我們為TheRouter開發了很多周邊能力:

TheRouter提供了圖形化介面的一鍵遷移工具,可以一鍵從其他路由遷移到TheRouter,整個遷移過程都是基於字串匹配完成的,不涉及任何黑科技,所有的替換點也都會展示出來,非常安全。在替換完成後,自動輸出改動頁面與測試點,大幅減少了開發與測試的工作量。

還有一個用於自動跳轉的高效IDE輔助外掛,可以直接從路由的宣告處檢視到哪些地方跳轉到本路由,再也不用怕路由字串滿天飛了。

只需要點一下左邊的圖示,就能自動跳轉到落地頁了。假設我們有多個跳轉,跳轉到同一個落地頁的,點選落地頁左側的圖示,也會展示出對應的程式碼,選擇以後也可以自動跳轉過去。

另外還有一個很好的特性,就是如果你寫了沒有落地頁的跳轉,會在IDE左側有個黃色的警告,提示你是不是因為手抖或其他原因,寫錯了path

另外TheRouter還提供了官網和微信群,官網有大量的技術文件和指導教程,有不懂的問題還可以加入微信群尋求幫助。

官網:https://therouter.cn

微信群:https://therouter.cn/wx/

總的來說,TheRouter 並不僅僅是一個小巧靈活的路由庫,而是一整套完整的 Android 模組化解決方案,能夠解決幾乎全部的模組化過程中會遇到的問題。 對於現有的路由框架,我們也在最大限度支援平滑遷移。你也可以在Github issue中提出需求,我們評估後會儘快支援,也歡迎任何人提供 Pull Requests