面试官,怎样实现 Router 框架?
Android 开发中,组件化,模块化是一个老生常谈的问题。随着项目复杂性的增长,模块化是一个必然的趋势。除非你能忍受改一下代码,就需要六七分钟的漫长时间。
模块化,组件化随之带来的另外一个问题是页面的跳转问题,由于代码的隔离,代码之间有时候会无法互相访问。于是,路由(Router)框架诞生了。
目前用得比较多的有阿里的 ARouter,美团的 WMRouter,ActivityRouter 等。
今天,就让我们一起来看一下怎样实现一个路由框架。
实现的功能有。
- 基于编译时注解,使用方便
- 结果回调,每次跳转 Activity 都会回调跳转结果
- 除了可以使用注解自定义路由,还支持手动分配路由
- 支持多模块使用,支持组件化使用
使用说明
基本使用
第一步,在要跳转的 activity 上面注明 path,
@Route(path = "activity/main")
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}
在要跳转的地方
Router.getInstance().build("activity/main").navigation(this);
如果想在多 moule 中使用
第一步,使用 @Modules({"app", "sdk"})
注明总共有多少个 moudle,并分别在 moudle 中注明当前 moudle 的 名字,使用 @Module("")
注解。注意 @Modules({“app”, “sdk”}) 要与 @Module("") 一一对应。
在主 moudle 中,
@Modules({"app", "moudle1"})
@Module("app")
public class RouterApplication extends Application {@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base);Router.getInstance().init();}
}
在 moudle1 中,
@Route(path = "my/activity/main")
@Module("moudle1")
public class MainActivity extends AppCompatActivity {@Overrideprotected 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() {@Overridepublic boolean beforeOpen(Context context, Uri uri) { // 在打开路由之前Log.i(TAG, "beforeOpen: uri=" + uri);return false;}// 在打开路由之后(即打开路由成功之后会回调)@Overridepublic void afterOpen(Context context, Uri uri) {Log.i(TAG, "afterOpen: uri=" + uri);}// 没有找到改 uri@Overridepublic void notFind(Context context, Uri uri) {Log.i(TAG, "notFind: uri=" + uri);}// 发生错误@Overridepublic 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() {@Overridepublic void onActivityResult(int requestCode, int resultCode, Intent data) {Log.i(TAG, "onActivityResult: requestCode=" + requestCode + ";resultCode=" + resultCode + ";data=" + data);}
});
原理说明
实现一个 Router 框架,涉及到的主要的知识点如下:
- 注解的处理
- 怎样解决多个 module 之间的依赖问题,以及如何支持多 module 使用
- router 跳转及 activty startActivityForResult 的处理
我们带着这三个问题,一起来探索一下。
总共分为四个部分,router-annotion, router-compiler,router-api,stub
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 文件。
主要包括以下两个步骤
- 根据是否有
@Modules
@Module
注解,然后生成相应的RouterInit
文件 - 扫描
@Route
注解,并根据moudleName
生成相应的 java 文件
注解基本介绍
在讲解 RouterProcessor 之前,我们先来了解一下注解的基本知识。
如果对于自定义注解还不熟悉的话,可以先看我之前写的这两篇文章。Android 自定义编译时注解1 - 简单的例子,Android 编译时注解 —— 语法详解
public class RouterProcessor extends AbstractProcessor {private static final boolean DEBUG = true;private Messager messager;private Filer mFiler;@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);messager = processingEnv.getMessager();mFiler = processingEnv.getFiler();UtilManager.getMgr().init(processingEnv);}/*** 定义你的注解处理器注册到哪些注解上*/@Overridepublic 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版本*/@Overridepublic 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;// moduleString 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;}// modulesString[] 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);// RouterInitif (hasModules) { // 有使用 @Modules 注解,生成 RouterInit 文件,适用于多个 moudledebug("generate modules RouterInit");generateModulesRouterInit(moduleNames);} else if (!hasModule) { // 没有使用 @Modules 注解,并且有使用 @Module,生成相应的 RouterInit 文件,使用与单个 moudledebug("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')
我们引入的方式是使用 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 的使用可以见官网文档,生成的 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;}
}
当我们想要初始化 Router 的时候,代用 init 方法即可。 init 方法会先判断是否初始化过,没有初始化过,会调用 RouterInit#init 方法区初始化。
而在 RouterInit#init 中,会调用 RouterMap_{@moduleName}#map 方法初始化,改方法又调用 Router.getInstance().add()
方法,从而完成初始化
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() {@Overridepublic void onActivityResult(int requestCode, int resultCode, Intent data) {Log.i(TAG, "onActivityResult: requestCode=" + requestCode + ";resultCode=" + resultCode + ";data=" + data);}
});
它的实现原理其实很简单,是借助一个空白 fragment 实现的,原理的可以看我之前的这一篇文章。
Android Fragment 的妙用 - 优雅地申请权限和处理 onActivityResult
小结
如果觉得效果不错的话,请到 github 上面 star, 谢谢。 Router
我们的 Router 框架,流程大概是这样的。
题外话
看了上面的文章,文章一开头提到的三个问题,你懂了吗,欢迎在评论区留言评论。
- 注解的处理
- 怎样解决多个 module 之间的依赖问题,以及如何支持多 module 使用
- router 跳转及 activty startActivityForResult 的处理
其实,现在很多 router 框架都借助 gradle 插件来实现。这样有一个好处,就是在多 moudle 使用的时候,我们只需要 apply plugin
就 ok,对外屏蔽了一些细节。但其实,他的原理跟我们上面的原理都是差不多的。
接下来,我也会写 gradle plugin 相关的文章,并借助 gradle 实现 Router 框架。有兴趣的话可以关注我的微信公众号,徐公码字,谢谢。
相关文章
java Type 详解
java 反射机制详解
注解使用入门(一)
Android 自定义编译时注解1 - 简单的例子
Android 编译时注解 —— 语法详解
带你读懂 ButterKnife 的源码
Android Fragment 的妙用 - 优雅地申请权限和处理 onActivityResult
Android 点九图机制讲解及在聊天气泡中的应用
面试官,怎样实现 Router 框架?
扫一扫,欢迎关注我的微信公众号 stormjun94(徐公码字), 目前是一名程序员,不仅分享 Android开发相关知识,同时还分享技术人成长历程,包括个人总结,职场经验,面试经验等,希望能让你少走一点弯路。
面试官,怎样实现 Router 框架?相关推荐
- 面试官:讲讲Spring框架Bean的加载过程
spring作为目前我们开发的基础框架,每天的开发工作基本和他形影不离,作为管理bean的最经典.优秀的框架,它的复杂程度往往令人望而却步. 不过作为朝夕相处的框架,我们必须得明白一个问题就是spri ...
- 卧槽,面试官质疑我不会配置中心原理,看不起谁呢?
前言 一位读者朋友跟我反馈,能不能写一篇比较全的配置中心的文章.自己最近在面试过程中有被面试官问:如何设计一个配置中心? 这个话题,由于自己在工作中也没实际使用过配置中心,所以对于如何去设计是完全没有 ...
- 记录一次操蛋,恶心,草率的面试经历!(面试官总是说,这个很简单啊,那个很简单啊,卧槽,企业中从来不缺乏说大话,吹牛皮的人!)
记得是去年2019年9月份的时候,我去一家企业面试,在深圳南山区深大地铁站那边,对我而言,有点远,屁颠屁颠的坐地铁过去了,坐了将近一个小时的时间.面试时间不超过10分钟,对,没错,面试10分 ...
- vue插槽面试题_关于前端Vue框架的面试题,面试官可能会问到哪些。?
这年头,程序员面试都讲究坐姿,姿势不对,努力白费. 参照下图,请同学们对号入座. 回想一下,自己平时面试的坐姿,你在面试官眼里,大概是什么形象,可能是工程师,也可能是键盘侠,或者找麻烦的. 当然了,想 ...
- 面试官:连框架都没用熟练,就这还来面试?
我们都知道,目前技术框架五花八门,面试最常见的一个问题是,你为啥选用XXX中间件? 虽然绝大多数人遇到的真实情况是:框架是老大选的! 但是这样回答明显是不行的,我们要明白,面试官想听到的究竟是什么? ...
- Spring框架你敢写精通,面试官就敢问@Autowired注解的实现原理
面试官:Spring框架中的@Autowired注解可以标注在哪些地方? 小小白:@Autowired注解可以被标注在构造函数.属性.setter方法或配置方法上,用于实现依赖自动注入. 面试官:有没 ...
- “约见”面试官系列之常见面试题之第九十四篇之MVVM框架(建议收藏)
目录 一句话总结:vm层(视图模型层)通过接口从后台m层(model层)请求数据,vm层继而和v(view层)实现数据的双向绑定. 1.我大前端应该不应该做复杂的数据处理的工作? 2.mvc和mvvm ...
- 面试官都扯不过你系列之集合框架类总结
文章目录 前言 框架概述 说一说集合类有什么特点及与数组的比较 说一说集合类之间的主要关系 List.Map.Set 三个接口,存取元素时,各有什么特点? 集合类全息图 哪些集合类是线程安全的? Ja ...
- js中立即执行函数会预编译吗_面试官:聊聊对Vue.js框架的理解
作者:yacan8 https://github.com/yacan8/blog/issues/26 本文为一次前端技术分享的演讲稿,所以尽力不贴 Vue.js 的源码,因为贴代码在实际分享中,比较枯 ...
最新文章
- Android开源框架ImageLoader的完美例子
- Finding iPhone Memory Leaks: A “Leaks” Tool Tutorial[转]
- 惠普ilo管理界面远程安装系统
- M2: XAML Controls(2)
- C#_动态获取鼠标位置的颜色
- 优迈系统服务器初始化,优迈系统手机操作器服务器操作使用说明.pptx
- etlgr是什么服务器_ETL是指什么 - 金融行业 - ITPUB论坛-中国专业的IT技术社区
- 16.Java中的String详解
- 【软工项目组】第十八次会议
- 【自然框架】n级下拉列表框的原理
- 打印等腰三角形javascript
- 关于Android中设置闹钟的相对完善的解决方案
- Faster-RCNN.Pytorch的使用
- java gzip解压请求_使用 gzip 压缩请求正文
- ECharts数据可视化:入门、实战与进阶干货
- 面转栅格之ERROR 999999:执行函数时出错
- 联想昭阳E43L笔记本无线开关停掉解决方案
- vue/uniapp 百度统计埋点
- python3是什么意思啊_python3指的是什么意思
- 腾讯优图实验室贾佳亚:加入优图第一年 | 专访
热门文章
- 浅谈:网站到底要不要备案?
- 华为的「薪酬领袖」战略和薪酬的谈判原则
- 金三银四春招特供|高质量面试攻略
- IVD-Net:多模态UNet在MRI中的椎间盘定位和分割
- Jenkins+Gitlab+Nginx+Maven编译Java项目自动发布与基于tag版本回退(重复构建问题已解决)
- 解决Chrome不支持NPAPI插件的方法(贴吧旺旺等)
- 常用RGB颜色值对照表
- pip install warning
- centos上通过ll或者du命令查看文件的大小
- 尘埃落定性描写细节摘要_解释“细节”和“摘要”元素