ARouter原理剖析及手動實現

ARouter原理剖析及手動實現

 

前言

路由跳轉在項目中用了一段時間了,最近對Android中的ARouter路由原理也是研究了一番,因而就給你們分享一下本身的心得體會,並教你們如何實現一款簡易的路由框架。

本篇文章分爲兩個部分,第一部分着重剖析ARouter路由的原理,第二部分會帶着你們仿照ARouter擼一個本身的路由框架,咱們本身擼的路由框架可能沒有Arouter衆多的功能如過濾器、provider等,可是卻實現了ARouter最核心的功能:路由跳轉,同時你也能學會如何去設計一個框架等等。

這裏先附上我本身實現的路由框架demo地址:ARouter原理剖析及手動實現,demo點我訪問,歡迎starjavascript

第一部分:ARouter原理剖析

說到路由便不得不提一下Android中的組件化開發思想,組件化是最近比較流行的架構設計方案,它能對代碼進行高度的解耦、模塊分離等,能極大地提升開發效率(若有同窗對組件化有不理解,能夠參考網上衆多的博客等介紹,而後再閱讀demo源碼中的組件化配置進行熟悉)。路由和組件化自己沒有什麼聯繫,由於路由的責任是負責頁面跳轉,可是組件化中兩個單向依賴的module之間須要互相啓動對方的Activity,由於沒有相互引用,startActivity()是實現不了的,必須須要一個協定的通訊方式,此時相似ARouter和ActivityRouter等的路由框架就派上用場了。php

  • 第一節:ARouter路由跳轉的原理

<img src="http://pcayc3ynm.bkt.clouddn.com/module_1.png" /> java

如上圖,在組件化中,爲了業務邏輯的完全解耦,同時也爲了每一個module均可以方便的單獨運行和調試,上層的各個module不會進行相互依賴(只有在正式聯調的時候纔會讓app殼module去依賴上層的其餘組件module),而是共同依賴於base module,base module中會依賴一些公共的第三方庫和其餘配置。那麼在上層的各個module中,如何進行通訊呢?

咱們知道,傳統的Activity之間通訊,經過startActivity(intent),而在組件化的項目中,上層的module沒有依賴關係(即使兩個module有依賴關係,也只能是單向的依賴),那麼假如login module中的一個Activity須要啓動pay_module中的一個Activity便不能經過startActivity來進行跳轉。那麼你們想一下還有什麼其餘辦法呢? 可能有同窗會想到隱式跳轉,這固然也是一種解決方法,可是一個項目中不可能全部的跳轉都是隱式的,這樣Manifest文件會有不少過濾配置,並且很是不利於後期維護。固然你用反射拿到Activity的class文件也能夠實現跳轉,可是第一:大量的使用反射跳轉對性能會有影響,第二:你須要拿到Activity的類文件,在組件開發的時候,想拿到其餘module的類文件是很麻煩的,由於組件開發的時候組件module之間是沒有相互引用的,你只能經過找到類的路徑去反射拿到這個class,那麼有沒有一種更好的解決辦法呢?辦法固然是有的。下面看圖:

<img src="http://pcayc3ynm.bkt.clouddn.com/module_2.png" />

在組件化中,咱們一般都會在base_module上層再依賴一個router_module,而這個router_module就是負責各個模塊之間頁面跳轉的。

用過ARouter路由框架的同窗應該都知道,在每一個須要對其餘module提供調用的Activity中,都會聲明相似下面@Route註解,咱們稱之爲路由地址git

@Route(path = "/main/main") public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } } @Route(path = "/module1/module1main") public class Module1MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_module1_main); } } 

那麼這個註解有什麼用呢,路由框架會在項目的編譯期經過註解處理器掃描全部添加@Route註解的Activity類,而後將Route註解中的path地址和Activity.class文件映射關係保存到它本身生成的java文件中。爲了讓你們理解,我這裏來使用近乎僞代碼給你們簡單演示一下。github

public class MyRouters{ //項目編譯後經過apt生成以下方法 public static HashMap<String, ClassBean> getRouteInfo(HashMap<String, ClassBean> routes) { route.put("/main/main", MainActivity.class); route.put("/module1/module1main", Module1MainActivity.class); route.put("/login/login", LoginActivity.class); } } 

這樣咱們想在app模塊的MainActivity跳轉到login模塊的LoginActivity,那麼便只需調用以下:api

//不一樣模塊之間啓動Activity public void login(String name, String password) { HashMap<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應該是下面這個寫法bash

// 2. Jump with parameters ARouter.getInstance().build("/test/login") .withString("password", 666666) .withString("name", "小三") .navigation(); 

那麼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來生成類文件而不是本身直接寫一個契約類來保存映射關係呢。若是站在一個框架開發者的角度去理解,就不難明白了,由於框架是給上層業務開發者調用的,若是業務開發者在開發頁面的過程當中還要時不時的更新或更改契約類文件,難免過於麻煩?若是有自動根據路由地址生成映射表文件的技術該多好啊!app

技術固然是有的,那就是被衆多框架使用的apt及javapoet技術,那麼什麼是apt,什麼是javapoet呢?咱們先來看下圖:

<img src="http://pcayc3ynm.bkt.clouddn.com/apt_javapoet.png" />

APT是Annotation Processing Tool的簡稱,即註解處理工具。由圖可知,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 Route { /** * 路由的路徑 * @return */ String path(); /** * 將路由節點進行分組,能夠實現動態加載 * @return */ String group() default ""; } 

這裏看到Route註解裏有path和group,這即是仿照ARouter對路由進行分組。由於當項目變得愈來愈龐大的時候,爲了便於管理和減少首次加載路由表過於耗時的問題,咱們對全部的路由進行分組。在ARouter中會要求路由地址至少須要兩級,如"/xx/xx",一個模塊下能夠有多個分組,這裏咱們就將路由地址定爲必須大於等於兩級,其中第一級是group。

第二步,在Activity上使用註解

@Route(path = "/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(Processor.class)
/** 處理器接收的參數 替代 {@link AbstractProcessor#getSupportedOptions()} 函數 */ @SupportedOptions(Constant.ARGUMENTS_NAME) /** * 註冊給哪些註解的 替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函數 */ @SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE) public class RouterProcessor extends AbstractProcessor { /** * key:組名 value:類名 */ private Map<String, String> rootMap = new TreeMap<>(); /** * 分組 key:組名 value:對應組的路由信息 */ private Map<String, List<RouteMeta>> groupMap = new HashMap<>(); /** * * @param set 使用了支持處理註解的節點集合 * @param roundEnvironment 表示當前或是以前的運行環境,能夠經過該對象查找找到的註解。 * @return true 表示後續處理器不會再處理(已經處理) */ @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { if (!Utils.isEmpty(set)) { //被Route註解的節點集合 Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class); if (!Utils.isEmpty(rootElements)) { processorRoute(rootElements); } return true; } return false; } //... } 

如代碼中所示,要想在編譯期對註解作處理,就須要RouterProcessor繼承自AbstractProcessor並經過@AutoService註解進行註冊,而後實現process()方法。尚未完,你還須要經過@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)指定要處理哪一個註解,Constant.ANNOTATION_TYPE_ROUTE即是咱們的Route註解的路徑。看process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)方法,set集合就是編譯期掃描代碼獲得的加入了Route註解的文件集合,而後咱們就能夠在process方法生成java文件了。

這裏的@AutoService是爲了註冊註解處理器,須要咱們引入一個google開源的自動註冊工具AutoService,以下依賴(固然也能夠手動進行註冊,不過略微麻煩,這裏不太推薦):

implementation 'com.google.auto.service:auto-service:1.0-rc2' 

第四步:經過javapoet生成java類:
在第三步中process()方法裏有一句代碼:processorRoute(rootElements),這個就是生成java文件的方法了,下面我貼出代碼:

private void processorRoute(Set<? extends Element> rootElements) { //... //生成Group記錄分組表 generatedGroup(iRouteGroup); //生成Root類 做用:記錄<分組,對應的Group類> generatedRoot(iRouteRoot, iRouteGroup); } 

processorRoute()方法內容不少,這裏我只貼出生成java文件相關,其餘代碼我會在第二部分手動實現路由框架中詳細介紹。如上,generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)就是生成java文件的核心了。這裏我只貼出generatedRoot()方法,由於生成類文件的原理都是同樣的,至於生成什麼功能的類,只要你會一個,觸類旁通,這便沒有什麼難度。

/** * 生成Root類 做用:記錄<分組,對應的Group類> * @param iRouteRoot * @param iRouteGroup */ private void generatedRoot(TypeElement iRouteRoot, TypeElement iRouteGroup) { //建立參數類型 Map<String,Class<? extends IRouteGroup>> routes> //Wildcard 通配符 ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get( ClassName.get(Map.class), ClassName.get(String.class), ParameterizedTypeName.get( ClassName.get(Class.class), WildcardTypeName.subtypeOf(ClassName.get(iRouteGroup)) )); //生成參數 Map<String,Class<? extends IRouteGroup>> routes> routes ParameterSpec parameter = ParameterSpec.builder(parameterizedTypeName, "routes").build(); //生成函數 public void loadInfo(Map<String,Class<? extends IRouteGroup>> routes> routes) MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constant.METHOD_LOAD_INTO) .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(parameter); //生成函數體 for (Map.Entry<String, String> entry : rootMap.entrySet()) { methodBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(Constant.PACKAGE_OF_GENERATE_FILE, entry.getValue())); } //生成$Root$類 String className = Constant.NAME_OF_ROOT + moduleName; TypeSpec typeSpec = TypeSpec.classBuilder(className) .addSuperinterface(ClassName.get(iRouteRoot)) .addModifiers(Modifier.PUBLIC) .addMethod(methodBuilder.build()) .build(); try { //生成java文件,PACKAGE_OF_GENERATE_FILE就是生成文件須要的路徑 JavaFile.builder(Constant.PACKAGE_OF_GENERATE_FILE, typeSpec).build().writeTo(filerUtils); log.i("Generated RouteRoot:" + Constant.PACKAGE_OF_GENERATE_FILE + "." + className); } catch (IOException e) { e.printStackTrace(); } } 

如上,我把每一塊代碼的做用註釋了出來,相信你們很容易就能理解每個代碼段的做用。可見,其實生成文件只是調用一些api而已,只要咱們熟知api的調用,生成java文件便沒有什麼難度。

第二部分:動手實現一個路由框架

經過第一部分的講述,我相信你們對於ARouter的原理已經有了總體輪廓的理解,這一部分,我便會經過代碼帶你去實現一個本身的路由框架。要實現這個路由框架,咱們先來實現生成路由映射文件這一塊,由於這一塊是路由框架可以運行起來的核心。

  • 第一節:生成路由映射文件

經過第一部分的講述咱們知道在Activity類上加上@Route註解以後,即可經過apt來生成對應的路由表,那麼如今咱們就來生成這些路由映射文件。首先,咱們要理解一個問題,就是咱們的路由映射文件是在編譯期間生成的,那麼在程序的運行期間咱們要統一調用這些路由信息,便須要一個統一的調用方式。咱們先來定義這個調用方式:

public interface IRouteGroup { void loadInto(Map<String, RouteMeta> atlas); } public interface IRouteRoot { void loadInto(Map<String, Class<? extends IRouteGroup>> routes); } 

咱們定義兩個接口來對生成的java文件進行約束,IRouteGroup是生成的分組關係契約,IRouteRoot是單個分組路由信息契約,只要咱們生成的java文件繼承自這個接口並實現loadInto()方法,在運行期間咱們就能夠統一的調用生成的java文件,獲取路由映射信息。

如今咱們來把RouterProcessor生成路由映射文件相關的代碼補全:

@AutoService(Processor.class) /** 處理器接收的參數 替代 {@link AbstractProcessor#getSupportedOptions()} 函數 */ @SupportedOptions(Constant.ARGUMENTS_NAME) /** * 指定使用的Java版本 替代 {@link AbstractProcessor#getSupportedSourceVersion()} 函數 */ @SupportedSourceVersion(SourceVersion.RELEASE_7) /** * 註冊給哪些註解的 替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函數 */ @SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE) public class RouterProcessor extends AbstractProcessor { /** * key:組名 value:類名 */ private Map<String, String> rootMap = new TreeMap<>(); /** * 分組 key:組名 value:對應組的路由信息 */ private Map<String, List<RouteMeta>> groupMap = new HashMap<>(); /** * 節點工具類 (類、函數、屬性都是節點) */ private Elements elementUtils; /** * type(類信息)工具類 */ private Types typeUtils; /** * 文件生成器 類/資源 */ private Filer filerUtils; private String moduleName; private Log log; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); //得到apt的日誌輸出 log = Log.newLog(processingEnvironment.getMessager()); 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 !"); } /** * * @param set 使用了支持處理註解的節點集合 * @param roundEnvironment 表示當前或是以前的運行環境,能夠經過該對象查找找到的註解。 * @return true 表示後續處理器不會再處理(已經處理) */ @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { if (!Utils.isEmpty(set)) { //被Route註解的節點集合 Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class); if (!Utils.isEmpty(rootElements)) { processorRoute(rootElements); } return true; } return false; } //... } 

咱們經過@SupportedOptions(Constant.ARGUMENTS_NAME)拿到每一個module的名字,用來生成對應module下存放路由信息的類文件名。這裏變量Constant.ARGUMENTS_NAME的值就是moduleName,在這以前,咱們須要在每一個組件module的gradle下配置以下

javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName()] } } 

@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)指定了須要處理的註解的路徑地址,在此就是Route.class的路徑地址。

RouterProcessor中咱們實現了init方法,拿到log apt日誌輸出工具用以輸出apt日誌信息,並經過如下代碼獲得上面提到的每一個module配置的moduleName

//參數是模塊名 爲了防止多模塊/組件化開發的時候 生成相同的 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 !"); } 

而後在process()方法裏開始生成文件名以EaseRouter_Route_moduleName和EaseRouter_Group_moduleName命名的文件。(這裏的moduleName指具體的module名,demo中apt相關的代碼實現都在easy-compiler module中),生成EaseRouter_Route_moduleName相關文件存儲的就是分組關係,生成EaseRouter_Group_moduleName相關文件裏存儲的就是分組下的路由映射關係。

好了,咱們終於能夠生成文件了,在process()方法裏有以下代碼,

if (!Utils.isEmpty(set)) { //被Route註解的節點集合 Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class); if (!Utils.isEmpty(rootElements)) { processorRoute(rootElements); } return true; } return false; 

set就是掃描獲得的支持處理註解的節點集合,而後獲得rootElements,即被@Route註解的節點集合,此時就能夠調用
processorRoute(rootElements)方法去生成文件了。processorRoute(rootElements)方法實現以下:

private void processorRoute(Set<? extends Element> rootElements) { //得到Activity這個類的節點信息 TypeElement activity = elementUtils.getTypeElement(Constant.ACTIVITY); TypeElement service = elementUtils.getTypeElement(Constant.ISERVICE); for (Element element : rootElements) { RouteMeta routeMeta; //類信息 TypeMirror typeMirror = element.asType(); log.i("Route class:" + typeMirror.toString()); Route route = element.getAnnotation(Route.class); if (typeUtils.isSubtype(typeMirror, activity.asType())) { routeMeta = new RouteMeta(RouteMeta.Type.ACTIVITY, route, element); } else if (typeUtils.isSubtype(typeMirror, service.asType())) { routeMeta = new RouteMeta(RouteMeta.Type.ISERVICE, route, element); } else { throw new RuntimeException("Just support Activity or IService Route: " + element); } categories(routeMeta); } TypeElement iRouteGroup = elementUtils.getTypeElement(Constant.IROUTE_GROUP); TypeElement iRouteRoot = elementUtils.getTypeElement(Constant.IROUTE_ROOT); //生成Group記錄分組表 generatedGroup(iRouteGroup); //生成Root類 做用:記錄<分組,對應的Group類> generatedRoot(iRouteRoot, iRouteGroup); } 

上面提到的生成的EaseRouter_Route_moduleName文件和EaseRouter_Group_moduleName文件分別實現了IRouteRoot和IRouteGroup接口,就是經過下面這兩行代碼拿到IRootGroup和IRootRoot的字節碼信息,而後傳入generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)方法,這兩個方法內部會經過javapoet api生成java文件,並實現這兩個接口。

TypeElement iRouteGroup = elementUtils.getTypeElement(Constant.IROUTE_GROUP);
TypeElement iRouteRoot = elementUtils.getTypeElement(Constant.IROUTE_ROOT);

generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)就是生成上面提到的EaseRouter_Root_app和EaseRouter_Group_main等文件的具體實現,生成的方法我在第一部分已經貼出來過了,這裏再也不闡述。

好了,如今咱們編譯下項目就會在每一個組件module的build/generated/source/apt目錄下生成相關映射文件。這裏我把app module編譯後生成的文件貼出來,app module編譯後會生成EaseRouter_Root_app文件和EaseRouter_Group_main、EEaseRouter_Group_show等文件,EaseRouter_Root_app文件對應於app module的分組,裏面記錄着本module下全部的分組信息,EaseRouter_Group_main、EaseRouter_Group_show文件分別記載着當前分組下的全部路由地址和ActivityClass映射信息。以下所示:

public class EaseRouter_Root_app implements IRouteRoot { @Override public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) { routes.put("main", EaseRouter_Group_main.class); routes.put("show", EaseRouter_Group_show.class); } } public class EaseRouter_Group_main implements IRouteGroup { @Override public void loadInto(Map<String, RouteMeta> atlas) { atlas.put("/main/main",RouteMeta.build(RouteMeta.Type.ACTIVITY,Main2\Activity.class,"/main/main","main")); atlas.put("/main/main2",RouteMeta.build(RouteMeta.Type.ACTIVITY,Main2\Activity.class,"/main/main2","main")); } } public class EaseRouter_Group_show implements IRouteGroup { @Override public void loadInto(Map<String, RouteMeta> atlas) { atlas.put("/show/info",RouteMeta.build(RouteMeta.Type.ACTIVITY,ShowActivity.class,"/show/info","show")); } } 

你們會看到生成的類分別實現了IRouteRoot和IRouteGroup接口,而且實現了loadInto()方法,而loadInto方法經過傳入一個特定類型的map就能把分組信息放入map裏,只要分組信息存入到特定的map裏後,咱們就能夠隨意的從map裏取路由地址對應的Activity.class作跳轉使用。那麼若是咱們在login_module中想啓動app_module中的MainActivity類,首先,咱們已知MainActivity類的路由地址是"/main/main",第一個"/main"表明分組名,那麼咱們豈不是能夠像下面這樣調用去獲得MainActivity類文件,而後startActivity()跳轉到MainActivity。(這裏的RouteMeta只是存有Activity class文件的封裝類,先不用理會)。

public void test() { EaseRouter_Root_app rootApp = new EaseRouter_Root_app(); HashMap<String, Class<? extends IRouteGroup>> rootMap = new HashMap<>(); rootApp.loadInto(rootMap); //獲得/main分組 Class<? extends IRouteGroup> aClass = rootMap.get("main"); try { HashMap<String, RouteMeta> groupMap = new HashMap<>(); aClass.newInstance().loadInto(groupMap); //獲得MainActivity RouteMeta main = groupMap.get("/main/main"); Class<?> mainActivityClass = main.getDestination(); Intent intent = new Intent(this, mainActivityClass); startActivity(intent); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } 

能夠看到,只要有了這些附帶路由映射信息的類文件,並將其保存的映射關係存入map裏,咱們便能輕易的啓動其餘module的Activity了。

  • 第二節 路由框架的初始化

上節咱們已經經過apt生成了映射文件,而且知道了如何經過映射文件去調用Activity,然而咱們要實現一個路由框架,就要考慮在合適的時機拿到這些映射文件中的信息,以供上層業務作跳轉使用。那麼在什麼時機去拿到這些映射文件中的信息呢?首先咱們須要在上層業務作路由跳轉以前把這些路由映射關係拿到手,但咱們不能事先預知上層業務會在何時作跳轉,那麼拿到這些路由關係最好的時機就是應用程序初始化的時候。

知道了在什麼時機去拿到映射關係,接下來就要考慮如何拿了。咱們在上面已經介紹過實現IRouteRoot接口的全部類文件裏保存着各個module的分組文件(分組文件就是實現了IRouteGroup接口的類文件),那麼只要拿到全部實現IRouteGroup接口的類的集合,便能獲得左右的路由信息了。下面看初始化的代碼:

public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); EasyRouter.init(this); } } //咱們手動實現的路由框架,咱們就叫它EasyRouter public class EasyRouter { private static final String TAG = "EasyRouter"; private static final String ROUTE_ROOT_PAKCAGE = "com.xsm.easyrouter.routes"; private static final String SDK_NAME = "EaseRouter"; private static final String SEPARATOR = "_"; private static final String SUFFIX_ROOT = "Root"; private static EasyRouter sInstance; private static Application mContext; private Handler mHandler; private EasyRouter() { mHandler = new Handler(Looper.getMainLooper()); } public static EasyRouter getsInstance() { synchronized (EasyRouter.class) { if (sInstance == null) { sInstance = new EasyRouter(); } } return sInstance; } public static void init(Application application) { mContext = application; try { loadInfo(); } catch (Exception e) { e.printStackTrace(); Log.e(TAG, "初始化失敗!", e); } } //... } 

能夠看到,進程啓動的時候咱們調用EasyRouter.init()方法,init()方法中調用了loadInfo()方法,而這個loadInfo()即是咱們初始化的核心。我把loadInfo的代碼貼出來:

private static void loadInfo() throws PackageManager.NameNotFoundException, InterruptedException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //得到全部 apt生成的路由類的全類名 (路由表) Set<String> routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE); for (String className : routerMap) { if (className.startsWith(ROUTE_ROOT_PAKCAGE + "." + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) { //root中註冊的是分組信息 將分組信息加入倉庫中 ((IRouteRoot) Class.forName(className).getConstructor().newInstance()).loadInto(Warehouse.groupsIndex); } } for (Map.Entry<String, Class<? extends IRouteGroup>> stringClassEntry : Warehouse.groupsIndex.entrySet()) { Log.d(TAG, "Root映射表[ " + stringClassEntry.getKey() + " : " + stringClassEntry.getValue() + "]"); } } 

咱們首先經過ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)獲得apt生成的全部實現IRouteRoot接口的類文件集合,經過上面的講解咱們知道,拿到這些類文件即可以獲得全部的路由地址和Activity映射關係。

這個ClassUtils.getFileNameByPackageName()方法就是具體的實現了,下面咱們看具體的代碼:

/** * 獲得路由表的類名 * @param context * @param packageName * @return * @throws PackageManager.NameNotFoundException * @throws InterruptedException */ public static Set<String> getFileNameByPackageName(Application context, final String packageName) throws PackageManager.NameNotFoundException, InterruptedException { final Set<String> classNames = new HashSet<>(); List<String> paths = getSourcePaths(context); //使用同步計數器判斷均處理完成 final CountDownLatch countDownLatch = new CountDownLatch(paths.size()); ThreadPoolExecutor threadPoolExecutor = DefaultPoolExecutor.newDefaultPoolExecutor(paths.size()); for (final String path : paths) { threadPoolExecutor.execute(new Runnable() { @Override public void run() { DexFile dexFile = null; try { //加載 apk中的dex 並遍歷 得到全部包名爲 {packageName} 的類 dexFile = new DexFile(path); Enumeration<String> dexEntries = dexFile.entries(); while (dexEntries.hasMoreElements()) { String className = dexEntries.nextElement(); if (!TextUtils.isEmpty(className) && className.startsWith(packageName)) { classNames.add(className); } } } catch (IOException e) { e.printStackTrace(); } finally { if (null != dexFile) { try { dexFile.close(); } catch (IOException e) { e.printStackTrace(); } } //釋放一個 countDownLatch.countDown(); } } }); } //等待執行完成 countDownLatch.await(); return classNames; } 

這個方法會經過開啓子線程,去掃描apk中全部的dex,遍歷找到全部包名爲packageName的類名,而後將類名再保存到classNames集合裏。

List<String> paths = getSourcePaths(context)這句代碼會得到全部的apk文件(instant run會產生不少split apk),這個方法的具體實現你們看demo便可,再也不闡述。這裏用到了CountDownLatch類,會分path一個文件一個文件的檢索,等到全部的類文件都找到後便會返回這個Set<String>集合。因此咱們能夠知道,初始化時找到這些類文件會有必定的耗時,若是你已經看過ARouter的源碼便會知道ARouter這裏會有一些優化,只會遍歷找一次類文件,找到以後就會保存起來,下次app進程啓動會檢索是否有保存這些文件,若是有就會直接調用保存後的數據去初始化。

  • 第三節 路由跳轉實現

通過上節的介紹,咱們已經可以在進程初始化的時候拿到全部的路由信息,那麼實現跳轉便好作了。直接看代碼:

@Route(path = "/main/main") public class MainActivity extends AppCompatActivity { public void startModule1MainActivity(View view) { EasyRouter.getsInstance().build("/module1/module1main").navigation(); } } 

在build的時候,傳入要跳轉的路由地址,build()方法會返回一個Postcard對象,咱們稱之爲跳卡。而後調用Postcard的navigation()方法完成跳轉。用過ARouter的對這個跳卡都應該很熟悉吧!Postcard裏面保存着跳轉的信息。下面我把Postcard類的代碼實現粘下來:

public class Postcard extends RouteMeta { private Bundle mBundle; private int flags = -1; //新版風格 private Bundle optionsCompat; //老版 private int enterAnim; private int exitAnim; public Postcard(String path, String group) { this(path, group, null); } public Postcard(String path, String group, Bundle bundle) { setPath(path); setGroup(group); this.mBundle = (null == bundle ? new Bundle() : bundle); } public Bundle getExtras() {return mBundle;} public int getEnterAnim() {return enterAnim;} public int getExitAnim() {return exitAnim;} /** * 跳轉動畫 * @param enterAnim * @param exitAnim * @return */ public Postcard withTransition(int enterAnim, int exitAnim) { this.enterAnim = enterAnim; this.exitAnim = exitAnim; return this; } /** * 轉場動畫 * @param compat * @return */ public Postcard withOptionsCompat(ActivityOptionsCompat compat) { if (null != compat) { this.optionsCompat = compat.toBundle(); } return this; } public Postcard withString(@Nullable String key, @Nullable String value) { mBundle.putString(key, value); return this; } public Postcard withBoolean(@Nullable String key, boolean value) { mBundle.putBoolean(key, value); return this; } public Postcard withInt(@Nullable String key, int value) { mBundle.putInt(key, value); return this; } //還有許多給intent中bundle設置值得方法我就不一一列出來了,能夠看demo裏全部的細節 public Bundle getOptionsBundle() { return optionsCompat; } public Object navigation() { return EasyRouter.getsInstance().navigation(null, this, -1, null); } public Object navigation(Context context) { return EasyRouter.getsInstance().navigation(context, this, -1, null); } public Object navigation(Context context, NavigationCallback callback) { return EasyRouter.getsInstance().navigation(context, this, -1, callback); } public Object navigation(Context context, int requestCode) { return EasyRouter.getsInstance().navigation(context, this, requestCode, null); } public Object navigation(Context context, int requestCode, NavigationCallback callback) { return EasyRouter.getsInstance().navigation(context, this, requestCode, callback); } } 

若是你是一個Android開發,Postcard類裏面的東西就不用我再給你介紹了吧!(哈哈)我相信你一看就明白了。咱們只介紹一個方法navigation(),他有好幾個重載方法,方法裏面會調用EasyRouter類的navigation()方法。EaseRouter的navigation()方法,就是跳轉的核心了。下面請看:

protected Object navigation(Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) { try { prepareCard(postcard); }catch (NoRouteFoundException e) { e.printStackTrace(); //沒找到 if (null != callback) { callback.onLost(postcard); } return null; } if (null != callback) { callback.onFound(postcard); } switch (postcard.getType()) { case ACTIVITY: final Context currentContext = null == context ? mContext : context; final Intent intent = new Intent(currentContext, postcard.getDestination()); intent.putExtras(postcard.getExtras()); int flags = postcard.getFlags(); if (-1 != flags) { intent.setFlags(flags); } else if (!(currentContext instanceof Activity)) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } mHandler.post(new Runnable() { @Override public void run() { //可能須要返回碼 if (requestCode > 0) { ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle()); } else { ActivityCompat.startActivity(currentContext, intent, postcard .getOptionsBundle()); } if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) { //老版本 ((Activity) currentContext).overridePendingTransition(postcard .getEnterAnim() , postcard.getExitAnim()); } //跳轉完成 if (null != callback) { callback.onArrival(postcard); } } }); break; default: break; } return null; } 

這個方法裏先去調用了prepareCard(postcard)方法,prepareCard(postcard)代碼我貼出來,

private void prepareCard(Postcard card) { RouteMeta routeMeta = Warehouse.routes.get(card.getPath()); if (null == routeMeta) { Class<? extends IRouteGroup> 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等信息。繼續往下看,若是判斷拿到的RouteMeta是空,說明這個路由地址尚未加載到map裏面(初始化時爲了節省性能,只會加載全部的分組信息,而每一個分組下的路由映射關係,會使用懶加載,在首次用到的時候去加載),只有在第一次用到當前路由地址的時候,會去Warehouse.routes裏面拿routeMeta,若是拿到的是空,會根據當前路由地址的group拿到對應的分組,經過反射建立實例,而後調用實例的loadInfo方法,把它裏面保存的映射信息添加到Warehouse.routes裏面,而且再次調用prepareCard(card),這時再經過Warehouse.routes.get(card.getPath())就能夠順利拿到RouteMeta了。進入else{}裏面,調用了card.setDestination(routeMeta.getDestination()),這個setDestination就是將RouteMeta裏面保存的activityClass放入Postcard裏面,下面switch代碼塊能夠先不用看,這是實現ARouter中經過依賴注入實現Provider 服務的邏輯,有心研究的同窗能夠去讀一下demo。

好了,prepareCard()方法調用完成後,咱們的postcard裏面就保存了activityClass,而後switch (postcard.getType()){}會判斷postcard的type爲ACTIVITY,而後經過ActivityCompat.startActivity啓動Activity。到這裏,路由跳轉的實現已經講解完畢了。

小結

EaseRouter自己只是參照ARouter手動實現的路由框架,而且剔除掉了不少東西,如過濾器等,若是想要用在項目裏,建議仍是用ARouter更好,畢竟這只是個練手項目,功能也不夠全面,固然有同窗想對demo擴展後使用那固然更好,遇到什麼問題能夠及時聯繫我。個人目的是經過本身手動實現路由框架來加深對知識的理解,如這裏面涉及到的知識點apt、javapoet和組件化思路、編寫框架的思路等。看到這裏,若是感受乾貨不少,歡迎關注個人github,裏面會有更多幹貨!

demo地址

仿ARouter一步步實現一個路由框架,點我訪問源碼,歡迎star

相關文章
相關標籤/搜索