在如今的 Android 组件化开发中,一款好的路由框架是不可或缺的。比如目前阿里的 ARouter 、美团的 WMRouter 等。路由框架可以降低 Activity 之间的耦合,从而在不需要关心目标 Activity 的具体实现类, 利用协议完成跳转。
ActivityRouter使用方法
在AndroidManifest.xml配置
<activity
android:name="com.github.mzule.activityrouter.router.RouterActivity"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mzule" /><!--改成自己的scheme-->
</intent-filter>
</activity>
在需要配置的Activity上添加注解
@Router("main")
public class MainActivity extends Activity {
...
}
想要跳转到 MainActivity ,只要调用以下代码即可
Routers.open(context, "mzule://main")
如果想用 @Router 来调用方法
@Router("logout")
public static void logout(Context context, Bundle bundle) {
Toast.makeText(context, "logout", Toast.LENGTH_SHORT).show();
}
源码解析
ActivityRouter 工程的结构如下
- activityrouter: 路由跳转的具体实现代码
- annotaition: 路由注解
- app: 路由 demo
- app_module: 路由 demo module
- compiler: 注解处理
- stub: 壳 module
annotation
先来看看 Router 的注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @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 ""; }
|
@Router 定义了该 Activity 路由的名字以及一些参数,这里可以注意到 @Retention 是 CLASS ,所以后面肯定在编译期间利用 Processor 来解析 @Router 生成路由表的。
另外,看到 @Target 是 ElementType.TYPE 和 ElementType.METHOD ,其实 @Router 除了跳转 Activity 之外,还有一个功能就是可以执行方法,只要在方法加上 @Router 即可。
路由表的生成源码我们到后面再讲,先来看看有了协议之后,Routers 是如何实现跳转 Activity 的。
activityrouter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public class Routers {
...
public static boolean open(Context context, String url) { return open(context, Uri.parse(url)); } public static boolean open(Context context, String url, RouterCallback callback) { return open(context, Uri.parse(url), callback); } public static boolean open(Context context, Uri uri) { return open(context, uri, getGlobalCallback(context)); } public static boolean open(Context context, Uri uri, RouterCallback callback) { return open(context, uri, -1, callback); } public static boolean openForResult(Activity activity, String url, int requestCode) { return openForResult(activity, Uri.parse(url), requestCode); } public static boolean openForResult(Activity activity, String url, int requestCode, RouterCallback callback) { return openForResult(activity, Uri.parse(url), requestCode, callback); } public static boolean openForResult(Activity activity, Uri uri, int requestCode) { return openForResult(activity, uri, requestCode, getGlobalCallback(activity)); } public static boolean openForResult(Activity activity, Uri uri, int requestCode, RouterCallback callback) { return open(activity, uri, requestCode, callback); } ...
}
|
可以看到不同的 open openForResult 方法重载,最后都是调用了 open(Context context, Uri uri, int requestCode, RouterCallback callback)
。那么接着跟踪:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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; }
|
open 方法中有很多都是不同状态下 callback 的回调,真正跳转的逻辑放在了 doOpen 方法中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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; }
|
我们一步步来分析 doOpen 中的具体步骤。先从 Path path = Path.create(uri);
开始看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public static Path create(Uri uri) { 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; }
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; } }
|
上面的代码看完可能会感觉很绕,简单地解释下。上面这段代码主要做的事情就是把传入的 uri 解析,生成了一个 Path 对象。该 Path 对象主要包含了 uri 中的 scheme 、host 、path 这三部分,利用单链表的特点把这三部分串连起来。这个 Path 也就是后面用来匹配路由表用的。
关于 uri 的 scheme 、 host 等,在这里就简单地普及下。
比如现在有一个 uri
mzule://main/home/login?username=tom
这个 uri 就可以分解为
scheme :mzule ,就是 “://” 前面的字符串
host :main ,“://” 后面的字符串
path :home 和 login 都属于 path,就是 “/” 与 “/” 之间的字符串
query :参数,可以理解成键值对,多个之间用 & 连接。获取 username 这个参数,对应的值就是 tom
生成好了 Path 之后,就是遍历路由表进行匹配了。
所谓的路由表其实就是一个 List
private static List<Mapping> mappings = new ArrayList<>();
在调用 RouterInit.init 时候会把路由数据添加到 List 中。准确的说, RouterInit.init 中调用了 Router.map 方法来实现添加的。
1 2 3
| static void map(String format, Class<? extends Activity> activity, MethodInvoker method, ExtraTypes extraTypes) { mappings.add(new Mapping(format, activity, method, extraTypes)); }
|
那么,我们来看下 Mapping 的结构
1 2 3 4 5 6 7 8 9 10
| public class Mapping { private final String format; private final Class<? extends Activity> activity; private final MethodInvoker method; private final ExtraTypes extraTypes; private Path formatPath; ... }
|
- format 就是我们传入的 uri
- activity 就是路由对应的 activity
- method 表示是否是执行方法
- extraTypes 是所携带的参数类型
- formatPath 就是 uri 对应的 Path
具体的 Mapping 初始化是在 Processor 生成的代码中完成的,我们到后面再讲。
在回过头来看 doOpen 方法,在 mapping.match(path) 方法中用来判断该 path 有没有匹配路由表中的路由
1 2 3 4 5 6 7 8 9 10 11 12 13
| public boolean match(Path fullLink) { if (formatPath.isHttp()) { return Path.match(formatPath, fullLink); } else { boolean match = Path.match(formatPath.next(), fullLink.next()); if (!match && fullLink.next() != null) { match = Path.match(formatPath.next(), fullLink.next().next()); } return match; } }
|
Mapping 的 match 方法就是把自身的 formatPath 和 fullLink 进行比较,最终调用的还是 Path.match 方法,本质就是把 Path 链表中的每一项进行比较,来判断两个 Path 是否相等。
再后面的就是判断 activity ,如果是空的,就认为是执行方法,否则就构造 Intent 来实现跳转,再利用 requestCode 来判断是 startActivity 还是 startActivityForResult 。其中执行方法主要调用了 MethodInvoker.invoke 方法
1 2 3
| public interface MethodInvoker { void invoke(Context context, Bundle bundle); }
|
再重点关注下 mapping.parseExtras(uri) 这句代码。这里主要做的事情就是构造 Bundle 传入 uri 的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| public Bundle parseExtras(Uri uri) { Bundle bundle = new Bundle(); 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(); } Set<String> names = UriCompact.getQueryParameterNames(uri); for (String name : names) { String value = uri.getQueryParameter(name); put(bundle, name, value); } return bundle; }
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; } }
|
这代码很简单,基本上都加了注释,相信大家都看得懂,就不讲咯。
到这里,整个 ActivityRouter 的流程就讲完啦。
剩下的,就是 Processor 解析注解生成代码了。
compiler
先告诉处理器支持的注解
1 2 3 4 5 6 7 8
| @Override public Set<String> getSupportedAnnotationTypes() { Set<String> ret = new HashSet<>(); ret.add(Modules.class.getCanonicalName()); ret.add(Module.class.getCanonicalName()); ret.add(Router.class.getCanonicalName()); return ret; }
|
剩下主要看 RouterProcessor 的 process 方法。
方法的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @Override 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; 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; } 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; } if (hasModules) { debug("generate modules RouterInit"); generateModulesRouterInit(moduleNames); } else if (!hasModule) { debug("generate default RouterInit"); generateDefaultRouterInit(); } return handleRouter(moduleName, roundEnv); }
|
process 方法中的逻辑可以分为三部分:
- 判断是否有 @module 和 @modules ,即是否是组件化开发的
- 生成 RouterInit
- 生成 RouterMapping
那我们慢慢分析,先来看第一部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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; }
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 类
1 2 3 4 5 6 7
| if (hasModules) { debug("generate modules RouterInit"); generateModulesRouterInit(moduleNames); } else if (!hasModule) { debug("generate default RouterInit"); generateDefaultRouterInit(); }
|
如果是多 module 组件化开发,最终会调用 generateModulesRouterInit ,否则调用的就是默认的 generateDefaultRouterInit 。
这里我们就看 generateModulesRouterInit 的代码吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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("com.github.mzule.activityrouter.router", routerInit) .build() .writeTo(filer); } catch (Exception e) { e.printStackTrace(); } }
|
可以看到,利用了 javapoet 来生成 java 代码,这代码很简单,就不用多讲啦,直接来看下最后生成 RouterInit 类的代码吧
1 2 3 4 5 6 7 8
| package com.github.mzule.activityrouter.router;
public final class RouterInit { public static final void init() { RouterMapping_app.map(); RouterMapping_sdk.map(); } }
|
RouterInit 生成好之后,最后的工作就是生成对应的 RouterMapping_app 和 RouterMapping_sdk 这两个类了。
生成的入口就是 handleRouter(moduleName, roundEnv) 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| 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<String,String> 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<String, String>()"); 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; }
private void addStatement(MethodSpec.Builder mapMethod, Class typeClz, String[] args) { String extras = join(args); if (extras.length() > 0) { String typeName = typeClz.getSimpleName(); String s = typeName.substring(0, 1).toUpperCase() + typeName.replaceFirst("\\w", "");
mapMethod.addStatement("extraTypes.set" + s + "Extra($S.split(\",\"))", extras); } }
|
来看一下最后生成的 RouterMapping_xxx 的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| public final class RouterMapping_app { public static final void map() { java.util.Map<String,String> transfer = null; com.github.mzule.activityrouter.router.ExtraTypes extraTypes;
transfer = null; extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes(); extraTypes.setTransfer(transfer); com.github.mzule.activityrouter.router.Routers.map("user/:userId", UserActivity.class, null, extraTypes); com.github.mzule.activityrouter.router.Routers.map("user/:nickname/city/:city/gender/:gender/age/:age", UserActivity.class, null, extraTypes);
transfer = new java.util.HashMap<String, String>(); transfer.put("web", "fromWeb"); extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes(); extraTypes.setTransfer(transfer); extraTypes.setLongExtra("id,updateTime".split(",")); extraTypes.setBooleanExtra("web".split(",")); com.github.mzule.activityrouter.router.Routers.map("http://mzule.com/main", MainActivity.class, null, extraTypes); com.github.mzule.activityrouter.router.Routers.map("main", MainActivity.class, null, extraTypes); com.github.mzule.activityrouter.router.Routers.map("home", MainActivity.class, null, extraTypes);
transfer = null; extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes(); extraTypes.setTransfer(transfer); com.github.mzule.activityrouter.router.Routers.map("with_host", HostActivity.class, null, extraTypes);
transfer = null; extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes(); extraTypes.setTransfer(transfer); com.github.mzule.activityrouter.router.Routers.map("home/:homeName", HomeActivity.class, null, extraTypes);
transfer = null; extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes(); extraTypes.setTransfer(transfer); com.github.mzule.activityrouter.router.Routers.map("logout", null, new MethodInvoker() { public void invoke(android.content.Context context, android.os.Bundle bundle) { NonUIActions.logout(context, bundle); } }, extraTypes);
transfer = null; extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes(); extraTypes.setTransfer(transfer); com.github.mzule.activityrouter.router.Routers.map("upload", null, new MethodInvoker() { public void invoke(android.content.Context context, android.os.Bundle bundle) { NonUIActions.uploadLog(context, bundle); } }, extraTypes);
transfer = null; extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes(); extraTypes.setTransfer(transfer); com.github.mzule.activityrouter.router.Routers.map("user/collection", UserCollectionActivity.class, null, extraTypes);
} }
|
至此,ActivityRouter 所有的流程都已经讲完啦!!!
RouterActivity
对啦,还有一点,ActivityRouter 支持从外部唤起 Activity 。
在 AndroidManifest.xml 中声明 RouterActivity ,填写对应 scheme 和 host 。
<activity
android:name="com.github.mzule.activityrouter.router.RouterActivity"
android:theme="@android:style/Theme.NoDisplay">
...
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="mzule.com" />
</intent-filter>
</activity>
其实先唤起的是 RouterActivity ,然后在 RouterActivity 中根据 uri 再跳转到对应的 Activity ,这点可以从 RouterActivity 的代码中印证。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class RouterActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
RouterCallback callback = getRouterCallback();
Uri uri = getIntent().getData(); if (uri != null) { Routers.open(this, uri, callback); } finish(); }
private RouterCallback getRouterCallback() { if (getApplication() instanceof RouterCallbackProvider) { return ((RouterCallbackProvider) getApplication()).provideRouterCallback(); } return null; } }
|