給大家推薦個開源項目
本文由
Xiasm
授權投稿
https://github.com/Xiasm/EasyRouter
前言
路由跳轉和組件化在項目中用了一段時間了,最近對Android中的ARouter路由原理也是研究了一番,於是就給大家分享一下自己的心得體會,並教大家如何實現一款簡易的路由框架。本篇文章分為兩個部分,第一部分著重剖析ARouter路由的原理,第二部分會帶著大家仿照ARouter擼一個自己的路由框架,我們自己擼的路由框架可能沒有Arouter眾多的功能如過濾器、provider等,但是卻實現了ARouter最核心的功能:路由跳轉,同時你也能學會如何去設計一個框架等。這裡先附上我自己實現的路由框架demo地址:
https://github.com/Xiasm/EasyRouter
第一部分:ARouter原理剖析
說到路由便不得不提一下Android中的組件化開發思想,組件化是最近比較流行的架構設計方案,它能對代碼進行高度的解耦、模塊分離等,極大地提高開發效率(如有同學對組件化有不理解,可以參考網上眾多的博客等介紹,然後再閱讀demo源碼中的組件化配置進行熟悉)。路由和組件化本身沒有什麼聯繫,因為路由的責任是負責頁面跳轉,但是組件化中兩個單向依賴的module之間需要互相啟動對方的Activity,因為沒有相互引用,startActivity()是實現不了的,必須需要一個協定的通信方式,此時類似ARouter和ActivityRouter等的路由框架就派上用場了。
第一節:ARouter路由跳轉的原理
如上圖,在組件化中,為了業務邏輯的徹底解耦,同時也為了每個module都可以方便的單獨運行和調試,上層的各個module不會進行相互依賴(只有在正式聯調的時候才會讓app殼module去依賴上層的其他組件module),而是共同依賴於base module,base module中會依賴一些公共的第三方庫和其他配置。既然上層的兩個module沒有依賴,便不能通過startActivity()進行通信,那麼如何進行通信呢?可能有同學會想到隱式跳轉,這當然也是一種解決方法,但是一個項目中不可能所有的跳轉都是隱式的,這樣Manifest文件會有很多過濾配置,而且非常不利於後期維護。當然你用反射拿到Activity的class文件也可以實現跳轉,但是第一:大量的使用反射跳轉對性能會有影響,第二:你需要拿到Activity的類文件,在組件開發的時候,想拿到其他module的類文件是很麻煩的,因為組件開發的時候組件module之間是沒有相互引用的,你只能通過找到類的路徑去反射拿到這個class,那麼有沒有一種更好的解決辦法呢?辦法當然是有的,看下面介紹。在組件化中,我們通常都會在base_module上層再依賴一個router_module,而這個router_module就是負責各個模塊之間頁面跳轉的。用過ARouter路由框架的同學應該都知道,在每個需要對其他module提供調用的Activity中,都會聲明類似下面@Route註解,我們稱之為路由地址:
@Route "/main/main" public class MainActivity
extends
AppCompatActivity
{}@Route
(path ="/module1/module1main"
)public
class
Module1MainActivity
extends
AppCompatActivity
{}
那麼這個註解有什麼用呢,路由框架會在項目的編譯期通過註解處理器掃描所有添加@Route註解的Activity類,然後將Route註解中的path地址和Activity.class文件映射關係保存到它自己生成的java文件中,只要拿到了映射關係便能拿到Activity.class。為了讓大家理解,我這裡來使用近乎偽代碼給大家簡單演示一下。
class MyRouters //項目編譯後通過apt生成如下方法 static String String "/main/main" classpublic
); route.put(
"/module1/module1main"
, Module1MainActivity.class
); route.put("/login/login"
, LoginActivity.class
); }}這樣我們想在app模塊的MainActivity跳轉到login模塊的LoginActivity,那麼便只需調用如下:
//不同模塊之間啟動Activity void String String String
, ClassBean> route = MyRouters.getRouteInfo(
new
HashMap<String
, ClassBean>); LoginActivity.class
classBean = route.get
("/login/login"
); Intent intent =new
Intent(this
, classBean); intent.putExtra("name"
, name); intent.putExtra("password"
, password); startActivity(intent);}這樣是不是很簡單就實現了路由的跳轉,既沒有隱式意圖的繁瑣,也沒有反射對性能的損耗。用過ARouter的同學應該知道,用ARouter啟動Activity應該是下面這個寫法:
// 2. Jump with parameters "/test/login" "password" 666666 "name" "小三"
那麼ARouter背後是怎麼樣實現跳轉的呢?實際上它的核心思想跟上面講解是一樣的,我們在代碼里加入的@Route註解,會在編譯時期通過apt生成一些存儲path和activity.class映射關係的類文件,然後app進程啟動的時候會載入這些類文件,把保存這些映射關係的數據讀到內存里(保存在map里),然後在進行路由跳轉的時候,通過build()方法傳入要到達頁面的路由地址,ARouter會通過它自己存儲的路由表找到路由地址對應的Activity.class(activity.class = map.get(path)),然後new Intent(context, activity.Class),當調用ARouter的withString()方法它的內部會調用intent.putExtra(String name, String value),調用navigation()方法,它的內部會調用startActivity(intent)進行跳轉,這樣便可以實現兩個相互沒有依賴的module順利的啟動對方的Activity了。
第二節:ARouter映射關係如何生成
通過上節我們知道在Activity類上加上@Route註解之後,便可通過apt生成對應的路由表。那麼現在我們來搞清楚,既然路由和Activity的映射關係我們可以很容易地得到(因為代碼都是我們寫的,當然很容易得到),那麼為什麼我們要繁瑣的通過apt來生成類文件而不是自己直接寫一個契約類來保存映射關係呢。如果站在一個框架開發者的角度去理解,就不難明白了,因為框架是給上層業務開發者調用的,如果業務開發者在開發頁面的過程中還要時不時的更新或更改契約類文件,不免過於麻煩?如果有自動根據路由地址生成映射表文件的技術該多好啊!
技術當然是有的,那就是被眾多框架使用的apt及javapoet技術,那麼什麼是apt,什麼是javapoet呢?我們先來看下圖: 由圖可知,apt是在編譯期對代碼中指定的註解進行解析,然後做一些其他處理(如通過javapoet生成新的Java文件)。我們常用的ButterKnife,其原理就是通過註解處理器在編譯期掃描代碼中加入的@BindView、@OnClick等註解進行掃描處理,然後生成XXX_ViewBinding類,實現了view的綁定。javapoet是鼎鼎大名的squareup出品的一個開源庫,是用來生成java文件的一個library,它提供了簡便的api供你去生成一個java文件。可以如下引入javapoet
implementation "com.squareup:javapoet:1.7.0"
下面我通過demo中的例子帶你了解如何通過apt和javapoet技術生成路由映射關係的類文件:
首先第一步,定義註解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface // 路由的路徑 // 將路由節點進行分組,可以實現動態載入
String group()
default
""
;}這裡看到Route註解里有path和group,這便是仿照ARouter對路由進行分組。因為當項目變得越來越龐大的時候,為了便於管理和減小首次載入路由表過於耗時的問題,我們對所有的路由進行分組。在ARouter中會要求路由地址至少需要兩級,如"/xx/xx",一個模塊下可以有多個分組,這裡我們就將路由地址定為必須大於等於兩級,其中第一級是group。
第二步,在Activity上使用註解:
@Route "/main/main" public class MainActivity extends
AppCompatActivity
{}@Route
(path ="/main/main2"
)public
class
Main2Activity
extends
AppCompatActivity
{}@Route
(path ="/show/info"
)public
class
ShowActivity
extends
AppCompatActivity
{}第三步,編寫註解處理器,在編譯期找到加入註解的類文件,進行處理,這裡我只展示關鍵代碼,具體的細節還需要你去demo中仔細研讀:
@AutoService class // 處理器接收的參數 @SupportedOptions // 註冊給哪些註解的 @SupportedAnnotationTypes class RouterProcessor extends AbstractProcessor
// key:組名 value:類名
privateMap
<String
,String
> rootMap =new
TreeMap<>();// 分組 key:組名 value:對應組的路由信息
privateMap
<String
,List
new
HashMap<>();//...
@Override
public synchronizedvoid
init(ProcessingEnvironment processingEnvironment) {super
.init(processingEnvironment);//...
elementUtils = processingEnvironment.getElementUtils(); typeUtils = processingEnvironment.getTypeUtils(); filerUtils = processingEnvironment.getFiler();//參數是模塊名 為了防止多模塊/組件化開發的時候 生成相同的 xx$$ROOT$$文件
Map
<String
,String
> options = processingEnvironment.getOptions();if
(!Utils.isEmpty(options)) { moduleName = options.get
(Constant.ARGUMENTS_NAME); }if
(Utils.isEmpty(moduleName)) {throw
new
RuntimeException("Not set processor moudleName option !"
); } log.i("init RouterProcessor "
+ moduleName +" success !"
); }@Override
public boolean process(Set
groupMeta = Warehouse.groupsIndex.get
(card.getGroup());if
(null
== groupMeta) {throw
new
NoRouteFoundException("沒找到對應路由:分組="
+ card.getGroup() +" 路徑="
+ card.getPath()); } IRouteGroup iGroupInstance;try
{ iGroupInstance = groupMeta.getConstructor().newInstance(); }catch
(Exception e) {throw
new
RuntimeException("路由分組映射表記錄失敗."
, e); } iGroupInstance.loadInto(Warehouse.routes);//已經準備過了就可以移除了 (不會一直存在內存中)
Warehouse.groupsIndex.remove
(card.getGroup());//再次進入 else
prepareCard(card); }else
{//類 要跳轉的activity 或IService實現類
card.setDestination(routeMeta.getDestination()); card.setType(routeMeta.getType());switch
(routeMeta.getType()) {case
ISERVICE: Class destination = routeMeta.getDestination(); IService service = Warehouse.services.get
(destination);if
(null
== service) {try
{ service = (IService) destination.getConstructor().newInstance(); Warehouse.services.put(destination, service); }catch
(Exception e) { e.printStackTrace(); } } card.setService(service);break
;default
:break
; } }}注意,Warehouse就是專門用來存放路由映射關係的類,裡面保存著存路由信息的map,這在ARouter裡面也是一樣的。這段代碼Warehouse.routes.get(card.getPath())通過path拿到對應的RouteMeta,這個RouteMeta裡面保存了activityClass等信息。繼續往下看,如果拿到的是空,會根據當前路由地址的group拿到對應的分組,通過反射創建實例,然後調用實例的loadInfo方法,把它裡面保存的映射信息添加到Warehouse.routes裡面,並且再次調用prepareCard(card),這時再通過Warehouse.routes.get(card.getPath())就可以順利拿到RouteMeta了。進入else{}裡面,調用了card.setDestination(routeMeta.getDestination()),這個setDestination就是將RouteMeta裡面保存的activityClass放入Postcard裡面,既然拿到了Activity.class,便可以通過startActivity啟動Activity了。
小結
到這我們的路由原理剖析及手寫實現已經講完了,由於篇幅限制,過多的細節不能一一給大家解釋,如果大家讀過文章之後還有什麼不能融會貫通的地方,可star我的github閱讀demo和更詳細的講解。EaseRouter本身只是參照ARouter手動實現的路由框架,並且剔除掉了很多東西,如過濾器等,如果想要用在項目里,建議還是用ARouter更好,畢竟這只是個練手項目,功能也不夠全面,當然有同學想對demo擴展後使用那當然更好,遇到什麼問題可以及時聯繫我。我的目的是通過自己手動實現路由框架來加深對知識的理解,如這裡面涉及到的知識點apt、javapoet和組件化思路、編寫框架的思路等。看到這裡,如果感覺乾貨很多,歡迎關注我的github,裡面會有更多乾貨!
Demo地址
仿ARouter一步步實現一個路由框架,點擊
閱讀原文
即可訪問,歡迎star。— — — END — — —
推薦閱讀
那些年面試求職中踩過的坑如果時光能倒流Android 開發應該掌握的 Proguard 技巧推薦閱讀:
※唐宋八大家文字功底最高的誰?
※最具升值潛力國畫十大家-----王天勝
※90 周年慶 | La Marzocco 怎樣贏得了大家的喜愛?(內附古董機型照片)
※運動、休閑、套裝西服文化三大家
※文壇伯樂——唐宋八大家中竟有五人出自他門下