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

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

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

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

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

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

使用说明

基本使用

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

1@Route(path = "activity/main") 2public class MainActivity extends AppCompatActivity { 3 4 @Override 5 protected void onCreate(Bundle savedInstanceState) { 6 super.onCreate(savedInstanceState); 7 setContentView(R.layout.activity_main); 8 }

在要跳转的地方

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

如果想在多 moule 中使用

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

在主 moudle 中,

1@Modules({"app", "moudle1"}) 2@Module("app") 3public class RouterApplication extends Application { 4 5 @Override 6 protected void attachBaseContext(Context base) { 7 super.attachBaseContext(base); 8 Router.getInstance().init(); 9 } 10}

在 moudle1 中,

1@Route(path = "my/activity/main") 2@Module("moudle1") 3public class MainActivity extends AppCompatActivity { 4 5 @Override 6 protected void onCreate(Bundle savedInstanceState) { 7 super.onCreate(savedInstanceState); 8 setContentView(R.layout.activity_main_2); 9 } 10}

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

自定义注入 router

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

跳转的时候调用

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

结果回调

路由跳转结果回调。

1Router.getInstance().build("my/activity/main", new RouterCallback() { 2 @Override 3 public boolean beforeOpen(Context context, Uri uri) { 4 // 在打开路由之前 5 Log.i(TAG, "beforeOpen: uri=" + uri); 6 return false; 7 } 8 9 // 在打开路由之后(即打开路由成功之后会回调) 10 @Override 11 public void afterOpen(Context context, Uri uri) { 12 Log.i(TAG, "afterOpen: uri=" + uri); 13 14 } 15 16 // 没有找到改 uri 17 @Override 18 public void notFind(Context context, Uri uri) { 19 Log.i(TAG, "notFind: uri=" + uri); 20 21 } 22 23 // 发生错误 24 @Override 25 public void error(Context context, Uri uri, Throwable e) { 26 Log.i(TAG, "error: uri=" + uri + ";e=" + e); 27 } 28}).navigation(this);

startActivityForResult 跳转结果回调

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

原理说明

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

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

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

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

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

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

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

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

router-annotion

主要定义了三个注解

1@Target({ElementType.TYPE}) 2@Retention(RetentionPolicy.CLASS) 3public @interface Route { 4 String path(); 5}

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

1@Retention(RetentionPolicy.CLASS) 2public @interface Module { 3 String value(); 4} 5``` 6 7Route 注解主要是用来注明跳转的 path 的。 8 9Modules 注解,注明总共有多少个 moudle。 10 11Module 注解,注明当前 moudle 的名字。 12 13Modules,Module 注解主要是为了解决支持多 module 使用的。 14 15* * * 16 17router-compiler 18--------------- 19 20router-compiler 只有一个类 RouterProcessor,他的原理其实也是比较简单的,扫描那些类用到注解,并将这些信息存起来,做相应的处理。这里是会生成相应的 java 文件。 21 22主要包括以下两个步骤 23 241. 根据是否有 `@Modules` `@Module` 注解,然后生成相应的 `RouterInit` 文件 252. 扫描 `@Route` 注解,并根据 `moudleName` 生成相应的 java 文件 26 27### 注解基本介绍 28 29在讲解 RouterProcessor 之前,我们先来了解一下注解的基本知识。 30 31如果对于自定义注解还不熟悉的话,可以先看我之前写的这两篇文章。[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) 32 33``` 34public class RouterProcessor extends AbstractProcessor { 35 private static final boolean DEBUG = true; 36 private Messager messager; 37 private Filer mFiler; 38 39 @Override 40 public synchronized void init(ProcessingEnvironment processingEnv) { 41 super.init(processingEnv); 42 messager = processingEnv.getMessager(); 43 mFiler = processingEnv.getFiler(); 44 UtilManager.getMgr().init(processingEnv); 45 } 46 47 /** 48 * 定义你的注解处理器注册到哪些注解上 49 */ 50 @Override 51 public Set<String> getSupportedAnnotationTypes() { 52 Set<String> annotations = new LinkedHashSet<>(); 53 annotations.add(Route.class.getCanonicalName()); 54 annotations.add(Module.class.getCanonicalName()); 55 annotations.add(Modules.class.getCanonicalName()); 56 return annotations; 57 } 58 59 /** 60 * java版本 61 */ 62 @Override 63 public SourceVersion getSupportedSourceVersion() { 64 return SourceVersion.latestSupported(); 65 } 66 67} 68``` 69 70首先我们先来看一下 `getSupportedAnnotationTypes` 方法,这个方法返回的是我们支持扫描的注解。 71 72### 注解的处理 73 74接下来我们再一起来看一下 `process` 方法 75 76``` 77@Override 78public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { 79 // 注解为 null,直接返回 80 if (annotations == null || annotations.size() == 0) { 81 return false; 82 } 83 84 UtilManager.getMgr().getMessager().printMessage(Diagnostic.Kind.NOTE, "process"); 85 boolean hasModule = false; 86 boolean hasModules = false; 87 // module 88 String moduleName = "RouterMapping"; 89 Set<? extends Element> moduleList = roundEnv.getElementsAnnotatedWith(Module.class); 90 if (moduleList != null && moduleList.size() > 0) { 91 Module annotation = moduleList.iterator().next().getAnnotation(Module.class); 92 moduleName = moduleName + "_" + annotation.value(); 93 hasModule = true; 94 } 95 // modules 96 String[] moduleNames = null; 97 Set<? extends Element> modulesList = roundEnv.getElementsAnnotatedWith(Modules.class); 98 if (modulesList != null && modulesList.size() > 0) { 99 Element modules = modulesList.iterator().next(); 100 moduleNames = modules.getAnnotation(Modules.class).value(); 101 hasModules = true; 102 } 103 104 debug("generate modules RouterInit annotations=" + annotations + " roundEnv=" + roundEnv); 105 debug("generate modules RouterInit hasModules=" + hasModules + " hasModule=" + hasModule); 106 // RouterInit 107 if (hasModules) { // 有使用 @Modules 注解,生成 RouterInit 文件,适用于多个 moudle 108 debug("generate modules RouterInit"); 109 generateModulesRouterInit(moduleNames); 110 } else if (!hasModule) { // 没有使用 @Modules 注解,并且有使用 @Module,生成相应的 RouterInit 文件,使用与单个 moudle 111 debug("generate default RouterInit"); 112 generateDefaultRouterInit(); 113 } 114 115 // 扫描 Route 注解 116 Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class); 117 List<TargetInfo> targetInfos = new ArrayList<>(); 118 for (Element element : elements) { 119 System.out.println("elements =" + elements); 120 // 检查类型 121 if (!Utils.checkTypeValid(element)) continue; 122 TypeElement typeElement = (TypeElement) element; 123 Route route = typeElement.getAnnotation(Route.class); 124 targetInfos.add(new TargetInfo(typeElement, route.path())); 125 } 126 127 // 根据 module 名字生成相应的 java 文件 128 if (!targetInfos.isEmpty()) { 129 generateCode(targetInfos, moduleName); 130 } 131 return false; 132} 133``` 134 135,首先判断是否有注解需要处理,没有的话直接返回 `annotations == null || annotations.size() == 0`136 137接着我们会判断是否有 `@Modules` 注解(这种情况是多个 moudle 使用),有的话会调用 `generateModulesRouterInit(String[] moduleNames)` 方法生成 RouterInit java 文件,当没有 `@Modules` 注解,并且没有 `@Module` (这种情况是单个 moudle 使用),会生成默认的 RouterInit 文件。 138 139``` 140private void generateModulesRouterInit(String[] moduleNames) { 141 MethodSpec.Builder initMethod = MethodSpec.methodBuilder("init") 142 .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC); 143 for (String module : moduleNames) { 144 initMethod.addStatement("RouterMapping_" + module + ".map()"); 145 } 146 TypeSpec routerInit = TypeSpec.classBuilder("RouterInit") 147 .addModifiers(Modifier.PUBLIC, Modifier.FINAL) 148 .addMethod(initMethod.build()) 149 .build(); 150 try { 151 JavaFile.builder(Constants.ROUTE_CLASS_PACKAGE, routerInit) 152 .build() 153 .writeTo(mFiler); 154 } catch (Exception e) { 155 e.printStackTrace(); 156 } 157} 158``` 159 160假设说我们有"app","moudle1" 两个 moudle,那么我们最终生成的代码是这样的。 161 162``` 163public final class RouterInit { 164 public static final void init() { 165 RouterMapping_app.map(); 166 RouterMapping_moudle1.map(); 167 } 168} 169``` 170 171如果我们都没有使用 @Moudles 和 @Module 注解,那么生成的 RouterInit 文件大概是这样的。 172 173``` 174public final class RouterInit { 175 public static final void init() { 176 RouterMapping.map(); 177 } 178} 179``` 180 181这也就是为什么有 stub module 的原因。因为默认情况下,我们需要借助 RouterInit 去初始化 map。如果没有这两个文件,ide 编辑器 在 compile 的时候就会报错。 182 183``` 184compileOnly project(path: ':stub') 185``` 186 187![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/0a776ea88a91afd62a9c1d08f16b2234.jpeg) 188 189我们引入的方式是使用 compileOnly,这样的话再生成 jar 的时候,不会包括这两个文件,但是可以在 ide 编辑器中运行。这也是一个小技巧。 190 191### Route 注解的处理 192 193我们回过来看 process 方法连对 Route 注解的处理。 194 195``` 196// 扫描 Route 自己注解 197Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class); 198List<TargetInfo> targetInfos = new ArrayList<>(); 199for (Element element : elements) { 200 System.out.println("elements =" + elements); 201 // 检查类型 202 if (!Utils.checkTypeValid(element)) continue; 203 TypeElement typeElement = (TypeElement) element; 204 Route route = typeElement.getAnnotation(Route.class); 205 targetInfos.add(new TargetInfo(typeElement, route.path())); 206} 207 208// 根据 module 名字生成相应的 java 文件 209if (!targetInfos.isEmpty()) { 210 generateCode(targetInfos, moduleName); 211} 212``` 213 214首先会扫描所有的 Route 注解,并添加到 targetInfos list 当中,接着调用 `generateCode` 方法生成相应的文件。 215 216``` 217private void generateCode(List<TargetInfo> targetInfos, String moduleName) { 218 219 MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("map") 220// .addAnnotation(Override.class) 221 .addModifiers(Modifier.STATIC) 222 .addModifiers(Modifier.PUBLIC); 223 224// .addParameter(parameterSpec); 225 for (TargetInfo info : targetInfos) { 226 methodSpecBuilder.addStatement("com.xj.router.api.Router.getInstance().add($S, $T.class)", info.getRoute(), info.getTypeElement()); 227 } 228 229 TypeSpec typeSpec = TypeSpec.classBuilder(moduleName) 230// .addSuperinterface(ClassName.get(interfaceType)) 231 .addModifiers(Modifier.PUBLIC) 232 .addMethod(methodSpecBuilder.build()) 233 .addJavadoc("Generated by Router. Do not edit it!\n") 234 .build(); 235 try { 236 JavaFile.builder(Constants.ROUTE_CLASS_PACKAGE, typeSpec) 237 .build() 238 .writeTo(UtilManager.getMgr().getFiler()); 239 System.out.println("generateCode: =" + Constants.ROUTE_CLASS_PACKAGE + "." + Constants.ROUTE_CLASS_NAME); 240 } catch (Exception e) { 241 e.printStackTrace(); 242 System.out.println("generateCode:e =" + e); 243 } 244 245 } 246``` 247 248这个方法主要是使用 javapoet 生成 java 文件,关于 javaposet 的使用可以见[官网文档](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fsquare%2Fjavapoet),生成的 java 文件是这样的。 249 250``` 251package com.xj.router.impl; 252 253import com.xj.arounterdemo.MainActivity; 254import com.xj.arounterdemo.OneActivity; 255import com.xj.arounterdemo.TwoActivity; 256 257/** 258 * Generated by Router. Do not edit it! 259 */ 260public class RouterMapping_app { 261 public static void map() { 262 com.xj.router.api.Router.getInstance().add("activity/main", MainActivity.class); 263 com.xj.router.api.Router.getInstance().add("activity/one", OneActivity.class); 264 com.xj.router.api.Router.getInstance().add("activity/two", TwoActivity.class); 265 } 266} 267``` 268 269可以看到我们定义的注解信息,最终都会调用 `Router.getInstance().add()` 方法存放起来。 270 271router-api 272---------- 273 274这个 module 主要是多外暴露的 api,最主要的一个文件是 Router。 275 276``` 277public class Router { 278 279 private static final String TAG = "ARouter"; 280 281 private static final Router instance = new Router(); 282 283 private Map<String, Class<? extends Activity>> routeMap = new HashMap<>(); 284 private boolean loaded; 285 286 private Router() { 287 } 288 289 public static Router getInstance() { 290 return instance; 291 } 292 293 public void init() { 294 if (loaded) { 295 return; 296 } 297 RouterInit.init(); 298 loaded = true; 299 } 300} 301``` 302 303![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/04f1b4ffc7c6ba1276425e1a1524db36.gif) 304 305当我们想要初始化 Router 的时候,代用 init 方法即可。 init 方法会先判断是否初始化过,没有初始化过,会调用 RouterInit#init 方法区初始化。 306 307而在 RouterInit#init 中,会调用 RouterMap\_{@moduleName}#map 方法初始化,改方法又调用 `Router.getInstance().add()` 方法,从而完成初始化 308 309![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/e96a2626d8dd5df0bdf9475f31bbce6c.jpeg) 310 311### router 跳转回调 312 313``` 314public interface RouterCallback { 315 316 /** 317 * 在跳转 router 之前 318 * @param context 319 * @param uri 320 * @return 321 */ 322 boolean beforeOpen(Context context, Uri uri); 323 324 /** 325 * 在跳转 router 之后 326 * @param context 327 * @param uri 328 */ 329 void afterOpen(Context context, Uri uri); 330 331 /** 332 * 没有找到改 router 333 * @param context 334 * @param uri 335 */ 336 void notFind(Context context, Uri uri); 337 338 /** 339 * 跳转 router 错误 340 * @param context 341 * @param uri 342 * @param e 343 */ 344 void error(Context context, Uri uri, Throwable e); 345}

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

1if (isFind) { 2 afterOpen(context); 3}

}

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

1 2主要看 navigation 方法,在跳转 activity 的时候,首先会会调用 beforeOpen 方法回调 RouterCallback#beforeOpen。接着 catch exception 的时候,如果发生错误,会调用 errorOpen 方法回调 RouterCallback#errorOpen 方法。同时调用 tryToCallNotFind 方法判断是否是 ClassNotFoundException,是的话回调 RouterCallback#notFind。 3 4如果没有发生 eception,会回调 RouterCallback#afterOpen。 5 6### Activity 的 startActivityForResult 回调 7 8可以看到我们的 Router 也是支持 startActivityForResult 的 9

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

1 2它的实现原理其实很简单,是借助一个空白 fragment 实现的,原理的可以看我之前的这一篇文章。 3 4[Android Fragment 的妙用 - 优雅地申请权限和处理 onActivityResult](https://links.jianshu.com/go?to=https%3A%2F%2Fblog.csdn.net%2Fgdutxiaoxu%2Farticle%2Fdetails%2F86498647) 5 6* * * 7 8小结 9-- 10 11如果觉得效果不错的话,请到 github 上面 star, 谢谢。 [Router](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fgdutxiaoxu%2FRouter) 12 13我们的 Router 框架,流程大概是这样的。 14 15![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/a52862813cedb2a2eca1e0a5319f554b.jpeg) 16 17![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/724220357add34a05ac1f4082f41db84.jpeg) 18 19* * * 20 21题外话 22--- 23 24看了上面的文章,文章一开头提到的三个问题,你懂了吗,欢迎在评论区留言评论。 25 261. 注解的处理 272. 怎样解决多个 module 之间的依赖问题,以及如何支持多 module 使用 283. router 跳转及 activty startActivityForResult 的处理 29 30其实,现在很多 router 框架都借助 gradle 插件来实现。这样有一个好处,就是在多 moudle 使用的时候,我们只需要 `apply plugin` 就 ok,对外屏蔽了一些细节。但其实,他的原理跟我们上面的原理都是差不多的。 31
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )