ARouter原理剖析及手動實現

簡介

    最近可能入了魔怔,也多是閒的蛋疼,本身私下學習了ARouter的原理以及一些APT的知識,爲了加深對技術的理解,同時也本着熱愛開源的精神爲你們提供分享,因此就帶着你們強行擼碼,分析下ARouter路由原理和Android中APT的使用吧
    本篇文章我會帶着你們一步步手動實現路由框架來理解相似ARouter的路由框架原理,擼碼的demo我會附在文末。本路由框架就叫EaseRouter。(注:demo裏搭建了組件化開發,組件化和路由自己並無什麼聯繫,可是兩個單向依賴的組件之間須要互相啓動對方的Activity,由於沒有相互引用,startActivity()是實現不了的,必須須要一個協定的通訊的方式,此時相似ARouter和ActivityRouter的框架就派上用場了)。java

涉及知識點

  • Router框架原理
  • apt、javapoet知識
  • Router框架實現
第一節:組件化原理

    本文的重點是對路由框架的實現進行介紹,因此對於組件化的基本知識在文中不會過多闡述,若有同窗對組件化有不理解,能夠參考網上衆多的博客等介紹,而後再閱讀demo中的組件化配置進行熟悉,這裏附上github demo地址:ARouter原理剖析及手動實現,點我訪問源碼,歡迎star

git

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

    在組件化中,咱們一般都會在base_module上層再依賴一個router_module,而這個router_module就是負責各個模塊之間服務暴露和頁面跳轉的。
    用過ARouter路由框架的同窗應該都知道,在每一個須要對其餘module提供調用的Activity中,都會聲明相似下面@Route註解,咱們稱之爲路由地址github

@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文件一一對應保存,如直接保存在map中。爲了讓你們理解,我這裏來使用近乎僞代碼給你們簡單演示一下。api

//項目編譯後經過apt生成以下方法
public HashMap<String, ClassBean> routeInfo() {
    HashMap<String, ClassBean> route = new HashMap<String, ClassBean>();
    route.put("/main/main", MainActivity.class);
    route.put("/module1/module1main", Module1MainActivity.class);
    route.put("/login/login", LoginActivity.class);
}
複製代碼

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

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

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

那麼ARouter背後的原理是怎麼樣的呢?實際上它的核心思想跟上面講解的是同樣的,咱們在代碼里加入的@Route註解,會在編譯時期經過apt生成一些存儲path和activityClass映射關係的類文件,而後app進程啓動的時候會拿到這些類文件,把保存這些映射關係的數據讀到內存裏(保存在map裏),而後在進行路由跳轉的時候,經過build()方法傳入要到達頁面的路由地址,ARouter會經過它本身存儲的路由表找到路由地址對應的Activity.class(activity.class = map.get(path)),而後new Intent(),當調用ARouter的withString()方法它的內部會調用intent.putExtra(String name, String value),調用navigation()方法,它的內部會調用startActivity(intent)進行跳轉,這樣即可以實現兩個相互沒有依賴的module順利的啓動對方的Activity了。框架

第二節:Route註解的做用

    簡單講,要經過apt生成咱們的路由表,首先第一步須要定義註解ide

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    /**
     * 路由的路徑
     * @return
     */
    String path();

    /**
     * 將路由節點進行分組,能夠實現動態加載
     * @return
     */
    String group() default "";

}
複製代碼

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

@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 {}

複製代碼

在項目編譯的時候,咱們將會經過apt生成EaseRouter_Root_app文件和EaseRouter_Group_main、EEaseRouter_Group_show等文件,EaseRouter_Root_app文件對應於app module,裏面記錄着本module下全部的分組信息,EaseRouter_Group_main、EaseRouter_Group_show文件分別記載着當前分組的全部路由地址和ActivityClass映射信息。
本demo在編譯的時候會生成類以下所示,先不要管這些類是怎麼生成的,仔細看類的內容工具

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裏。這兩個接口是幹嗎的咱們先擱置,繼續往下看
若是咱們在login_module中想啓動app_module中的MainActivity類,首先,咱們已知MainActivity類的路由地址是"/main/main",第一個"/main"表明分組名,那麼咱們豈不是能夠像下面這樣調用去獲得MainActivity類文件,而後startActivity。這裏的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();
    }

}
複製代碼

能夠看到,只要有了這些實現了IRouteRoot和IRouteGroup的類文件,咱們便能輕易的啓動其餘module的Activity了。這些類文件,咱們能夠約定好以後,在代碼的編寫過程當中本身動手實現,也能夠經過apt生成。做爲一個框架,固然是自動解析Route註解而後生成這些類文件更好了。那麼就看下節,如何去生成這些文件。

第三節:apt和javapoet詳解

    經過上節咱們知道在Activity類上加上@Route註解以後,即可經過apt來生成對應的路由表,那麼這節咱們就來說述一下如何經過apt來生成路由表。這節我會拿着demo裏面的代碼來跟你們詳細介紹,咱們先來了解一下apt吧!
    APT是Annotation Processing Tool的簡稱,即註解處理工具。它是在編譯期對代碼中指定的註解進行解析,而後作一些其餘處理(如經過javapoet生成新的Java文件)。咱們經常使用的ButterKnife,其原理就是經過註解處理器在編譯期掃描代碼中加入的@BindView、@OnClick等註解進行掃描處理,而後生成XXX_ViewBinding類,實現了view的綁定。

    第一步:定義註解處理器,用來在編譯期掃描加入@Route註解的類,而後作處理。
這也是apt最核心的一步,新建RouterProcessor 繼承自 AbstractProcessor,而後實現process方法。在項目編譯期會執行RouterProcessor的process()方法,咱們即可以在這個方法裏處理Route註解了。此時咱們須要爲RouterProcessor指明它須要處理什麼註解,這裏引入一個google開源的自動註冊工具AutoService,以下依賴(也能夠手動進行註冊,不過略微麻煩):

implementation 'com.google.auto.service:auto-service:1.0-rc2'
複製代碼

這個工具能夠經過添加註解來爲RouterProcessor指定它須要的配置(固然也能夠本身手動去配置,不過會有點麻煩),以下所示

@AutoService(Processor.class)
public class RouterProcessor extends AbstractProcessor {

  //...
}
複製代碼

完整的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下存放路由信息的類文件名。在這以前,咱們須要在module的gradle下配置以下

javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName()]
            }
        }
複製代碼

Constant.ARGUMENTS_NAME即是每一個module的名字。

@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文件。這裏在process()裏生成文件用javapoet,這是squareup公司開源的一個庫,經過調用它的api,能夠很方便的生成java文件,在含有註解處理器(demo中apt相關的代碼實現都在easy-compiler module中)的module中引入依賴以下:

implementation 'com.squareup:javapoet:1.7.0'
複製代碼

好了,咱們終於能夠生成文件了,在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);
}

複製代碼

上節中提到過生成的root文件和group文件分別實現了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等文件的具體實現,代碼太多,我粘出一個實現供你們參考,其實生成java文件的思路都是同樣的,咱們只須要熟悉javapoet的api如何使用便可。你們能夠後續在demo裏詳細分析,這裏我只是講解核心的實現。

/**
 * 生成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 {
        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();
    }
}

複製代碼

能夠看到,ParameterizedTypeName是建立參數類型的api,ParameterSpec是建立參數的實現,MethodSpec是函數的生成實現等等。最後,當參數、方法、類信息都準備好了以後,調用JavaFileapi生成類文件。JavaFile的builder ()方法傳入了PACKAGE_OF_GENERATE_FILE變量,這個就是指定生成的類文件的目錄,方便咱們在app進程啓動的時候去遍歷拿到這些類文件。

第四節 實現路由框架的初始化

    經過前幾節的講解,咱們知道了看似很複雜的路由框架,其實原理很簡單,咱們能夠理解爲一個map(實際上是兩個map,一個保存group列表,一個保存group下的路由地址和activityClass關係)保存了路由地址和ActivityClass的映射關係,而後經過map.get("router address") 拿到AncivityClass,經過startActivity()調用就行了。但一個框架的設計要考慮的事情遠遠沒有這麼簡單。下面咱們就來分析一下:

    要實現這麼一個路由框架,首先咱們須要在用戶使用路由跳轉以前把這些路由映射關係拿到手,拿到這些路由關係最好的時機就是應用程序初始化的時候,前面的講解中我貼過幾行代碼,是經過apt生成的路由映射關係文件,爲了方便你們理解,我把這些文件從新粘貼到下面代碼中(這幾個類都是單獨的文件,在項目編譯後會在各個模塊的/build/generated/source/apt文件夾下面生成,爲了演示方便我只貼出來了app模塊下生成的類,其餘模塊如module一、module2下面的類跟app下面的沒有什麼區別),在程序啓動的時候掃描這些生成的類文件,而後獲取到映射關係信息,保存起來。

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接口的類都是保存了group分組映射信息,實現了IRouteGroup接口的類都保存了單個分組下的路由映射信息。只要咱們獲得實現IRouteRoot接口的全部類文件,便能經過循環調用它的loadInfo()方法獲得全部實現IRouteGroup接口的類,而全部實現IRouteGroup接口的類裏面保存了項目的全部路由信息。IRouteGroup的loadInfo()方法,經過傳入一個map,便會將這個分組裏的映射信息存入map裏。能夠看到map裏的value是「RouteMeta.build(RouteMeta.Type.ACTIVITY,ShowActivity.class,"/show/info","show")」,RouteMeta.build()會返回RouteMeta,RouteMeta裏面便保存着ActivityClass的全部信息。那麼咱們這個框架,就有了第一個功能需求,即是在app進程啓動的時候進行框架的初始化(或者在你開始用路由跳轉以前進行初始化均可以),在初始化中拿到映射關係信息,保存在map裏,以便程序運行中能夠快速找到路由映射信息實現跳轉。下面看具體的初始化代碼。
注:這裏咱們只講解大致的思路,不會細緻到講解每個方法每一行代碼的具體做用,跟着個人思路你會明白框架設計的具體細節,每一步要實現的功能是什麼,可是精確到方法和每一行代碼的具體含義你還須要仔細研讀demo。

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        EasyRouter.init(this);
    }
}

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);
       }
   }

   //...
}

複製代碼

能夠看到,init()方法中調用了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接口的類文件集合,經過上面的講解咱們知道,拿到這些類文件即可以獲得全部的routerAddress---activityClass映射關係。
這個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 paths = getSourcePaths(context)這句代碼會得到全部的apk文件(instant run會產生不少split apk),這個方法的具體實現你們kandemo便可,再也不闡述。這裏用到了CountDownLatch類,會分path一個文件一個文件的檢索,等到全部的類文件都找到後便會返回這個Set集合。因此咱們能夠知道,初始化時找到這些類文件會有必定的耗時,因此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;

    //服務
    private IService service;

    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;}

    public IService getService() {
        return service;
    }

    public void setService(IService service) {
        this.service = service;
    }

    /**
     * Intent.FLAG_ACTIVITY**
     * @param flag
     * @return
     */
    public Postcard withFlags(int flag) {
        this.flags = flag;
        return this;
    }

    public int getFlags() {
        return flags;
    }

    /**
     * 跳轉動畫
     *
     * @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 withShort(@Nullable String key, short value) {
        mBundle.putShort(key, value);
        return this;
    }


    public Postcard withInt(@Nullable String key, int value) {
        mBundle.putInt(key, value);
        return this;
    }


    public Postcard withLong(@Nullable String key, long value) {
        mBundle.putLong(key, value);
        return this;
    }


    public Postcard withDouble(@Nullable String key, double value) {
        mBundle.putDouble(key, value);
        return this;
    }


    public Postcard withByte(@Nullable String key, byte value) {
        mBundle.putByte(key, value);
        return this;
    }


    public Postcard withChar(@Nullable String key, char value) {
        mBundle.putChar(key, value);
        return this;
    }


    public Postcard withFloat(@Nullable String key, float value) {
        mBundle.putFloat(key, value);
        return this;
    }


    public Postcard withParcelable(@Nullable String key, @Nullable Parcelable value) {
        mBundle.putParcelable(key, value);
        return this;
    }


    public Postcard withStringArray(@Nullable String key, @Nullable String[] value) {
        mBundle.putStringArray(key, value);
        return this;
    }


    public Postcard withBooleanArray(@Nullable String key, boolean[] value) {
        mBundle.putBooleanArray(key, value);
        return this;
    }


    public Postcard withShortArray(@Nullable String key, short[] value) {
        mBundle.putShortArray(key, value);
        return this;
    }


    public Postcard withIntArray(@Nullable String key, int[] value) {
        mBundle.putIntArray(key, value);
        return this;
    }


    public Postcard withLongArray(@Nullable String key, long[] value) {
        mBundle.putLongArray(key, value);
        return this;
    }


    public Postcard withDoubleArray(@Nullable String key, double[] value) {
        mBundle.putDoubleArray(key, value);
        return this;
    }


    public Postcard withByteArray(@Nullable String key, byte[] value) {
        mBundle.putByteArray(key, value);
        return this;
    }


    public Postcard withCharArray(@Nullable String key, char[] value) {
        mBundle.putCharArray(key, value);
        return this;
    }


    public Postcard withFloatArray(@Nullable String key, float[] value) {
        mBundle.putFloatArray(key, value);
        return this;
    }


    public Postcard withParcelableArray(@Nullable String key, @Nullable Parcelable[] value) {
        mBundle.putParcelableArray(key, value);
        return this;
    }

    public Postcard withParcelableArrayList(@Nullable String key, @Nullable ArrayList<? extends
            Parcelable> value) {
        mBundle.putParcelableArrayList(key, value);
        return this;
    }

    public Postcard withIntegerArrayList(@Nullable String key, @Nullable ArrayList<Integer> value) {
        mBundle.putIntegerArrayList(key, value);
        return this;
    }

    public Postcard withStringArrayList(@Nullable String key, @Nullable ArrayList<String> value) {
        mBundle.putStringArrayList(key, value);
        return this;
    }

    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;
        case ISERVICE:
            return postcard.getService();
        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就是專門用來存放路由映射關係的類,這在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和組件化思路、編寫框架的思路等。看到這裏,若是感受乾貨不少,歡迎點個star或分享給更多人。

demo地址

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

聯繫方式

email:xiasem@163.com

相關文章
相關標籤/搜索