@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)
public class RouterProcessor extends AbstractProcessor {
// key:组名 value:类名
private Map rootMap = new TreeMap<>();
// 分组 key:组名 value:对应组的路由信息
private Map> groupMap = new HashMap<>();
//…
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//…
elementUtils = processingEnvironment.getElementUtils();
typeUtils = processingEnvironment.getTypeUtils();
filerUtils = processingEnvironment.getFiler();
//参数是模块名 为了防止多模块/组件化开发的时候 生成相同的 xx?ROOT?文件
Map 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 !”);
}
@Override
public boolean process(Set set, RoundEnvironment roundEnvironment) {
if (!Utils.isEmpty(set)) {
//被Route注解的节点集合
Set rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
if (!Utils.isEmpty(rootElements)) {
processorRoute(rootElements);
}
return true;
}
return false;
}
//…
}
如代码中所示,要想在编译期对注解做处理,就需要RouterProcessor继承自AbstractProcessor并通过@AutoService注解进行注册,然后实现process()方法。process()方法里的set集合就是编译期扫描代码得到的加入了Route注解的文件集合,@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)指定了需要处理的注解的路径地址,在此就是Route.class的路径地址。init()方法会在注解处理器初始化的时候拿到一些工具类,然后看这句代码moduleName = options.get(Constant.ARGUMENTS_NAME),会得到一个moduleName,这个moduleName需要我们在要使用注解处理器生成路由映射文件的模块gradle文件里配置,而@SupportedOptions(Constant.ARGUMENTS_NAME)会拿到当前module的名字,用来生成唯一对应module下存放分组信息的类文件名。这里变量Constant.ARGUMENTS_NAME的值就是moduleName,在这之前,我们需要在每个组件module的gradle下配置如下
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
这里的@AutoService是为了注册注解处理器,需要我们引入一个google开源的自动注册工具AutoService,如下依赖(当然也可以手动进行注册,不过略微麻烦,这里不太推荐):
implementation ‘com.google.auto.service:auto-service:1.0-rc2’
第四步:通过javapoet生成java类:在第三步中process()方法里有一句代码:processorRoute(rootElements),processorRoute()方法里会调用generatedGroup()和generatedRoot()方法分别去生成分组信息相关和路由映射相关Java文件,关于路由映射信息和分组信息的关系,我们下面会讲到,这里先不用理会,你只需要知道它们都是生成的存有映射关系的文件,这里我只贴出generatedRoot()方法,因为生成类文件的原理都是一样的,至于生成什么功能的类,只要你会一个,举一反三,这便没有什么难度。
/**
- 生成Root类 作用:记录<分组,对应的Group类>
*/
private void generatedRoot(TypeElement iRouteRoot, TypeElement iRouteGroup) {
//创建参数类型 Map> 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> routes> routes
ParameterSpec parameter = ParameterSpec.builder(parameterizedTypeName, “routes”).build();
//生成函数 public void loadInfo(Map> routes> routes)
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constant.METHOD_LOAD_INTO)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(parameter);
//生成函数体
for (Map.Entry entry : rootMap.entrySet()) {
methodBuilder.addStatement(“routes.put($S, $T.class)”, entry.getKey(), ClassName.get(Constant.PACKAGE_OF_GENERATE_FILE, entry.getValue()));
}
//生成XX_Root_XX类
String className = Constant.NAME_OF_ROOT + moduleName;
TypeSpec typeSpec = TypeSpec.classBuilder(className)
.addSuperinterface(ClassName.get(iRouteRoot))
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build();
try {
//生成java文件,PACKAGE_OF_GENERATE_FILE就是生成文件需要的路径
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();
}
}
如上,我把每一块代码的作用注释了出来,相信大家很容易就能理解每一个代码段的作用。可见,其实生成文件只是调用一些api而已,只要我们熟知api的调用,生成java文件便没有什么难度。那么大家现在想一个问题,只要我以统一的规则生成所有的映射文件,然后拿到这些映射文件,是不是就可以很轻易的进行路由跳转了。好了,下一部分我们就来实现这个路由框架。
第二部分:动手实现一个路由框架
通过第一部分的讲述,我相信大家对于ARouter的原理已经有了整体轮廓的理解,这一部分,我便会通过代码带你去实现一个自己的路由框架。上节我们讲了如何生成路由映射文件,这节我们考虑下生成这些路由映射文件后,如何统一的去使用这些文件。
- 第一节:如何拿到和统一管理路由映射文件
通过第一部分的讲述我们知道在Activity类上加上@Route注解之后,便可通过apt来生成对应的路由映射文件,那么现在我们考虑一个问题,就是我们的路由映射文件是在编译期间生成的,那么在程序的运行期间我们要统一调用这些路由信息,便需要一个统一的调用方式。我们先来定义这个调用方式:
public interface IRouteGroup {
void loadInto(Map atlas);
}
public interface IRouteRoot {
void loadInto(Map> routes);
}
我们定义两个接口来对生成的java文件进行约束,IRouteGroup是生成的分组关系契约,IRouteRoot是单个分组下路由信息契约,只要我们生成的java文件分别继承自这两个接口并实现loadInto()方法,在运行期间我们就可以统一的调用生成的java文件,获取路由映射信息。在上文中我们时不时的提到分组信息和路由映射信息,其实两者都是通过javapoet生成的类文件,理论上我们只需要生成一种文件,即记录路由地址和Activity一一映射的文件,但是一个项目中众多的路由地址没有任何分组,会很难辨别,所以我们对路由进行分组,下面我用一张图来说明分组和路由的关系: 由图可知,xxx_Root_xxx就是记录着此module下的分组信息,而每一个分组会单独记录此分组下的所有路由映射关系。这样,只要我们拿到所有的分组文件,就能通过分组文件找到任一个分组,从而拿到所有的路由信息。
好了,生成映射文件我们上面已经讲过了,现在我们编译下项目就会在每个组件module的build/generated/source/apt目录下生成相关映射文件。这里我把app module编译后生成的文件贴出来,app module编译后会生成EaseRouter_Root_app文件和EaseRouter_Group_main、EEaseRouter_Group_show等文件,EaseRouter_Root_app文件对应于app module的分组,里面记录着本module下所有的分组信息,EaseRouter_Group_main、EaseRouter_Group_show文件分别记载着当前分组下的所有路由地址和ActivityClass映射信息。如下所示:
public class EaseRouter_Root_app implements IRouteRoot {
@Override
public void loadInto(Map> 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 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 atlas) {
atlas.put(“/show/info”,RouteMeta.build(RouteMeta.Type.ACTIVITY,ShowActivity.class,“/show/info”,“show”));
}
}
大家会看到生成的类分别实现了IRouteRoot和IRouteGroup接口,并且实现了loadInto()方法,而loadInto方法通过传入一个特定类型的map就能把分组信息放入map里,只要分组信息存入到特定的map里后,我们就可以随意的从map里取路由地址对应的Activity.class做跳转使用。那么如果我们在login_module中想启动app_module中的MainActivity类,首先,我们已知MainActivity类的路由地址是"/main/main",第一个"/main"代表分组名,那么我们岂不是可以轻易的通过path取到路由地址对应的Activity.class,然后将Activity.class传入intent实现跳转。
- 第二节 路由框架的初始化
上节我们已经通过apt生成了映射文件,并且知道了如何通过映射文件去调用Activity,然而我们要实现一个路由框架,就要考虑在合适的时机拿到这些映射文件中的信息,以供上层业务做跳转使用。拿到这些路由关系肯定越早越好,这里我们就在Application的onCreate方法中进行框架的初始化,调用EaseRoute.init(),初始化方法里我们去调用loadInfo()方法进行加载映射文件。下面看loadInfo()方法:
private static void loadInfo() throws PackageManager.NameNotFoundException, InterruptedException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//获得所有 apt生成的路由类的全类名 (路由表)
Set 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> stringClassEntry : Warehouse.groupsIndex.entrySet()) {
Log.d(TAG, "Root映射表[ " + stringClassEntry.getKey() + " : " + stringClassEntry.getValue() + “]”);
}
}
我们首先通过ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)得到apt生成的所有实现IRouteRoot接口的类文件集合,通过上面的讲解我们知道,拿到这些类文件便可以得到所有的路由地址和Activity映射关系。这个ClassUtils.getFileNameByPackageName()方法通过传入一个路径得到此路径下所有的类名,篇幅有限,这里我就不贴出来了,有兴趣的可以去看demo源码,拿到所有的类名之后,通过for循环拿到所有路由分组文件,并反射创建出实例,把这些路由分组存入Warehouse.groupsIndex里。这里Warehouse.groupsIndex就是一个map,是为了存放所有分组,Warehouse类里面还有一个Warehouse.routes的map,此map就是存放路由信息的表了。这里我们把每一个分组文件的分组信息都加入到Warehouse.groupsIndex里,就可以来实现路由跳转了。
- 第三节 路由跳转实现
经过上节的介绍,我们已经能够在进程初始化的时候拿到所有的路由信息,那么实现跳转便好做了。假如我们要在MainActivity中点击按钮跳转到其他module的activity,那么便只需要在点击按钮时调用如下方法:
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;
public Postcard(String path, String group) {
this(path, group, null);
}
public Bundle getExtras() {return mBundle;}
public int getEnterAnim() {return enterAnim;}
public int getExitAnim() {return exitAnim;}
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 withInt(@Nullable String key, int value) {
mBundle.putInt(key, value);
return this;
}
//还有许多给intent中bundle设置值得方法我就不一一列出来了,可以看demo里所有的细节
public Object navigation() {
return EasyRouter.getsInstance().navigation(null, this, -1, null);
}
}
如果你是一个Android开发,Postcard类里面的东西就不用我再给你介绍了吧!我们只介绍一个方法navigation(),方法里面会调用EasyRouter类的navigation()方法。EaseRouter的navigation()方法内部会调用prepareCard(postcard)找到路由地址对应的Activity.class。下面请看:
private void prepareCard(Postcard card) {
RouteMeta routeMeta = Warehouse.routes.get(card.getPath());
if (null == routeMeta) {
Class 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:
文末
当你打算跳槽的时候,应该把“跳槽成功后,我能学到什么东西?对我的未来发展有什么好处”放在第一位。这些东西才是真正引导你的关键。在跳槽之前尽量“物尽其用”,把手头上的工作做好,最好是完成了某个项目或是得到提升之后再走。跳槽不是目的,而是为了达到最终职业目标的手段
最后祝大家工作升职加薪,面试拿到心仪Offer
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
正引导你的关键。在跳槽之前尽量“物尽其用”,把手头上的工作做好,最好是完成了某个项目或是得到提升之后再走。跳槽不是目的,而是为了达到最终职业目标的手段**
最后祝大家工作升职加薪,面试拿到心仪Offer
[外链图片转存中…(img-30nPGJ6A-1715356466286)]
[外链图片转存中…(img-Y7r0PqmI-1715356466286)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!