ARouter原理剖析及手動實現

xiasem發表於2018-07-30

簡介

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

涉及知識點

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

    本文的重點是對路由框架的實現進行介紹,所以對於元件化的基本知識在文中不會過多闡述,如有同學對元件化有不理解,可以參考網上眾多的部落格等介紹,然後再閱讀demo中的元件化配置進行熟悉,這裡附上github demo地址:ARouter原理剖析及手動實現,點我訪問原始碼,歡迎star
ARouter原理剖析及手動實現

    如上圖,在元件化中,為了業務邏輯的徹底解耦,同時也為了每個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,顯然非常麻煩),那麼有沒有一種更好的解決辦法呢?辦法當然是有的。下面看圖:
ARouter原理剖析及手動實現
    在元件化中,我們通常都會在base_module上層再依賴一個router_module,而這個router_module就是負責各個模組之間服務暴露和頁面跳轉的。
    用過ARouter路由框架的同學應該都知道,在每個需要對其他module提供呼叫的Activity中,都會宣告類似下面@Route註解,我們稱之為路由地址

@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中。為了讓大家理解,我這裡來使用近乎虛擬碼給大家簡單演示一下。

//專案編譯後通過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,那麼便只需呼叫如下:

//不同模組之間啟動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應該是下面這個寫法

// 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生成我們的路由表,首先第一步需要定義註解

@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模組下生成的類,其他模組如module1、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

相關文章