天天看點

開源最佳實踐:Android平台頁面路由架構ARouter

<b>摘要:</b>為了更好地讓開發者們更加深入了解阿裡開源,阿裡雲雲栖社群在3月1号了舉辦“阿裡開源項目最佳實踐”線上技術峰會,直播講述了目前阿裡新興和經典開源項目實戰經驗以及背後的開發思路。在本次線上技術峰會上,阿裡雲資深開發工程師劉志龍分享了android平台頁面路由架構arouter的技術方案、解決的問題以及在實際場景中的最佳實踐。

<b>演講嘉賓介紹:</b>

劉志龍(花名正緯),阿裡雲資深開發工程師,主要從事android端應用開發,負責阿裡雲app的android端架構設計、中間件開發;阿裡雲app服務于阿裡雲官網使用者,使用者可以便捷的在移動端管控雲上資源,了解雲栖社群資訊等。

<b>本次分享将主要圍繞以下幾個方面:</b>

一、為什麼需要路由架構

二、arouter的技術方案

三、使用arouter的最佳實踐

四、未來開發計劃

<b>一、為什麼需要路由架構</b>

<b>原生的路由方案存在的問題</b>

首先談一談原生的路由方案存在的問題以及為什麼需要路由架構。我們所使用的原生路由方案一般是通過顯式intent和隐式intent兩種方式實作的,而在顯式intent的情況下,因為會存在直接的類依賴的問題,導緻耦合非常嚴重;而在隐式intent情況下,則會出現規則集中式管理,導緻協作變得非常困難。而且一般而言配置規則都是在manifest中的,這就導緻了擴充性較差。除此之外,使用原生的路由方案會出現跳轉過程無法控制的問題,因為一旦使用了startactivity()就無法插手其中任何環節了,隻能交給系統管理,這就導緻了在跳轉失敗的情況下無法降級,而是會直接抛出營運級的異常。

開源最佳實踐:Android平台頁面路由架構ARouter

這時候如果考慮使用自定義的路由元件就可以解決以上的一些問題,比如通過url索引就可以解決類依賴的問題;通過分布式管理頁面配置可以解決隐式intent中集中式管理path的問題;自己實作整個路由過程也可以擁有良好的擴充性,還可以通過aop的方式解決跳轉過程無法控制的問題,與此同時也能夠提供非常靈活的降級方式。

<b></b>

為什麼要用路由元件

前面提到的主要是開發與協作中的問題,而使用一款路由架構時還會涉及到其他的兩個大方面:一方面是元件化,而另一方面就是native和h5的問題。剛才所提到的主要是開發和協作中作為開發者所需要面對的問題,而一旦一款app達到一定體量的時候,業務就會膨脹得比較嚴重,而開發團隊的規模也會越來越大,這時候一般都會提出元件化的概念。元件化就是将app按照一定的功能和業務拆分成多個小元件,不同的元件由不同的開發小組來負責,這樣就可以解決大型app開發過程中的開發與協作的問題,将這些問題分散到小的app中。目前而言元件化已經有非常多比較成熟的方案了,而自定義路由架構也可以非常好地解決整個app完成元件化之後子產品之間沒有耦合的問題,因為沒有耦合時使用原生的路由方案肯定是不可以的。

開源最佳實踐:Android平台頁面路由架構ARouter

另外一個問題就是native與h5的問題,因為現在的app很少是純native的,也很少會有純h5的,一般情況下都是将兩者進行結合。這時候就需要非常便捷并且統一的跳轉方案,因為在h5中是無法使用startactivity()跳轉到native頁面的,而從native跳轉到h5頁面也隻能通過配置浏覽器的方式實作。

<b>路由架構的特點</b>

為了解決以上的問題就需要實作一個自定義的路由架構,而路由架構一般都具有以下的三種特點:

<b>分發</b>:把一個url或者請求按照一定的規則配置設定給一個服務或者頁面來處理,這個流程就是分發,分發是路由架構最基本的功能,當然也可以了解成為簡單的跳轉。

<b>管理</b>:将元件和頁面按照一定的規則管理起來,在分發的時候提供搜尋、加載、修改等操作,這部分就是管理,也是路由架構的基礎,上層功能都是建立在管理之上。

<b>控制</b>:就像路由器一樣,路由的過程中,會有限速、屏蔽等一些控制操作,路由架構也需要在路由的過程中,對路由操作做一些定制性的擴充,比方剛才提到的aop,後期的功能更新,也是圍繞這個部分來做的。

今天分享的主題是arouter,arouter是阿裡巴巴開源的android平台中對頁面、服務提供路由功能的中間件,提倡的是<b>簡單且夠用</b>。

開源最佳實踐:Android平台頁面路由架構ARouter

<b>arouter的7個優勢</b>

arouter大緻有以下7個優勢:

開源最佳實踐:Android平台頁面路由架構ARouter

優勢一:直接解析url路由,解析參數并指派到對應目标字段的頁面中。

優勢二:支援多子產品項目,因為現在很少有app是單子產品的項目,一般都是多子產品單工程的,由不同的團隊負責不同的子產品開發,這時候支援多子產品項目開發就顯得尤為重要。

優勢三:支援instantrun,目前很多路由架構并不支援instantrun,而instantrun是google在androidstudio2.0阿爾法版本中提供的新功能,其類似于代碼的日更新,其隻不過面向的是開發過程,這樣做可以在開發的過程中減少開發和編譯的次數,可以簡單地将代碼修改即時地同步到apk中,進而可以大規模降低開發複雜度。

優勢四:允許自定義攔截器,arouter是支援攔截器的,而攔截器其實就是aop的實作,可以自定義多個攔截器解決一些面向行為程式設計上出現的問題。

優勢五:arouter可以提供ioc容器,ioc其實就是控制反轉,這一部分做過服務端開發的朋友可能比較了解,因為服務端開發經常用到的spring架構能夠提供的一個非常重要的能力就是控制反轉。

優勢六:映射關系自動注冊,在頁面不是很多的小型app上面,自動注冊并不會展現出太大優勢,但是對于大型app而言,可能頁面數量已經達到的幾十個或者數百個,在這樣的情況下,自動注冊就顯得非常重要了,因為不可能将每一個頁面都通過代碼的方式進行注冊。

優勢七:靈活的降級政策,arouter可以提供很多種降級政策供使用者自行選擇,而原生的路由方案存在無法靈活降級的問題,startactivity()一旦失敗将會抛出營運級異常。

<b>二、arouter的技術方案</b>

接下來進入分享的第二部分:arouter的技術方案。其實如果大家看過arouter的源碼就會知道arouter提供了兩個sdk,分别是面向兩個不同的階段。本身api這個sdk是面向運作期的,而compiler這個sdk則是作用于編譯期的,從工程上arouter就是劃分成了這兩個sdk。

開源最佳實踐:Android平台頁面路由架構ARouter

最基礎的就是compiler這個sdk,其内部有三個處理器,分别是:route processor,interceptor processor以及autowire processor,通過名字就可以看出這三個處理器分别是處理路徑路由、攔截器和進行自動裝配的。而api的sdk是使用者在運作期使用的,這一部分主要分為四層。最上層是launcher層,這一層是開發者可以直接用到的,其實所有的api都是在這一層中。在launcher層的下一層就是frossard層,從上圖中可以看到frossard層也是綠色的,表示這一層也是可以被外部調用的,frossard層其實包含了三部分,分别是:service、callback和template,這裡的service概念和服務端的service概念是相似的,也是在用戶端的簡單引申,但是卻不同于android元件中的service,這裡的service是arouter抽象出來的概念,從本質上講,這裡的service是接口,從意義上講是将一定的功能群組件封裝成接口,并對外提供能力。template則是模闆,主要用于在編譯期執行的sdk,這個sdk會在編譯期生成一些映射檔案,而這些映射檔案會按照template元件中提供的模闆來生成,這樣按照一定的規則和限制生成映射檔案也友善route在運作的時候進行讀取。再往下一層就完全是sdk的内部實作了,這一層包括了ware house、thread、log、exception以及class工具。ware house主要存儲了arouter在運作期間加載的一些配置檔案以及映射關系;而thread則是提供了線程池,因為存在多個攔截器的時候以及跳轉過程中都是需要異步執行的;class工具則是用于解決不同類型apk的相容問題的。再下一層就是logistics center,從名字上翻譯就是物流中心,整個sdk的流轉以及内部調用最終都會下沉到這一層,當然也會按照功能子產品進行劃分。

下圖是按照功能元件的方式來對于整個架構進行劃分的,其實arouter在設計上使用了三種思想:bootstrapping、extensibility以及simple &amp; enough。首先,arouter的元件是自舉的,這個概念借鑒了程式設計中的自舉;除此之外arouter元件還具有良好的擴充性,因為像route這樣的東西是整個apk的基礎元件,不可能經常變更,也不可能經常更新,是以應該具有良好的擴充性,而不需要通過經常更新來解決問題;而arouter最重要的宗旨就是簡單并且夠用,arouter不會有非常複雜的使用方式和調用方式,但是功能卻是非常全面的。

開源最佳實踐:Android平台頁面路由架構ARouter

可以從圖中看出arouter的最外面一層就是route,這一層是整個架構的基礎,而這一層也應該非常穩定,幾乎不會發生變更。再往上一層就是service層,這一層是依賴于底層的route建構起來的,也就是說service層是通過route才實作的功能。再往上一層就是interceptor層,攔截器層則是通過service的機制實作的,攔截器和service都會作用于整個路由的過程中,是以說元件之間是自舉的,因為service和interceptor在沒有route時是不會出現的,它們都是由route層建構起來的,反過來又會作用于route層,這也是arouter的可擴充性的表現,後續的擴充都會基于service層來實作。

接下來分享一下arouter的具體解決方案,也就是arouter是如何解決上述問題的。

<b>頁面注冊:注解&amp;注解處理器</b>

開源最佳實踐:Android平台頁面路由架構ARouter

首先,對于頁面自動注冊的問題,arouter是可以自動注冊映射關系的,因為大型app的頁面往往很多,會存在幾十甚至上百個頁面,是以手動注冊映射關系會非常麻煩,需要寫很多重複備援的代碼,并且需要調用很多接口,而為了避免這樣的麻煩,arouter實作了頁面的自動注冊。而為了解決隐式intent的問題和将所有配置都存儲在manifest中這樣集中式的問題,首先想到的就是分布式管理,可以将所有的配置都放在目标頁面,這樣就實作了“all in one”,就是一個頁面中所有的配置都要聚合在該頁面中,這樣就解決上面的問題。不同的頁面由不同的配置負責,這樣修改也變得非常容易,而不需要将配置散落在整個app四處。

其實配置相當于一個注解,是以arouter采用的方案就是在每個目标頁面上使用注解來标注一些參數,比方上圖中的path标注就是其路徑,圖中也可以看到對于注解的聲明。使用注解時會遇到的第一個問題就是需要找到處理注解注解的時機,如果在運作期處理注解則會大量地運用反射,而這在軟體開發中是非常不合适的,因為反射本身就存在性能問題,如果大量地使用反射會嚴重影響app的使用者體驗,而又因為路由架構是非常基礎的架構,是以大量使用反射也會使得跳轉流程的使用者體驗非常差。是以arouter最終使用的方式是在編譯期處理被注解的類,而可以做到在運作中盡可能不使用反射。其實這一部分就是注解處理器,注解處理器其實是作用在jvm上的,可以通過插入一部分代碼來處理被注解标注的類。

頁面注冊的整個流程如下圖所示:首先通過注解處理器掃出被标注的類檔案;然後按照不同種類的源檔案進行分類,這是因為arouter是一個架構,其能夠提供的功能非常多,是以不僅僅提供了跳轉功能,它也能夠實作子產品之間的解耦,除此之外arouter還能夠提供很多的功能,像剛才提到的攔截器可以實作自動注冊,其實arouter中的所有元件都是自動注冊的;在按照不同種類的源檔案進行分類完成之後,就能夠按照固定的命名格式生成映射檔案,這部分完成之後就意味着編譯期的部分已經結束了;而最後一步的初始化其實是發生在運作期的,在運作期隻需要通過固定的包名來加載映射檔案就可以了,因為生成是由開發者自己完成的,是以會了解其中的規則,就可以在使用的時候利用相應的規則反向地提取出來。這就是頁面自動注冊的整個流程。

開源最佳實踐:Android平台頁面路由架構ARouter

下圖是arouter在編譯期生成的類檔案,命名規則就是工程名+$$+group+$$+子產品名。可以看出這裡面包含了group、interceptor以及route,是以會有很多種不同的映射檔案,對于這部分而言,大家可以在github上自行下載下傳demo,運作一下看看在build目錄下生成的一些映射檔案。

開源最佳實踐:Android平台頁面路由架構ARouter

<b>加載:分組管理,按需加載</b>

開源最佳實踐:Android平台頁面路由架構ARouter

接下來要分享的就是加載,剛才已經解決了注冊的問題,這時候就到了運作期,而在運作期就需要将映射關系加載進來。而加載的時候就會遇到另一個問題,因為需要面對長久的app的設計,是以不可能一次性把所有的頁面都加載進來,當app有一百或者幾百個頁面的時候,一次性将所有頁面都加載到記憶體中本身對于記憶體的損耗是非常可怕的,同時對于性能的損耗也是不可忽視的。是以arouter中提出了分組的概念,arouter允許某一個子產品下有多個分組,所有的分組最終會被一個root節點管理。如上圖中所示,假設有4個子產品,每個子產品下面都有一個root結點,每個root結點都會管理整個子產品中的group節點,每個group結點則包含了該分組下的所有頁面,也就是說可以按照一定的業務規則或者命名規範把一部分頁面聚合成一個分組,每個分組其實就相當于路徑中的第一段,而每個子產品中都會有一個攔截器節點就是interceptor結點,除此之外每個子產品還會有控制攔截反轉的provider結點。

下圖表現的就是剛才提到的按需加載。arouter在初始化的時候隻會一次性地加載所有的root結點,而不會加載任何一個group結點,這樣就會極大地降低初始化時加載結點的數量。因為每個子產品中可能有n個分組,每個分組中可能有n個頁面,如果一次性地将所有的頁面全部加載進來,那麼整個複雜度可能不隻是o(n^2),但是每個子產品都隻加載其根節點,從算法的角度考慮可能就是複雜度為o(n)的方案,也就是有多少個子產品就隻需要加載多少個結點。下圖中的三個圈中展現的就是arouter初始化時加載的狀況。那麼什麼時候加載分組結點呢?其實就是當某一個分組下的某一個頁面第一次被通路的時候,整個分組的全部頁面都會被加載進去,這就是arouter的按需加載。其實在整個app運作的周期中,并不是所有的頁面都需要被通路到,可能隻有20%的頁面能夠被通路到,是以這時候使用按需加載的政策就顯得非常重要了,這樣就會減輕很大的記憶體壓力。

開源最佳實踐:Android平台頁面路由架構ARouter

<b>攔截器</b>

分享完分組管理和按需加載之後,接下來分享一下關于攔截器的内容。原生的路由方案中存在的問題就是其無法在頁面跳轉的過程中插入一些自定義邏輯,而攔截器就是arouter中提出的針對aop思想的實作。

開源最佳實踐:Android平台頁面路由架構ARouter

那麼arouter是如何實作攔截器的呢?其實arouter對于攔截器的實作方式與剛才提到的路徑注冊方式是一樣的,隻是使用了不同的注解而已。如上圖中所顯示的,存在攔截器1至5,但是這5個攔截器并不是會都生效。在上圖中可以看出從a頁面到b頁面的跳轉流程中隻有三個攔截器生效了,首先跳轉到第一個攔截器,如果跳轉的條件符合那麼隻需要在攔截器進行一些自定義的操作,等攔截器處理完成之後會放行給下一個攔截器,以此類推當經過了所有的攔截器之後才會結束整個跳轉的流程,如果每個攔截器都放過的話才能夠跳轉到最終的頁面。這裡因為是自動注冊的,是以可以将不同功能的攔截器放在不同功能的子產品中,隻有子產品被打包到整個項目中,因為自動注冊機制是以攔截器就會生效,如果不将這些攔截器放到子產品并打包到項目中,那就不會生效,這樣就不用去做很多注冊與反注冊的工作。如圖所示的攔截器2就是沒有被打包進來的,是以就不會生效,如果修改打包參數,将攔截器2打包到app中就會生效,這部分就是對于攔截器的實作。

直接講攔截器可能不容易讓大家了解,那麼就用這樣形象的比喻來解釋一下,攔截器就是像是一個漢堡,漢堡中夾心的無論是生菜、牛肉還是芝士都像攔截器一樣,當在做漢堡時就相當于在做apk,打包了哪些子產品就相當于在漢堡中放了哪些層,在吃的時候就會把這一層都咬掉,但是漢堡的每一層都有可能是芝士、牛肉或者鐵片,當遇到某一層是鐵片的時候就無法咬下去了,也就是被攔截住了。同樣的攔截器就是需要當條件符合的時候才能讓跳轉流程繼續執行,同樣像漢堡一樣,如果使用了太多的攔截器最終會導緻漢堡變成了“巨無霸”,所有的攔截器會在任意兩次跳轉之間生效,聲明了大量的攔截器會影響整個跳轉流程的性能,攔截器的更詳細内容會在第三部分的最佳實踐中繼續為大家介紹。

<b>instantrun相容</b>

接下來分享一下arouter如何實作對于instantrun的相容。市面上的架構一般對于這一部分的相容都是缺失的,對于instantrun的相容從技術上看并不是非常難以實作的,在實作時隻需仔細閱讀instantrun的源碼就可以了。在實作對于instantrun的相容時是存在如下圖所示的四種情況的,當androidsdk版本大于21的時候,會存在splitapk的特性支援的,會允許将一個apk切分成多個小apk,當然其實這并不是apk的切分,而實際上是dex的切分,也就每個依賴都會打包成小的dex放在app+包名的目錄下的,這與傳統情況下是不同的。

開源最佳實踐:Android平台頁面路由架構ARouter

是以隻需要參照這張表格并根據androidsdk和gradleplugin的版本就可以解決了。如果android版本超過21并且gradle插件的版本超過2.3.0,這時候就會支援splitapk,從中可以擷取所有dex的位置,進而實作映射關系的加載。除此之外的三種情況都不支援splitapk的,這種情況下就需要看一下instantrun的源碼,就會發現在源碼中原本應該存放業務代碼的dex的地方替換成了instantrun的sdk的dex,而是将業務代碼打包在一個zip中,此時隻需要通過運作時的反射拿到instantrun的sdk的一個類的path,而在擷取path時是存在靜态方法的getdexfiledirectory,隻需要執行一下就可以知道目前版本将真實的dex放在什麼地方,通過對于這兩種方式的相容就可以實作對于instantrun的相容。

<b>依賴注入的實作</b>

接下來分享依賴注入的實作,這一部分是路由架構在進行大規模元件之間解耦時比較重要的一點。其實依賴注入就是對于控制反轉思想的實作,這部分服務端使用的比較多,用戶端可能使用不是非常多。arouter對于依賴注入的實作主要分成如下圖所示的兩個部分。

開源最佳實踐:Android平台頁面路由架構ARouter

首先編譯期掃出需要自動裝配的字段,之前對于自動裝配也已經提到了,就是在compiler中的處理器autowire processor,自動展現在将字段自動地進行指派而不需要使用者手動幹預,在掃除自動轉配的字段之後,需要把自動裝配的字段注冊在映射檔案中,然後跳轉的時候按照預先的配置從url中提取參數,并按照類型放入intent中,這樣就解決了如何通過url跳轉到native頁面,并将url中的參數傳遞進來。上圖中綠色的部分則是在運作期的早期實作,這部分通過反射拿到activitythread類,調用它的currentactivitythread方法,拿到目前的activitythread執行個體,之後通過反射替換activitythread執行個體中的字段minstrumentation,并覆寫instrumentation的newactivity方法,在activity執行個體化的時候,通過反射把intent預先存好的參數值寫入到需要自動裝配的字段中。這是早期的做法,這種做法有一個非常嚴重的問題就是會不夠穩定,路由架構作為整個app的基礎如果不足夠穩定,那麼造成的影響是非常嚴重的。使用者如果使用自動裝配這樣的功能的時候失敗的話,問題就非常嚴重了,可能導緻使用者的代碼出現npe,出現這樣的問題就不簡單是使用者體驗的問題了,有可能導緻app崩潰。

開源最佳實踐:Android平台頁面路由架構ARouter

是以目前的實作方式則換成了上圖的方式,在編譯期基本沒有變化,但是在運作期進行了調整。在運作期會在目标頁面進行初始化的時候調用arouter.inject(this),将自身的執行個體傳遞進去。arouter會查找到編譯期為調用方生成的注入輔助類,而這裡提到的注入輔助類就是比方在編譯期是掃描到一個a頁面需要進行自動裝配,此時就會為a頁面生成一個注入輔助類,在運作的時候調用注入輔助類的方法對于字段進行指派,這其實就是模拟使用者對于字段進行指派,雖然看起來可能麻煩一些,但是可以保證注入的穩定性,而且最終展現的效果是相同的,使用者不需要寫重複備援的代碼,而且在實作時并不需要在每一個目标頁面上都調用這一行代碼,完全可以将這些代碼放在基類中,而在執行個體化輔助類之後,調用其中的inject方法完成對于字段的指派。

下圖所示的代碼就是在編譯期生成的注入輔助類,這部分實際上就是模仿了使用者的寫法,通過一定的工具和規則生成這樣的代碼,免去使用者手寫重複和備援的代碼,在使用者的角度來看也是自動注入,這一部分就是依賴注入的具體實作,大家也可以參考github上的源碼來研究具體實作。

開源最佳實踐:Android平台頁面路由架構ARouter

<b>三、最佳實踐</b>

接下來就進入到了本次分享的重點:arouter的最佳實踐,在這部分将分享如何在項目中運用arouter,如何讓arouter幫助我們加快開發的速度。

<b>頁面跳轉</b>

開源最佳實踐:Android平台頁面路由架構ARouter

分享的第一個最佳實踐就是頁面跳轉。大家可能提出這樣的問題:如果我們在使用arouter這樣的路由架構的時候,将每一個目标頁面都通過一定的規則注解上如圖所示的path,在任何場景下都通過path跳轉,會不會出現在寫代碼的時候完全不知道要跳轉到哪裡,也不知道目前頁面會從哪些頁面跳進來的問題。其實這樣的問題在程式設計實作的時候對于開發者而言是非常難受的,這也是無耦合所帶來的代價,但是其實也可以簡單地通過類似于文法糖的寫法解決這樣的問題。其實在進行了元件化之後,在寫代碼時也不是所有的頁面都需要route進行跳轉的,但是在最終實作上卻希望所有的頁面都通過route進行管理。為了實作這樣的目标,其實隻需要在目标頁面上放一個靜态的launch(這裡的launch可以換成任何一個你喜歡的方法名字),然後在這個launch方法中調用route跳轉到目前頁面,這樣在無法耦合到目前類的時候可以直接使用arouter的api并通過path的方式跳轉進來。在可以依賴到這個類的場景下,可以直接調用這個類的靜态方法跳轉到這個頁面,這樣就解決了我們在日常開發中同一個子產品之間的跳轉還需要使用route的非常尴尬的情況,而且這樣也可以最終實作所有的頁面都被route管理,但是看起來并非所有的跳轉都需要通過route,這樣至少在開發中是非常舒服的。

<b>從外部導航到内部頁面</b>

接下來要分享的也是路由架構的一個非常重要的功能:從外部導航到内部頁面。可以看到下圖中的兩個截圖分别是使用了自定義的scheme,另一張圖則是使用了原聲的https的scheme。

開源最佳實踐:Android平台頁面路由架構ARouter

對于這些url進行逐段分析,在scheme後面的就是域名,再之後就是test/activity1,這部分就是真實的頁面上所标注的注解,也就是需要将這一行url映射到标注了test/activity1的頁面上。當然我們可以想到之前使用隐式intent也可以做的很好,但是隐式intent卻存在着很多的局限性,而且無法将參數也注入進去。可以看到url中“?”之後就是參數,通過arouter這樣的路由架構不但可以跳轉到目标頁面也可以将後面的一些get參數注入到目标頁面的對應字段中。

接下來具體分享這部分是如何實作的,首先需要在app的manifest聲明一個activity,但是是這個activity不需要頁面,隻需要注冊一個intent-filter就可以了。這個intent-filter就是用于監聽剛剛生成的scheme的,而且scheme可以換成任何想要的,比如http或者https,也可以使用自定義scheme。為什麼說這裡是一個最佳實踐呢,其實通常情況下使用隐式intent的時候,每一個從外面跳轉進來的頁面都需要注冊上intent-filter,每個頁面都需要設定export=true,也就是需要讓每一個頁面都可以導出,在外部可以通路到。但是這樣做會帶來非常嚴重的安全風險,就像是一個房子有十個門還是隻有一個門,看門的成本是不同的。而現在使用的這種場景隻需要對外暴露出一個activity,然後在這個activity中注冊一個intent-filter,這樣之後所有的外部路由請求都會經過這唯一的門,然後在這個activity中擷取到url并将其交給arouter,剩下的就由路由架構做分發了。

開源最佳實踐:Android平台頁面路由架構ARouter

下面這張圖就是基類,其實每個app都有自己的基類,比方像沉浸式狀态欄等統一的配置都會做成基類。為了實作自動裝配的功能,是以需要将這一行代碼加入基類的oncreate中,然後傳一個this。

開源最佳實踐:Android平台頁面路由架構ARouter

隻需要在基類中加入這一行代碼,下圖就是目标頁面,剛剛我們在浏覽器中通路之前的url的時候最終會導入到這個目标頁面中,而這個頁面首先在上面标注好了目标位址,下面也可以看到為什麼可以将每一個get參數解析到對應的字段中。在實作時需要聲明出需要進行解析的字段,其名字會映射到外面的url的參數上,然後需要将其标注好autowired這樣的注解,autowired注解中有一個屬性就是name,相當于别名,标注了别名之後arouter會自動提取别名所對應的參數。可以看到隻要繼承自剛才看到的基類,就不需要在每一個頁面都重複地寫inject方法了,這樣就可以實作無論通過什麼樣的途徑跳轉進來都可以拿到對應的參數,完全不需要使用getintent這樣備援的代碼,可以簡化開發,這就是使用路由架構所帶來的好處之一。對于這一部分而言,github上也有更加詳細的文檔供大家檢視學習。

開源最佳實踐:Android平台頁面路由架構ARouter

<b>處理登入邏輯 : 攔截器的運用</b>

以上分享的就是如何從外部的url跳轉到内部的頁面并解析參數,接下來分享如何處理登入邏輯。登入邏輯是每個app都會有的功能,有的app是隻要使用者進入就需要登入的,也有的app是對于一些頁面需要登入,另外一些頁面也不需要登入,而對于後面的這種app而言,在每個頁面中都需要判斷是否使用者登入了則是非常不合适的做法,這也是最開始考慮到系統原生的路由方案不支援在系統中插入自定義跳轉邏輯的比較坑的狀況。是以假如使用arouter,就能夠使用arouter所提供的攔截器的機制解決登入問題。使用arouter解決登入邏輯隻需要實作登入攔截器就可以了,不需要在每一個頁面都判斷是不是需要登入,而隻需要在登入攔截器中進行判斷。登入攔截器會作用在所有的跳轉之間,假設從來源頁面跳轉到下面的a、b、c和d這四個目标頁面,可以看到圖中綠色的是不需要登入頁面的,可以直接跳轉進入,也就是如綠色的箭頭展示的一樣是可以直接放行的;而對于c頁面而言,則屬于需要登入的頁面,這時就會被攔截器攔截并直接導航到登入頁,在使用者完成登入或者取消登入後,通過回調或者廣播等形式回到攔截器,然後根據從攔截器中得到的結果判斷可以直接往下跳轉還是終止本次跳轉流程,每一個攔截器中都有一個回調,這個回調可以終止本次路由過程也允許直接放行。這就是典型的面向切面程式設計,當然登入攔截器隻是諸多攔截器之一,可以聲明n個攔截器可以實作登入的判斷以及使用者權限的判斷等,這些就交給開發者自由發揮了。談到這部分還會存在一個問題就是如何才能在一個地方判斷出所有的頁面哪些需要登入,哪些不需要登入,如果這時候儲存兩個非常大的清單,一個用于儲存需要登入的頁面,另一個儲存不需要登入的頁面,将會是非常不合适的了。

開源最佳實踐:Android平台頁面路由架構ARouter

<b>辨別目标頁面資訊 : 配置extra參數</b>

是以接下來分享一下如何配置頁面的參數,剛剛提到了“all in one”,這是什麼意思呢?其實就是希望所有頁面中的配置都能夠濃縮到這一個頁面中,也就是高内聚低耦合的思想,不希望頁面的配置逃出頁面,配置到像manifest的其他地方。像在攔截器中配置哪些地方需要登入哪些不需要登入的話就違背了剛才提出的這個原則,arouter架構的設計思想就是希望所有的屬性标注在自己的頁面中。可以看一下頁面中标注的頁面注解,如下圖所示可注解中在ide的提示中有extras這樣的參數,大家看到這個數字應該非常熟悉,這個數字就是int的最小值,而為什麼extras這個參數是int呢?其實是因為int本身在java中是由4個位元組實作的,每個位元組是8位,是以一共是32個标志位,去除掉符号位還剩下31個,也就是說轉化成為二進制之後,一個int中可以配置31個1或者0,而每一個0或者1都可以表示一項配置,這時候隻需要從這31個位置中随便挑選出一個表示是否需要登入就可以了,隻要将标志位置為1,就可以在剛才聲明的攔截器中擷取到這個标志位,通過位運算的方式判斷目标頁面是否需要登入,這樣是簡單并且高效的,因為位運算的速度要遠遠高于字元串比對以及其他的方式的,而且一個int值就可以提供31個開關。目前而言沒有一個目标頁面需要配置30多個屬性,是以使用int是足夠的,而開發者隻需要實作一個簡單的位運算的工具類就可以提取出二進制int中的每一位,并對其中每一個值進行判斷。

開源最佳實踐:Android平台頁面路由架構ARouter

如下圖所示,一個int中有31個開關,可以針對每一位進行定制。這部分在arouter中是沒有任何限制的,arouter在攔截器中會把目标頁面的資訊封裝一個類,這個類就包含了目标頁面注解上辨別的各種資訊。對于按需加載中的各種資訊并不是通過反射來做的,是以性能還是很高的。

開源最佳實踐:Android平台頁面路由架構ARouter

<b>子產品間通信解耦 :控制反轉</b>

除此之外,另一個比較重要的問題就是如何實作子產品間的通信解耦。實作元件化的時候希望對于不同的元件進行分别打包,而且子產品之間應該不存在任何依賴,可以看出下圖中左邊的圖中的四個元件完全是耦合依賴的,這樣就導緻四個元件之間根本無法解耦,是以打包的時候也必須一起打包,否則就會出現no class found的問題,是以現在的實作是如下圖右邊所示的,通過ioc容器,也就是控制反轉容器将耦合解開。為什麼這樣能将耦合解開呢?其實是因為這樣就可以讓各個元件之間不産生直接依賴,而是通過ioc控制反轉容器拿到對方的執行個體,這樣我們在寫代碼的時候就不會存在直接依賴的問題。而arouter本身也是一個ioc容器,它在實作這部分功能的時候用到的一個元素就是service,如果大家做過服務端開發的話就會對于service很熟悉了,service就是将一部分功能群組件封裝起來成為接口,以接口的形式對外提供能力,是以在這部分就可以将每個功能作為一個service,而service的實作就是具體的業務功能,這部分也需要通過ioc容器進行擷取。這樣整個的流程将通過使用者的直接依賴轉化成通過控制反轉容器依賴的這種形式。

開源最佳實踐:Android平台頁面路由架構ARouter

接下來分享一下在工程中如何将控制反轉的流程運用起來。首先需要聲明一個服務,而服務在表現上其實就是一個接口。隻需要聲明如下圖中的helloservice控制反轉,使其實作了iprovider,iprovider就是最開始提到的arouter架構中的template中提供的很多模闆中的一項,iprovider用于限制服務,其中隻有一個方法就是init(),也就是執行個體化服務時需要調用的初始化方法。服務本身也是按需加載的,是以不會一次性全部加載。在下圖的例子中helloservice中隻有一個自己的方法就是sayhello(),圖中的下半部分就是表示如何實作這個服務的。其實可以随便聲明一個類讓他實作這個服務,因為實作helloservice的同時也需要實作iprovider中的init(),可以看到init()方法中使用到了上下文也就是context,除此之外就是實作了sayhello()方法,之後将服務的實作使用route注解标注起來,當然這個注解可以按照個人喜好書寫,但是還是需要進行标注。之前提到的了arouter中的所有元件是自舉的并且是自動注冊的,而服務這部分就是自動注冊的。其實通過注解可以看出,arouter在處理注解在服務和基礎路由上的方案是基本一緻的,是以也存在分組加載和按需加載的情況,而服務是全局單例的,隻有在第一次使用到的時候才會被初始化,而服務的初始化就是調用了自己的init()方法,這裡需要注意一下的就是攔截器使用的是iinterceptor這樣的接口,而iinterceptor接口中也隻有一個方法就是init()。攔截器和服務不一樣,所有的服務不會因為在一個生命周期中都用到,隻有20%的服務可能在一次生命周期中使用到,是以如果一次性都初始化對于記憶體也會造成很大的壓力,而攔截器則是不同的,因為攔截器會在任意一次跳轉中生效,是以攔截器的初始化是在整個sdk啟動的時候進行的,這部分也是服務和攔截器的差別。

開源最佳實踐:Android平台頁面路由架構ARouter

下面這張圖則主要介紹了如何使用服務,也就是将服務交給ioc容器管理和如何去調用服務。其實這裡和擷取跳轉之間的intent參數裡面的方法是一樣的,隻需要聲明一個字段,這個字段就是剛剛使用的服務,然後通過autowired的注解進行标注,這樣隻需要在基類中寫剛才的那句arouter.getinstance().inject()方法,這些服務就會在運作的時候自動注入進來,完全不需要使用者進行手動操作。可以看到autowired上面是有幾個屬性的,首先會有一個name,這個name和intent參數中的name是一緻的,這個name就是别名,一旦辨別了name,arouter在内部實作的時候會通過依賴查找的方式來對這個服務進行搜尋。而依賴查找和依賴注入就是對于控制反轉的兩種實作。依賴查找是使用者主動觸發的,是通過ioc容器進行查找的,并不是由使用者執行個體化這個類的,是以控制權還在ioc容器中。而不标注name的這種形式sdk會通過直接的方式進行擷取,其實這也是依賴查找,但是從使用者的角度來看這就是依賴注入了,因為在sdk的具體實作上實際是通過依賴查找實作的,後面也會有例子進行介紹。

開源最佳實踐:Android平台頁面路由架構ARouter

這裡還需要談一下為什麼在一些場景下還需要标注name。因為在java中接口是可以被多實作的,也就是一個接口有多個具體的實作方式,通過bytype的方式可能難以拿到想要的多種實作,這時候就可以通過name的方式擷取真實想要的服務。是以其實大多數情況是不需要辨別name的,如果有多實作的時候就需要标注上别名了。可以看到在上圖的例子中的oncreate()方法中可以直接調用這個接口的方法,這樣就完成了子產品間的解耦,因為完全沒有依賴到服務的具體實作,而服務的具體實作的控制權完全掌握在ioc容器的route層。

下圖就具體地解釋了剛才提到的兩種情況也就是byname和bytype的依賴查找的方式,而上一張圖中則是依賴注入。依賴查找是應用在不希望在類初始化的時候就把一些功能注入進來的場景以及在某些頁面上才會觸發這樣的功能的情況下,那麼隻需要在使用到的時候去擷取這個服務就可以了。而這種情況就是通過使用者的主動依賴查找來擷取服務,其實就是圖中所示的byname和bytype的依賴查找的方式。

開源最佳實踐:Android平台頁面路由架構ARouter

<b>解決運作期動态修改路由的問題</b>

然後需要分享的就是如何解決運作期動态修改路由的問題。如下圖所示,這種情況下隻需要實作一個服務就可以了,從下圖也可以看出為什麼說arouter的元件都是自舉的,因為服務的查找還是需要依賴于底層路由的查找的,是以服務功能的實作是由路由層作為基礎的,并且服務是用來解決動态修改路由的問題的,是以隻需要實作一個服務。其實這個pathreplaceservice就是arouter提供的一個服務,是在arouter的frossard層提供的服務,其實就是一個接口,隻需要将其實作并标注上就可以,因為有自動注冊的機制,是以在app啟動的時候就會注冊到arouter架構上,這樣之後架構在跳轉的時候就會跳轉到這個服務,而如果沒有實作,架構就無法調用,自然也就不會有這部分功能。這樣就實作了arouter架構的非常好的可擴充性,後期arouter架構不需要更改其底層基礎,隻需要聲明更多的服務,由使用者主動實作,并在最後運作期的時候通過自動注冊的方式将這些服務加載到架構中。

開源最佳實踐:Android平台頁面路由架構ARouter

而對于pathreplaceservice這個服務而言,可以看到它有三個方法,首先init()用于初始化,下面的兩個方法分别是forstring()以及foruri()。foruri()是從外部通過uri的形式跳轉到頁面的時候會使用到的一個方法,參數中的uri就是原始的uri,如果你有需要的話可以在這個方法中按照自己的一些邏輯和規則進行替換,然後直接return回來就可以了。這裡return之後就會交給arouter的架構繼續處理,這時候就實作了對于目标頁面的重定向。而forstring()則是在正常情況下通過arouter的api寫代碼的時候會使用到的方法。以上的這兩個方法可以使用同樣的邏輯來做,實作運作期動态地修改路由。

<b>解決降級問題</b>

接下來分享的就是關于解決降級的問題。其實這部分的方法和剛才的方法是異曲同工的,隻需要實作另一個服務就好了,arouter在發展中會越來越多地為大家提供各種服務讓使用者自己進行具體的實作,當然如果不實作也不會有這部分功能,如果app實作了降級服務,那麼随便辨別一個注解就可以了,當然這個注解是由使用者決定的,可以選擇自己喜歡的規則,可以将這些服務都放在不同的分組下或者都放在同一個分組下。而現在相當于放在了sdk這個分組下面,對于這一部分隻需要實作onlost()方法就可以了,arouter如果發現在目标跳轉的情況下失敗了,就會回調這個onlost()方法。onlost()方法的第二個參數postcard翻譯過來就是明信片,這裡面就包含了本次跳轉中所有的内容,通過拿到這些内容就可以實作自己的降級方案。下圖中所列舉的例子是通過跳轉到第三方的h5的錯誤頁面來解決的,因為app不能夠重複釋出,但是h5是可以重複釋出的,是以可以通過h5的方式解決降級問題,把去向的目标頁面作為目标的參數傳遞到h5中。

開源最佳實踐:Android平台頁面路由架構ARouter

<b>四、未來的開發計劃</b>

最後想分享的就是arouter的未來開發計劃。未來arouter會支援插件化并且支援生成映射關系文檔,因為插件化是現在很多大型app中會使用的技術方案,很多的dex和功能是動态地下發到app中的,而在這種情況下,是無法找到所有的dex檔案的,也就是對于沒有加載過的dex而言,裡面的映射關系是跳轉不過去的,是以一旦dex檔案位置發生變動,正常的方案是無法找到dex的,也不能實作映射檔案初始化,這一部分會在後面的版本中進行支援。因為像手淘和360等很多插件化的方案之後也許會開源,這樣可能越來越多的app會支援插件化,如果arouter作為一個技術元件如果不能支援插件化的話,就會造成麻煩。

開源最佳實踐:Android平台頁面路由架構ARouter

未來的另一個發展方向就是生成映射關系文檔,目前因為在多個子產品下需要支援生成映射關系文檔,而且多個子產品之間是沒有耦合的,如果沒有生成映射關系文檔的功能,可能就不知道一個功能子產品中有哪些頁面是可以被路由進去的,是以後續的版本會對這部分進行簡化并添加版本控制解決多版本的相容性問題,也将可以幫助使用者生成友善快捷的文檔。這兩部分就是未來arouter需要重點進行支援的。

arouter是從去年的年底時開始開源的,到現在大概經過了兩三個月,目前已經有一千多個star,已經有一部分開發者在關注了,而我們也有一個溝通與交流的群,大家如果感興趣的話可以直接到github上找到arouter的源碼來分析具體的實作,如果大家有更好的思路和方案也可以貢獻代碼,和我們一起更好地完善arouter。當然一個技術選型肯定是簡單又好用的,并且應該是長期進行維護保證足夠穩定的,arouter也具有這樣的特點,歡迎大家選用并貢獻代碼。

開源最佳實踐:Android平台頁面路由架構ARouter