路由器的作用是什么?通俗的讲,路由器的作用就是一根网线满足多人上网的需求。而在开发中路由器模块的作用就是实现中转分发,也就是说将原来有关系的模块(有依赖的模块分开),产生一个中间的模块,让原来依赖的两个模块都去和路由模块交互,从而将原来两个有关系的模块拆分开,利如我现在在开发一个app,根据业务需求这里需要开发一个便民中心模块,但是进入这个模块肯定得先从主模块点击进去,这里如果有数据之间的传递的话,就产生了交互。而交互产生了的话,就会产生依赖,此时便民中心模块依赖主模块,而此时如果我们搭建路由器模块的话,那么主模块就和便民中心模块解耦了,主模块通过路由器模块将数据分发给便民中心模块,而不像以前我们在android端跳转的转的话直接通过Intent跳转,传值也通过Intent传,这里有个弊端就如定义传值的key值,在接受这个key随确定value值得时候,你必须得知道这个key是什么,后期维护你需要一个一个Activity的去找,跳转到那个Activity需要传什么值,甚是头疼,那么路由器的另一个优点来了,我们从一个模块跳到另一个模块需要传的值的key完全可以通过注解标记出来,一目了然,而不用那么费力的找了,而且具体怎么传的完全隔离出去,用户完全不用考虑实现的细节。
通俗一点的讲,这里我们先不讨论模块之间的隔离,只简单的讨论一下从Activity(A)跳转到Activity(B)的场景,首先我们第一点得确定是从哪一个Activity跳到哪一个Activity,最初的跳转是你要么直接显示跳转、要么隐式跳转,但是不管怎么跳转都需要知道具体是那个Activity(显示)、隐式(知道action什么的等),而采用路由器模式的时候,你完全不用关心他的Activity的具体名字是什么,或者他的action等是什么,你只需要给它造一个匹配规则,让路由器自己找到你想传值和跳转的Activity到底是哪一个,就好比我定义一个IP地址,这个Ip地址就是指向B(Activity)的,那么A(Activity)通过将ip地址传给路由器然后路由器帮你分析你想跳转到哪一个Activity中,最终锁定到B,,日后你想修改跳转规则或传值是怎么传的,只要查看路由表就好了,这就是路由模块的好处了。
如果你想自己实现一个路由框架的话,得做哪些准备呢?首先你得知道路由器到底是干嘛使的,通过上面的分析,你大体应该知道路由器在Android端的作用了,那么我们首先需要做的就是将所有的模块的主Activity制定他们匹配的ip,也就是说为A模块主(Activity)标记一个唯一的ip地址,也就是加个域名,例如A对应http://feiyu/,B模块对应http://zp/
然后将对应关系保存到缓存中,保存到缓存中后,我们通过路由器调用之后,路由器从路由表中取出对应关系,但是路由表必须知道这个关系的规则是什么,他需要根据规则将路由表中数据解析出来,然后实现跳转,那么在写路由器模块的时候我们必须为路由器写上解析器模块,那么这里我们已经想到要用至少到两个设计模式了,单利模式、解释器模式(专门用来解析规则例如正则表达式就是用的这种设计模式)。
如果我们要保存映射关系,是否将它固定,也就是说每写一个模块的话,将它手动添加到路由表中,很显然这是不科学的,因为我们写的路由器模块是给其他小伙伴用的,他不一定愿意看你的代码,那么怎么样让他写的模块映射出路由表里的数据,这里就用到了注解,我们在路由器模块中写出注解规则,和你合作的小伙伴只要按照你的注解规则,为他的模块主activity标记上注解,我们就能动态的将注解解析出来,然后将它放到路由表中。这样路由器解析的时候就会找到需要跳转的模块。
但是这里又有一个问题,该采用运行时注解还是编译器时注解呢,运行时注解,就是你动态通过反射将注解解析出来放在集合中,但是那需要在运行时解析,比较耗费点时间,那么采用编译器注解呢,就是说在编译的时候先检查有没有编译注解,如果有的话先通过注解生成java文件,然后才将新生成的java文件和你写的项目java文件一起编译成class文件,但是这么做的话会多出java文件,从而使apk包增大,还有可能遇到android的65536的限制,但是一点不影响运行时的速度,综合考虑还是采用编译时注解(apt技术)。
看了这么多,是不是有点累了,先欣赏下美女休息一下
好了,进入正题,这里我们来一起分析一下ActivityRouter源码,参观一下别人是怎么实现的,ActivityRouter源码 至于为什么要通过这个框架分析,因为它虽然有点缺点,但是它小巧并且已经能将路由框架的原理思想大体的表现出来。
前面提到编译时期的注解,那么先从这个框架的apt部分开始说起,如果想在android studio中实现apt功能只需要在app下build.gradle配置文件中加入
compile 'com.google.auto.service:auto-service:1.0-rc3'
然后在你的编译处理类上加入这个注解
@AutoService(Processor.class)
public class RouterProcessor extends AbstractProcessor
先来看下一下实现这个编译处理类需要实现哪些方法:
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
filer = processingEnv.getFiler();
}
具体的其它辅助类,小伙伴们请查阅相关文档。
public Set getSupportedAnnotationTypes() {
Set ret = new HashSet<>();
ret.add(Modules.class.getCanonicalName());
ret.add(Module.class.getCanonicalName());
ret.add(Router.class.getCanonicalName());
return ret;
}
这个方法用来告诉注解处理器那些注解需要处理。这里Module、Modules和Router处理注解需要处理,也就是说这个框架只声明了这
三种注解
@Retention(RetentionPolicy.CLASS)
public @interface Module {
String value();
}
@Retention(RetentionPolicy.CLASS)
public @interface Modules {
String[] value();
}
@Retention(RetentionPolicy.CLASS)这个注解用来标记是在编译时的注解,没有标记要注解的类型的话,默认为类注解
Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
public @interface Router {
String[] value();
String[] stringParams() default "";
String[] intParams() default "";
String[] longParams() default "";
String[] booleanParams() default "";
String[] shortParams() default "";
String[] floatParams() default "";
String[] doubleParams() default "";
String[] byteParams() default "";
String[] charParams() default "";
String[] transfer() default "";
}
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
最重要的是实现下面这个方法,只要捕捉到你设置的注解最终就会回调这个方法供你生成java文件,如下:
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
debug("process apt with " + annotations.toString());
if (annotations.isEmpty()) {
return false;
}
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;
}
// RouterInit
if (hasModules) {
debug("generate modules RouterInit");
generateModulesRouterInit(moduleNames);
} else if (!hasModule) {
debug("generate default RouterInit");
generateDefaultRouterInit();
}
// RouterMapping
return handleRouter(moduleName, roundEnv);
}
ackage com.example; // PackageElement
public class Foo { // TypeElement 类型元素
private int a; // VariableElement代表成员变量
private Foo other; // VariableElement
public Foo () {} // ExecuteableElement 匹配方法元素
public void setA ( // ExecuteableElement
int newA // TypeElement 参数也代表TypeElement
) {}
}
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("com.github.mzule.activityrouter.router", routerInit)
.build()
.writeTo(filer);
} catch (Exception e) {
e.printStackTrace();
}
}
这个方法用了javaPoet来生成java文件,javaPoet是啥?javaPoet是JakeWharton大神编写的用于辅助生成java文件的框架。
javaPoet
这个方法的意思就是创建包名为com.github.mzule.activityrouter.router的类,并在这个类中创建静态init方法,并在init方法中调用
RouterMapping_module(注解module的名字被添加注解的activity)类的map方法,当然RouterMapping_module类也是动态生成的,来看一下
它生成的代码
private boolean handleRouter(String genClassName, RoundEnvironment roundEnv) {
Set extends Element> elements = roundEnv.getElementsAnnotatedWith(Router.class);
MethodSpec.Builder mapMethod = MethodSpec.methodBuilder("map")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC)
.addStatement("java.util.Map transfer = null")
.addStatement("com.github.mzule.activityrouter.router.ExtraTypes extraTypes")
.addCode("\n")
for (Element element : elements) {
Router router = element.getAnnotation(Router.class);
String[] transfer = router.transfer();
if (transfer.length > 0 && !"".equals(transfer[0])) {
mapMethod.addStatement("transfer = new java.util.HashMap()");
for (String s : transfer) {
String[] components = s.split("=>");
if (components.length != 2) {
error("transfer `" + s + "` not match a=>b format");
break;
}
mapMethod.addStatement("transfer.put($S, $S)", components[0], components[1]);
}
} else {
mapMethod.addStatement("transfer = null");
}
mapMethod.addStatement("extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes()");
mapMethod.addStatement("extraTypes.setTransfer(transfer)");
addStatement(mapMethod, int.class, router.intParams());
addStatement(mapMethod, long.class, router.longParams());
addStatement(mapMethod, boolean.class, router.booleanParams());
addStatement(mapMethod, short.class, router.shortParams());
addStatement(mapMethod, float.class, router.floatParams());
addStatement(mapMethod, double.class, router.doubleParams());
addStatement(mapMethod, byte.class, router.byteParams());
addStatement(mapMethod, char.class, router.charParams());
for (String format : router.value()) {
ClassName className;
Name methodName = null;
if (element.getKind() == ElementKind.CLASS) {
className = ClassName.get((TypeElement) element);
} else if (element.getKind() == ElementKind.METHOD) {
className = ClassName.get((TypeElement) element.getEnclosingElement());
methodName = element.getSimpleName();
} else {
throw new IllegalArgumentException("unknow type");
}
if (format.startsWith("/")) {
error("Router#value can not start with '/'. at [" + className + "]@Router(\"" + format + "\")");
return false;
}
if (format.endsWith("/")) {
error("Router#value can not end with '/'. at [" + className + "]@Router(\"" + format + "\")");
return false;
}
if (element.getKind() == ElementKind.CLASS) {
mapMethod.addStatement("com.github.mzule.activityrouter.router.Routers.map($S, $T.class, null, extraTypes)", format, className);
} else {
mapMethod.addStatement("com.github.mzule.activityrouter.router.Routers.map($S, null, " +
"new MethodInvoker() {\n" +
" public void invoke(android.content.Context context, android.os.Bundle bundle) {\n" +
" $T.$N(context, bundle);\n" +
" }\n" +
"}, " +
"extraTypes)", format, className, methodName);
}
}
mapMethod.addCode("\n");
}
TypeSpec routerMapping = TypeSpec.classBuilder(genClassName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(mapMethod.build())
.build();
try {
JavaFile.builder("com.github.mzule.activityrouter.router", routerMapping)
.build()
.writeTo(filer);
} catch (Throwable e) {
e.printStackTrace();
}
return true;
}
java.util.Map transfer = null
添加转化的对象(顾名思义就是将一个名字转化为另一个名字),介绍完生成java的对象的时候会详细讨论
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes()
创建传值类型的类,用来标记所传的值是什么类型的
mapMethod.addStatement("com.github.mzule.activityrouter.router.Routers.map($S, $T.class, null, extraTypes)", format, className);
调用Routers类的map方法,将映射存到路由表中
讨论到这里,大体的可以看出这个框架利用apt技术动态的生成两个java类,这两个java类的主要作用就是将注解交给路由器的
解析类解析映射关系,最终将,映射关系存到路由器缓存中。只要在app启动时调用这两个java类,就可以将Activity和ip的映射
关系保存到路由器表中,在路由器中转中,就可以找到合适的模块的主Activity进行分发跳转了。
现在来简单的运用这个框架,如果想把某个模块的主Activity加入到路由表中,直接在这个Activity上添加这个注解:
@Router("user/collection")
public class UserCollectionActivity extends DumpExtrasActivity {
这个注解相当于为这个Activity在路由器表中添加了user/collection这个域名映射记录,接下来在想要跳转的地方加上这么一句话:
Routers.open(context, "router://user/collection")
只是跳转到该Activity,该Activity结束的时候不需要传值给上一个Activity
Routers.openForResult(context,"router://user/collection" ,requestCode);
该Activity结束的时候需要传值给上一个Activity
用起来确实很简单,那么如果要传值的话,需要加上Router(value = {"main", "home"},
longParams = {"id", "updateTime"},
booleanParams = "web")
完整的url为router://main?id=1103&updateTime=537896&web=true,是不是类似于get方式传值,其它的方式小伙伴们请看作者的介绍
ActivityRouter介绍
好,继续看跳转的流程,当调用open或者openForResult的方法时都会走到下面这个方法
private static boolean open(Context context, Uri uri, int requestCode, RouterCallback callback) {
boolean success = false;
if (callback != null) {
if (callback.beforeOpen(context, uri)) {
return false;
}
}
try {
success = doOpen(context, uri, requestCode);
} catch (Throwable e) {
e.printStackTrace();
if (callback != null) {
callback.error(context, uri, e);
}
}
if (callback != null) {
if (success) {
callback.afterOpen(context, uri);
} else {
callback.notFound(context, uri);
}
}
return success;
}
public interface RouterCallback {
void notFound(Context context, Uri uri);
boolean beforeOpen(Context context, Uri uri);
void afterOpen(Context context, Uri uri);
void error(Context context, Uri uri, Throwable e);
private static boolean doOpen(Context context, Uri uri, int requestCode) {
initIfNeed();
Path path = Path.create(uri);
for (Mapping mapping : mappings) {
if (mapping.match(path)) {
if (mapping.getActivity() == null) {
mapping.getMethod().invoke(context, mapping.parseExtras(uri));
return true;
}
Intent intent = new Intent(context, mapping.getActivity());
intent.putExtras(mapping.parseExtras(uri));
intent.putExtra(KEY_RAW_URL, uri.toString());
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
if (requestCode >= 0) {
if (context instanceof Activity) {
((Activity) context).startActivityForResult(intent, requestCode);
} else {
throw new RuntimeException("can not startActivityForResult context " + context);
}
} else {
context.startActivity(intent);
}
return true;
}
}
return false;
}
先来看一看这个框架是怎么将注解信息注册到路由表中的?
private static void initIfNeed() {
if (!mappings.isEmpty()) {
return;
}
RouterInit.init();
sort();
}
public class RouterInit {
public static void init() {
}
}
public final class RouterMapping {
public static final void map() {
}
起到骗过编译器检查的效果,正常的使用还没有被编译注解处理器生成的java类的时候是利用反射,这个欺骗反而增加了那么点速度,编译器完成之后,新生成的java类将覆盖掉那两个写死的java类(占坑java类),在前面的APT生成java类时已经提到过,最终生成的类最后是调用了这个框架已经存在的类方法,也就是Routers.map的方法
mappings.add(new Mapping(format, activity, method, extraTypes));
直接向表中装填mapping对象,这里注意一下第三个参数,如果Router注解标记的是类,那么第三个参数为null,如果标记的是方法,那么第三个参数为MethodInvoker引用,第二个参数为null。 public Mapping(String format, Class extends Activity> activity, MethodInvoker method, ExtraTypes extraTypes) {
if (format == null) {
throw new NullPointerException("format can not be null");
}
this.format = format;
this.activity = activity;
this.method = method;
this.extraTypes = extraTypes;
if (format.toLowerCase().startsWith("http://") || format.toLowerCase().startsWith("https://")) {
this.formatPath = Path.create(Uri.parse(format));
} else {
this.formatPath = Path.create(Uri.parse("helper://".concat(format)));
}
}
public static Path create(Uri uri) {
//鍒涘缓helper
Path path = new Path(uri.getScheme().concat("://"));
//得到路径
String urlPath = uri.getPath();
if (urlPath == null) {
urlPath = "";
}
//截取掉最后一个/
if (urlPath.endsWith("/")) {
urlPath = urlPath.substring(0, urlPath.length() - 1);
}
parse(path, uri.getHost() + urlPath);
return path;
}
//有多少种路径可以找到它就用多少种path
private static void parse(Path scheme, String s) {
String[] components = s.split("/");
Path curPath = scheme;
for (String component : components) {
Path temp = new Path(component);
curPath.next = temp;
curPath = temp;
}
}
ok,将Activity的映射信息注册到路由表后,那么又回到doOpen方法,遍历路由表信息看看有没有匹配的Path有的话跳转,没有的话回调notFound方法,接下来假设要跳转的url为router://main?id=1103&updateTime=537896&web=true,这里会创建一个拥有两个Path的链表,然后遍历Mapping,调用下面这个方法看Path是否匹配
public boolean match(Path fullLink) {
if (formatPath.isHttp()) {
return Path.match(formatPath, fullLink);
} else {
// fullLink without host
boolean match = Path.match(formatPath.next(), fullLink.next());
if (!match && fullLink.next() != null) {
// fullLink with host
match = Path.match(formatPath.next(), fullLink.next().next());
}
return match;
}
}
public static boolean match(final Path format, final Path link) {
if (format == null || link == null) {
return false;
}
if (format.length() != link.length()) {
return false;
}
Path x = format;
Path y = link;
while (x != null) {
if (!x.match(y)) {
return false;
}
x = x.next;
y = y.next;
}
return true;
}
public Bundle parseExtras(Uri uri) {
Bundle bundle = new Bundle();
// path segments // ignore scheme
Path p = formatPath.next();
Path y = Path.create(uri).next();
while (p != null) {
if (p.isArgument()) {
put(bundle, p.argument(), y.value());
}
p = p.next();
y = y.next();
}
// parameter
Set names = UriCompact.getQueryParameterNames(uri);
for (String name : names) {
String value = uri.getQueryParameter(name);
put(bundle, name, value);
}
return bundle;
}
public static Set getQueryParameterNames(Uri uri) {
String query = uri.getEncodedQuery();
if (query == null) {
return Collections.emptySet();
}
Set names = new LinkedHashSet();
int start = 0;
do {
int next = query.indexOf('&', start);
int end = (next == -1) ? query.length() : next;
int separator = query.indexOf('=', start);
if (separator > end || separator == -1) {
separator = end;
}
String name = query.substring(start, separator);
names.add(Uri.decode(name));
// Move start to end of name.
start = end + 1;
} while (start < query.length());
return Collections.unmodifiableSet(names);
}
最后一步就是将参数转化为不同的类型 private void put(Bundle bundle, String name, String value) {
int type = extraTypes.getType(name);
name = extraTypes.transfer(name);
if (type == ExtraTypes.STRING) {
type = extraTypes.getType(name);
}
switch (type) {
case ExtraTypes.INT:
bundle.putInt(name, Integer.parseInt(value));
break;
case ExtraTypes.LONG:
bundle.putLong(name, Long.parseLong(value));
break;
case ExtraTypes.BOOL:
bundle.putBoolean(name, Boolean.parseBoolean(value));
break;
case ExtraTypes.SHORT:
bundle.putShort(name, Short.parseShort(value));
break;
case ExtraTypes.FLOAT:
bundle.putFloat(name, Float.parseFloat(value));
break;
case ExtraTypes.DOUBLE:
bundle.putDouble(name, Double.parseDouble(value));
break;
case ExtraTypes.BYTE:
bundle.putByte(name, Byte.parseByte(value));
break;
case ExtraTypes.CHAR:
bundle.putChar(name, value.charAt(0));
break;
default:
bundle.putString(name, value);
break;
}
}
在APT中解析参数类型的时候,每一个mapping都有一个唯一的ExtraTypes类来储存不同的参数类型,以参数的名字标记之 public int getType(String name) {
if (arrayContain(intExtra, name)) {
return INT;
}
if (arrayContain(longExtra, name)) {
return LONG;
}
if (arrayContain(booleanExtra, name)) {
return BOOL;
}
if (arrayContain(shortExtra, name)) {
return SHORT;
}
if (arrayContain(floatExtra, name)) {
return FLOAT;
}
if (arrayContain(doubleExtra, name)) {
return DOUBLE;
}
if (arrayContain(byteExtra, name)) {
return BYTE;
}
if (arrayContain(charExtra, name)) {
return CHAR;
}
return STRING;
}
private String[] intExtra;
private String[] longExtra;
private String[] booleanExtra;
private String[] shortExtra;
private String[] floatExtra;
private String[] doubleExtra;
private String[] byteExtra;
private String[] charExtra;
ExtraTypes类总共有这么多集合,在编译期已经将参数名字保存到ExtraTypes中,最后通过新生成的java文件,调用RouterInit.init()方法将ExtraTypes和mapping绑定,从而达到在传参数时的动态转化。好了,在android的实现一个路由架构的原理基本就介绍完了,欢迎小伙伴点赞和留言。