面试官:给你了十分钟,讲出实现 Router 框架的原理

浩浩 等级 812 0 0

Android 开发中,组件化,模块化是一个老生常谈的问题。随着项目复杂性的增长,模块化是一个必然的趋势。除非你能忍受改一下代码,就需要十几分钟的漫长阅读时间。

模块化,组件化随之带来的另外一个问题是页面的跳转问题,由于代码的隔离,代码之间有时候会无法互相访问。于是,路由(Router)框架诞生了。

目前用得比较多的有阿里的 ARouter,美团的 WMRouter,ActivityRouter 等。

今天,就让我们一起来看一下怎样实现一个路由框架。 实现的功能有。

  1. 基于编译时注解,使用方便
  2. 结果回调,每次跳转 Activity 都会回调跳转结果
  3. 除了可以使用注解自定义路由,还支持手动分配路由
  4. 支持多模块使用,支持组件化使用

使用说明

基本使用

第一步,在要跳转的 activity 上面注明 path,

@Route(path = "activity/main")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    } 

面试官:给你了十分钟,讲出实现 Router 框架的原理

在要跳转的地方

Router.getInstance().build("activity/main").navigation(this); 

面试官:给你了十分钟,讲出实现 Router 框架的原理

如果想在多 moule 中使用

第一步,使用 @Modules({"app", "sdk"}) 注明总共有多少个 moudle,并分别在 moudle 中注明当前 moudle 的 名字,使用 @Module("") 注解。注意 @Modules({"app", "sdk"}) 要与 @Module("") 一一对应。

在主 moudle 中,

@Modules({"app", "moudle1"})
@Module("app")
public class RouterApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        Router.getInstance().init();
    }
} 

在 moudle1 中,

@Route(path = "my/activity/main")
@Module("moudle1")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_2);
    }
} 

这样就可以支持多模块使用了。

自定义注入 router

Router.getInstance().add("activity/three", ThreeActivity.class); 

跳转的时候调用

Router.getInstance().build("activity/three").navigation(this); 

结果回调

路由跳转结果回调。

Router.getInstance().build("my/activity/main", new RouterCallback() {
    @Override
    public boolean beforeOpen(Context context, Uri uri) { 
    // 在打开路由之前
        Log.i(TAG, "beforeOpen: uri=" + uri);
        return false;
    }

   // 在打开路由之后(即打开路由成功之后会回调)
    @Override
    public void afterOpen(Context context, Uri uri) {
        Log.i(TAG, "afterOpen: uri=" + uri);

    }

    // 没有找到改 uri
    @Override
    public void notFind(Context context, Uri uri) {
        Log.i(TAG, "notFind: uri=" + uri);

    }

    // 发生错误
    @Override
    public void error(Context context, Uri uri, Throwable e) {
        Log.i(TAG, "error: uri=" + uri + ";e=" + e);
    }
}).navigation(this); 

startActivityForResult 跳转结果回调

Router.getInstance().build("activity/two").navigation(this, new Callback() {
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        Log.i(TAG, "onActivityResult: requestCode=" + requestCode + ";resultCode=" + resultCode + ";data=" + data);
    }
}); 

原理说明

实现一个 Router 框架,涉及到的主要的知识点如下:

  1. 注解的处理
  2. 怎样解决多个 module 之间的依赖问题,以及如何支持多 module 使用
  3. router 跳转及 activty startActivityForResult 的处理

我们带着这三个问题,一起来探索一下。

总共分为四个部分,router-annotion, router-compiler,router-api,stub

面试官:给你了十分钟,讲出实现 Router 框架的原理

router-annotion 主要是定义注解的,用来存放注解文件

router-compiler 主要是用来处理注解的,自动帮我们生成代码

router-api 是对外的 api,用来处理跳转的。

stub 这个是存放一些空的 java 文件,提前占坑。不会打包进 jar。

router-annotion

主要定义了三个注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    String path();
} 

@Retention(RetentionPolicy.CLASS) public @interface Modules { String[] value(); }

@Retention(RetentionPolicy.CLASS)
public @interface Module {
    String value();
} 
```

Route 注解主要是用来注明跳转的 path 的。

Modules 注解,注明总共有多少个 moudle。

Module 注解,注明当前 moudle 的名字。

Modules,Module 注解主要是为了解决支持多 module 使用的。

* * *

router-compiler
---------------

router-compiler 只有一个类 RouterProcessor,他的原理其实也是比较简单的,扫描那些类用到注解,并将这些信息存起来,做相应的处理。这里是会生成相应的 java 文件。

主要包括以下两个步骤

1.  根据是否有 `@Modules` `@Module` 注解,然后生成相应的 `RouterInit` 文件
2.  扫描 `@Route` 注解,并根据 `moudleName` 生成相应的 java 文件

### 注解基本介绍

在讲解 RouterProcessor 之前,我们先来了解一下注解的基本知识。

如果对于自定义注解还不熟悉的话,可以先看我之前写的这两篇文章。[Android 自定义编译时注解1 - 简单的例子](https://links.jianshu.com/go?to=https%3A%2F%2Fxujun.blog.csdn.net%2Farticle%2Fdetails%2F70244169),[Android 编译时注解 —— 语法详解](https://links.jianshu.com/go?to=https%3A%2F%2Fblog.csdn.net%2Fgdutxiaoxu%2Farticle%2Fdetails%2F70822023)

```
public class RouterProcessor extends AbstractProcessor {
    private static final boolean DEBUG = true;
    private Messager messager;
    private Filer mFiler;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        mFiler = processingEnv.getFiler();
        UtilManager.getMgr().init(processingEnv);
    }

    /**
     * 定义你的注解处理器注册到哪些注解上
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(Route.class.getCanonicalName());
        annotations.add(Module.class.getCanonicalName());
        annotations.add(Modules.class.getCanonicalName());
        return annotations;
    }

    /**
     * java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

} 
```

首先我们先来看一下 `getSupportedAnnotationTypes` 方法,这个方法返回的是我们支持扫描的注解。

### 注解的处理

接下来我们再一起来看一下 `process` 方法

```
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   // 注解为 null,直接返回
   if (annotations == null || annotations.size() == 0) {
       return false;
   }

   UtilManager.getMgr().getMessager().printMessage(Diagnostic.Kind.NOTE, "process");
   boolean hasModule = false;
   boolean hasModules = false;
   // module
   String moduleName = "RouterMapping";
   Set<? extends Element> moduleList = roundEnv.getElementsAnnotatedWith(Module.class);
   if (moduleList != null && moduleList.size() > 0) {
       Module annotation = moduleList.iterator().next().getAnnotation(Module.class);
       moduleName = moduleName + "_" + annotation.value();
       hasModule = true;
   }
   // modules
   String[] moduleNames = null;
   Set<? extends Element> modulesList = roundEnv.getElementsAnnotatedWith(Modules.class);
   if (modulesList != null && modulesList.size() > 0) {
       Element modules = modulesList.iterator().next();
       moduleNames = modules.getAnnotation(Modules.class).value();
       hasModules = true;
   }

   debug("generate modules RouterInit annotations=" + annotations + " roundEnv=" + roundEnv);
   debug("generate modules RouterInit hasModules=" + hasModules + " hasModule=" + hasModule);
   // RouterInit
   if (hasModules) { // 有使用 @Modules 注解,生成 RouterInit 文件,适用于多个 moudle
       debug("generate modules RouterInit");
       generateModulesRouterInit(moduleNames);
   } else if (!hasModule) { // 没有使用 @Modules 注解,并且有使用 @Module,生成相应的 RouterInit 文件,使用与单个 moudle
       debug("generate default RouterInit");
       generateDefaultRouterInit();
   }

   // 扫描 Route 注解
   Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class);
   List<TargetInfo> targetInfos = new ArrayList<>();
   for (Element element : elements) {
       System.out.println("elements =" + elements);
       // 检查类型
       if (!Utils.checkTypeValid(element)) continue;
       TypeElement typeElement = (TypeElement) element;
       Route route = typeElement.getAnnotation(Route.class);
       targetInfos.add(new TargetInfo(typeElement, route.path()));
   }

   // 根据 module 名字生成相应的 java 文件
   if (!targetInfos.isEmpty()) {
       generateCode(targetInfos, moduleName);
   }
   return false;
} 
```

,首先判断是否有注解需要处理,没有的话直接返回 `annotations == null || annotations.size() == 0` 。

接着我们会判断是否有 `@Modules` 注解(这种情况是多个 moudle 使用),有的话会调用 `generateModulesRouterInit(String[] moduleNames)` 方法生成 RouterInit java 文件,当没有 `@Modules` 注解,并且没有 `@Module` (这种情况是单个 moudle 使用),会生成默认的 RouterInit 文件。

```
private void generateModulesRouterInit(String[] moduleNames) {
   MethodSpec.Builder initMethod = MethodSpec.methodBuilder("init")
           .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
   for (String module : moduleNames) {
       initMethod.addStatement("RouterMapping_" + module + ".map()");
   }
   TypeSpec routerInit = TypeSpec.classBuilder("RouterInit")
           .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
           .addMethod(initMethod.build())
           .build();
   try {
       JavaFile.builder(Constants.ROUTE_CLASS_PACKAGE, routerInit)
               .build()
               .writeTo(mFiler);
   } catch (Exception e) {
       e.printStackTrace();
   }
} 
```

假设说我们有"app","moudle1" 两个 moudle,那么我们最终生成的代码是这样的。

```
public final class RouterInit {
  public static final void init() {
    RouterMapping_app.map();
    RouterMapping_moudle1.map();
  }
} 
```

如果我们都没有使用 @Moudles 和 @Module 注解,那么生成的 RouterInit 文件大概是这样的。

```
public final class RouterInit {
  public static final void init() {
    RouterMapping.map();
  }
} 
```

这也就是为什么有 stub module 的原因。因为默认情况下,我们需要借助 RouterInit 去初始化 map。如果没有这两个文件,ide 编辑器 在 compile 的时候就会报错。

```
compileOnly project(path: ':stub') 
```

![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/0a776ea88a91afd62a9c1d08f16b2234.jpeg)

我们引入的方式是使用 compileOnly,这样的话再生成 jar 的时候,不会包括这两个文件,但是可以在 ide 编辑器中运行。这也是一个小技巧。

### Route 注解的处理

我们回过来看 process 方法连对 Route 注解的处理。

```
// 扫描 Route 自己注解
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class);
List<TargetInfo> targetInfos = new ArrayList<>();
for (Element element : elements) {
    System.out.println("elements =" + elements);
    // 检查类型
    if (!Utils.checkTypeValid(element)) continue;
    TypeElement typeElement = (TypeElement) element;
    Route route = typeElement.getAnnotation(Route.class);
    targetInfos.add(new TargetInfo(typeElement, route.path()));
}

// 根据 module 名字生成相应的 java 文件
if (!targetInfos.isEmpty()) {
    generateCode(targetInfos, moduleName);
} 
```

首先会扫描所有的 Route 注解,并添加到 targetInfos list 当中,接着调用 `generateCode` 方法生成相应的文件。

```
private void generateCode(List<TargetInfo> targetInfos, String moduleName) {

        MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("map")
//                .addAnnotation(Override.class)
                .addModifiers(Modifier.STATIC)
                .addModifiers(Modifier.PUBLIC);

//                .addParameter(parameterSpec);
        for (TargetInfo info : targetInfos) {
            methodSpecBuilder.addStatement("com.xj.router.api.Router.getInstance().add($S, $T.class)", info.getRoute(), info.getTypeElement());
        }

        TypeSpec typeSpec = TypeSpec.classBuilder(moduleName)
//                .addSuperinterface(ClassName.get(interfaceType))
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodSpecBuilder.build())
                .addJavadoc("Generated by Router. Do not edit it!\n")
                .build();
        try {
            JavaFile.builder(Constants.ROUTE_CLASS_PACKAGE, typeSpec)
                    .build()
                    .writeTo(UtilManager.getMgr().getFiler());
            System.out.println("generateCode: =" + Constants.ROUTE_CLASS_PACKAGE + "." + Constants.ROUTE_CLASS_NAME);
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("generateCode:e  =" + e);
        }

    } 
```

这个方法主要是使用 javapoet 生成 java 文件,关于 javaposet 的使用可以见[官网文档](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fsquare%2Fjavapoet),生成的 java 文件是这样的。

```
package com.xj.router.impl;

import com.xj.arounterdemo.MainActivity;
import com.xj.arounterdemo.OneActivity;
import com.xj.arounterdemo.TwoActivity;

/**
 * Generated by Router. Do not edit it!
 */
public class RouterMapping_app {
  public static void map() {
    com.xj.router.api.Router.getInstance().add("activity/main", MainActivity.class);
    com.xj.router.api.Router.getInstance().add("activity/one", OneActivity.class);
    com.xj.router.api.Router.getInstance().add("activity/two", TwoActivity.class);
  }
} 
```

可以看到我们定义的注解信息,最终都会调用 `Router.getInstance().add()` 方法存放起来。

router-api
----------

这个 module 主要是多外暴露的 api,最主要的一个文件是 Router。

```
public class Router {

    private static final String TAG = "ARouter";

    private static final Router instance = new Router();

    private Map<String, Class<? extends Activity>> routeMap = new HashMap<>();
    private boolean loaded;

    private Router() {
    }

    public static Router getInstance() {
        return instance;
    }

    public void init() {
        if (loaded) {
            return;
        }
        RouterInit.init();
        loaded = true;
    }
} 
```

![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/04f1b4ffc7c6ba1276425e1a1524db36.gif)

当我们想要初始化 Router 的时候,代用 init 方法即可。 init 方法会先判断是否初始化过,没有初始化过,会调用 RouterInit#init 方法区初始化。

而在 RouterInit#init 中,会调用 RouterMap\_{@moduleName}#map 方法初始化,改方法又调用 `Router.getInstance().add()` 方法,从而完成初始化

![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/e96a2626d8dd5df0bdf9475f31bbce6c.jpeg)

### router 跳转回调

```
public interface RouterCallback {

    /**
     * 在跳转 router 之前
     * @param context
     * @param uri
     * @return
     */
    boolean beforeOpen(Context context, Uri uri);

    /**
     * 在跳转 router 之后
     * @param context
     * @param uri
     */
    void afterOpen(Context context, Uri uri);

    /**
     * 没有找到改 router
     * @param context
     * @param uri
     */
    void notFind(Context context, Uri uri);

    /**
     * 跳转 router 错误
     * @param context
     * @param uri
     * @param e
     */
    void error(Context context, Uri uri, Throwable e);
} 

public void navigation(Activity context, int requestCode, Callback callback) { beforeOpen(context); boolean isFind = false; try { Activity activity = (Activity) context; Intent intent = new Intent(); intent.setComponent(new ComponentName(context.getPackageName(), mActivityName)); intent.putExtras(mBundle); getFragment(activity) .setCallback(callback) .startActivityForResult(intent, requestCode); isFind = true; } catch (Exception e) { errorOpen(context, e); tryToCallNotFind(e, context); }

if (isFind) {
    afterOpen(context);
}

}

private void tryToCallNotFind(Exception e, Context context) { if (e instanceof ClassNotFoundException && mRouterCallback != null) { mRouterCallback.notFind(context, mUri); } }


主要看 navigation 方法,在跳转 activity 的时候,首先会会调用 beforeOpen 方法回调 RouterCallback#beforeOpen。接着 catch exception 的时候,如果发生错误,会调用 errorOpen 方法回调 RouterCallback#errorOpen 方法。同时调用 tryToCallNotFind 方法判断是否是 ClassNotFoundException,是的话回调 RouterCallback#notFind。

如果没有发生 eception,会回调 RouterCallback#afterOpen。

### Activity 的 startActivityForResult 回调

可以看到我们的 Router 也是支持 startActivityForResult 的

Router.getInstance().build("activity/two").navigation(this, new Callback() { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.i(TAG, "onActivityResult: requestCode=" + requestCode + ";resultCode=" + resultCode + ";data=" + data); } });


它的实现原理其实很简单,是借助一个空白 fragment 实现的,原理的可以看我之前的这一篇文章。

[Android Fragment 的妙用 - 优雅地申请权限和处理 onActivityResult](https://links.jianshu.com/go?to=https%3A%2F%2Fblog.csdn.net%2Fgdutxiaoxu%2Farticle%2Fdetails%2F86498647)

* * *

小结
--

如果觉得效果不错的话,请到 github 上面 star, 谢谢。 [Router](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fgdutxiaoxu%2FRouter)

我们的 Router 框架,流程大概是这样的。

![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/a52862813cedb2a2eca1e0a5319f554b.jpeg)

![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/724220357add34a05ac1f4082f41db84.jpeg)

* * *

题外话
---

看了上面的文章,文章一开头提到的三个问题,你懂了吗,欢迎在评论区留言评论。

1.  注解的处理
2.  怎样解决多个 module 之间的依赖问题,以及如何支持多 module 使用
3.  router 跳转及 activty startActivityForResult 的处理

其实,现在很多 router 框架都借助 gradle 插件来实现。这样有一个好处,就是在多 moudle 使用的时候,我们只需要 `apply plugin` 就 ok,对外屏蔽了一些细节。但其实,他的原理跟我们上面的原理都是差不多的。
收藏
评论区

相关推荐

Vue | 路由守卫面试常考
前言 最近在整理基础,欢迎掘友们一起交流学习,关注公众号<a href"https://z3.ax1x.com/2021/03/27/6zmqSI.jpg"前端自学社区</a <br/ 结尾有彩蛋哦! 🎉🎉🎉 Vue Router 路由守卫 导图目录 1. 路由守卫分类 2. 全局路由守卫 3. 单个路由守卫
2021前端技术面试必备Vue:(三)Router篇
<section id"nice" datatool"mdnice编辑器" datawebsite"https://www.mdnice.com" style"padding: 0 10px; wordspacing: 0px; wordwrap: breakword; textalign: left; fontfamily: Opti
面向初学者的 React 路由-React Router的完整指南!
因此,您正在尝试学习React.js。也许您甚至已经在其中构建了几个简单的项目。无论您是新开发人员还是有一定经验的人,都可能会发现自己必须开发具有不同页面和路线的Web应用程序。那就是React Router发挥作用的时候。什么是React Router?React Router提供了一种在React或React Native应用程序中实现路由的简便方法。入
SOFA 源码分析— 自定义路由寻址
![](https://oscimg.oschina.net/oscnet/a53072c91815c9369fa3822fec65defbc2b.png) 前言 -- SOFA-RPC 中对服务地址的选择也抽象为了一条处理链,由每一个 Router 进行处理。同 Filter 一样, SOFA-RPC 对 Router 提供了同样的扩展能力。 那么就
0718日志
c端线上地址 http://x.diandanme.com/fe/?d=183#/ 什么时候来需求,我做好准备了吗? router取值问题  --host 192.168.33.121 git下拉覆盖本地文件 $ git fetch --all $ git reset origin/master  $ git pull git
219. 单页应用 会话管理(session、cookie、jwt)
> 原文链接:[https://github.com/ly525/blog/issues/219](https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fgithub.com%2Fly525%2Fblog%2Fissues%2F219) 关键字:`http-only`, `cookie`,`se
219. 单页应用 会话管理(session、cookie、jwt)
> 原文链接:[https://github.com/ly525/blog/issues/219](https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fgithub.com%2Fly525%2Fblog%2Fissues%2F219) 关键字:`http-only`, `cookie`,`se
Flutter企业级路由跳转框架fluro的使用(一)
久违了。 记录 fluro 路由框架的使用。 ### 导入依赖   fluro: ^1.6.3 ### 组件封装 #### routers.dart 这个文件封装了一个路由器,定义了配置方法,封装了有返回值,和无返回值的路由跳转方法。 //封装一个Routes 类class Routes {  //定义Router 对象  st
HTTP、HTTPS常用的默认端口号
端口号标识了一个主机上进行通信的不同的应用程序。 1.HTTP协议代理服务器常用端口号:80/8080/3128/8081/9098 2.SOCKS代理协议服务器常用端口号:1080 3.FTP(文件传输)协议代理服务器常用端口号:21 4.Telnet(远程登录)协议代理服务器常用端口号:23 HTTP服务器,默认端口号为80/tcp(木马Exe
React Router 4.x 开发,这些雷区我们都帮你踩过了
前言 -- 在前端框架层出不穷的今天,React 以其虚拟 DOM 、组件化开发思想等特性迅速占据了主流位置,成为前端开发工程师热衷的 Javascript 库。作为 React 体系中的重要组成部分:React Router 也成为开发者首选的路由库,其主要功能是通过管理 url 实现组件的切换和状态的变化。 正文 -- 在 React Router
React:react
使用react构建单页面应用:   实现方法:(1)react-router        (2)react-router-dom `react-router`: 实现了路由的核心功能,而react-router-dom依赖react-router, `react-router-dom`: 基于`react-router`,加入了在浏览器运行环境下的
Vue Router 4.0 正式发布!焕然一新。
今天,Vue Router 4 正式发布稳定版本。 在经历了 14 个 Alpha,13 个 Beta 和 6 个 RC 版本之后,Vue Router v4 闪亮登场,为你带来了 TypeScript 集成、新功能以及对现代应用程序的一致性改进,已经准备好成为 Vue3 新应用的最佳伴侣。 将近 2 年的时间,大约 1500 次提交,15 个RFC\[
springboot+vue 登录页面(一)
首先了解的技术点是: 后台:springboot,mybatis,mybatis逆向工程,mysql,日志 前端: nodejs,npm ,cnpm,vue,vue-cli,webpack,element ui,router,axios 开发工具:idea,webstorm 该项目前端使用的是vue ,目的是实现前后端分离 后台: 1.选择spr
vue 项目部署后 刷新一下 页面找不到 解决
1.修改配置router的参数  (效果不好) ![](https://oscimg.oschina.net/oscnet/dff6f524904c9e7851f22a532f975ce41ef.png) 2\. (不能解决 出现403) 后端配置例子 Apache <IfModule mod_rewrite
vue
**Vue-router 报NavigationDuplicated的可能解决方案** 参考文章: [(1)Vue-router 报NavigationDuplicated的可能解决方案](https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fwww.codeprj.com%2Fblog%2F