给大家推荐个开源项目
Posted 玉刚说
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了给大家推荐个开源项目相关的知识,希望对你有一定的参考价值。
本文由
Xiasm
授权投稿
https://github.com/Xiasm/EasyRouter
前言
https://github.com/Xiasm/EasyRouter
第一部分:ARouter原理剖析
说到路由便不得不提一下android中的组件化开发思想,组件化是最近比较流行的架构设计方案,它能对代码进行高度的解耦、模块分离等,极大地提高开发效率(如有同学对组件化有不理解,可以参考网上众多的博客等介绍,然后再阅读demo源码中的组件化配置进行熟悉)。路由和组件化本身没有什么联系,因为路由的责任是负责页面跳转,但是组件化中两个单向依赖的module之间需要互相启动对方的Activity,因为没有相互引用,startActivity()是实现不了的,必须需要一个协定的通信方式,此时类似ARouter和ActivityRouter等的路由框架就派上用场了。
第一节:ARouter路由跳转的原理
@Route(path = "/main/main")
public class MainActivity extends AppCompatActivity {}
@Route(path = "/module1/module1main")
public class Module1MainActivity extends AppCompatActivity {}
public class MyRouters{
//项目编译后通过apt生成如下方法
public static HashMap<String, ClassBean> getRouteInfo(HashMap<String, ClassBean> routes) {
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 = MyRouters.getRouteInfo(new HashMap<String, ClassBean>);
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映射关系如何生成
技术当然是有的,那就是被众多框架使用的apt及javapoet技术,那么什么是apt,什么是javapoet呢?我们先来看下图:
由图可知,apt是在编译期对代码中指定的注解进行解析,然后做一些其他处理(如通过javapoet生成新的Java文件)。我们常用的ButterKnife,其原理就是通过注解处理器在编译期扫描代码中加入的@BindView、@OnClick等注解进行扫描处理,然后生成XXX_ViewBinding类,实现了view的绑定。javapoet是鼎鼎大名的squareup出品的一个开源库,是用来生成java文件的一个library,它提供了简便的api供你去生成一个java文件。可以如下引入javapoet
implementation 'com.squareup:javapoet:1.7.0'
下面我通过demo中的例子带你了解如何通过apt和javapoet技术生成路由映射关系的类文件:
首先第一步,定义注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
// 路由的路径
String path();
// 将路由节点进行分组,可以实现动态加载
String group() default "";
}
第二步,在Activity上使用注解:
@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 {}
第三步,编写注解处理器,在编译期找到加入注解的类文件,进行处理,这里我只展示关键代码,具体的细节还需要你去demo中仔细研读:
@AutoService(Processor.class)
// 处理器接收的参数
@SupportedOptions(Constant.ARGUMENTS_NAME)
// 注册给哪些注解的
@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<>();
//...
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//...
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 !");
}
@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;
}
//...
}
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<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()));
}
//生成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<String, RouteMeta> atlas);
}
public interface IRouteRoot {
void loadInto(Map<String, Class<? extends IRouteGroup>> routes);
}
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"));
}
}
第二节 路由框架的初始化
上节我们已经通过apt生成了映射文件,并且知道了如何通过映射文件去调用Activity,然而我们要实现一个路由框架,就要考虑在合适的时机拿到这些映射文件中的信息,以供上层业务做跳转使用。拿到这些路由关系肯定越早越好,这里我们就在Application的onCreate方法中进行框架的初始化,调用EaseRoute.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() + "]");
}
}
第三节 路由跳转实现
经过上节的介绍,我们已经能够在进程初始化的时候拿到所有的路由信息,那么实现跳转便好做了。假如我们要在MainActivity中点击按钮跳转到其他module的activity,那么便只需要在点击按钮时调用如下方法:
EasyRouter.getsInstance().build("/module1/module1main").navigation();
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);
}
}
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;
}
}
}
小结
到这我们的路由原理剖析及手写实现已经讲完了,由于篇幅限制,过多的细节不能一一给大家解释,如果大家读过文章之后还有什么不能融会贯通的地方,可star我的github阅读demo和更详细的讲解。EaseRouter本身只是参照ARouter手动实现的路由框架,并且剔除掉了很多东西,如过滤器等,如果想要用在项目里,建议还是用ARouter更好,毕竟这只是个练手项目,功能也不够全面,当然有同学想对demo扩展后使用那当然更好,遇到什么问题可以及时联系我。我的目的是通过自己手动实现路由框架来加深对知识的理解,如这里面涉及到的知识点apt、javapoet和组件化思路、编写框架的思路等。看到这里,如果感觉干货很多,欢迎关注我的github,里面会有更多干货!
Demo地址
仿ARouter一步步实现一个路由框架,点击 阅读原文 即可访问,欢迎star。
— — — END — — —
推荐阅读
以上是关于给大家推荐个开源项目的主要内容,如果未能解决你的问题,请参考以下文章