首先着重分析下Arouter的源码设计,主要研究这个框架的原因,一个是它算比较新的框架,功能较为全面和强大,并且不断在维护。其次和作者是一个公司的,可以有更深层的交流。
先放一篇作者的演讲既要,这个也是分析源码时候思路的参考和本文很多图片的来源。这篇文章中,也很好的说明了为什么要设计路由的原因以及所带来的好处。
首先来看下整个Arouter的大致代码结构。
整个代码结构应该是分成三个module。
图中API,基本是路由框架在初始化,和工作过程中的相关代码。
Postcard
,在跳转过程中,这个对象用于携带一些参数,和相关属性,无论是跳到目标页面后,还是发生拦截的过程中,框架处理的流程中,均会用到这些信息。Compiler 是个注解处理器。
下面看下结合ARouter框架进行改造后的代码结构。
和一些较为简单的模块化框架相比,ARouter中 引入了组的概念,将同一模块的模块的目标页面分成不同小组,然后通过一个根节点统一管理,这样的好处,是可以根据需要,按需加载模块。具体可以参考作者的一段话,图就不引用了。
ARouter在初始化的时候只会一次性地加载所有的root结点,而不会加载任何一个Group结点,这样就会极大地降低初始化时加载结点的数量。因为每个模块中可能有N个分组,每个分组中可能有N个页面,如果一次性地将所有的页面全部加载进来,那么整个复杂度可能不只是O(N^2),但是每个模块都只加载其根节点,从算法的角度考虑可能就是复杂度为O(N)的方案,也就是有多少个模块就只需要加载多少个结点。下图中的三个圈中体现的就是ARouter初始化时加载的状况。那么什么时候加载分组结点呢?其实就是当某一个分组下的某一个页面第一次被访问的时候,整个分组的全部页面都会被加载进去,这就是ARouter的按需加载。其实在整个APP运行的周期中,并不是所有的页面都需要被访问到,可能只有20%的页面能够被访问到,所以这时候使用按需加载的策略就显得非常重要了,这样就会减轻很大的内存压力。
再看下经过 ARouter 模块化后的代码是如何工作的。主要步骤在上图中已经有了说明,初始化时候,会加载对应的映射文件到缓存中,然后通过URL跳转目标页面时候,会通过映射文件的规则,经由Arouter 模块进行跳转,拦截,操作服务中方法等动作,从而达到模块间解耦。
主要用 Router Processor 为例,简要分析下注解处理器,生成一个URL路由映射关系的流程。在如下的示例代码中,@Route的path中,会和当前目标页面Test1Activity 进行匹配,被 @Autowired 所注解的变量,可以直接通过跳转的URL被赋值。
/**
* https://m.aliyun.com/test/activity1?name=老王&age=23&boy=true&high=180
*/
@Route(path = "/test/activity1")
public class Test1Activity extends AppCompatActivity {
@Autowired
String name;
@Autowired
int age;
@Autowired(name = "boy")
boolean girl;
@Autowired
TestParcelable pac;
@Autowired
TestObj obj;
关于注解处理器,可以参考 Android APT(编译时代码生成)最佳实践 了解基本用法。
在注解处理器中,和路由信息相关的主要有两个比较重要的数据结构,分别表示来组的关系,和根结点。
private Map<String, Set<RouteMeta>> groupMap = new HashMap<>(); // ModuleName and routeMeta.
private Map<String, String> rootMap = new TreeMap<>(); // Map of root metas, used for generate class file in order.
首先在 process 方法中拿到 被Route注解的元素。
Set extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
try {
logger.info(">>> Found routes, start... <<<");
this.parseRoutes(routeElements);
} catch (Exception e) {
logger.error(e);
}
然后交给 parseRoutes 流程完成解析,并生成代码。
生成代码的流程用到来 javapoet 这个第三方库来生成路由映射关系的代码,而不是靠纯的硬编码的方式。
类似参数类型名,参数名称,方法等都可以通过api的方式来生成,最后组装成代码。
目标生成路由表结构如下
public class ARouter$$Group$$test implements IRouteGroup {
@Override
public void loadInto(Map atlas) {
atlas.put("/test/activity1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/activity1", "test", new java.util.HashMap() {{put("name", 18); put("boy", 0); put("age", 3); put("url", 18); }}, -1, -2147483648));
atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap() {{put("key1", 18); }}, -1, -2147483648));
atlas.put("/test/activity3", RouteMeta.build(RouteType.ACTIVITY, Test3Activity.class, "/test/activity3", "test", new java.util.HashMap() {{put("name", 18); put("boy", 0); put("age", 3); }}, -1, -2147483648));
atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648));
atlas.put("/test/webview", RouteMeta.build(RouteType.ACTIVITY, TestWebview.class, "/test/webview", "test", null, -1, -2147483648));
}
}
具体在 parseRoutes 解析过程中的流程如下。
ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ClassName.get(RouteMeta.class)
);
ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas").build();
loadinfo
方法。MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(groupParamSpec);
loadIntoMethodOfGroupBuilder.addStatement(
"atlas.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, " + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap(){{" + mapBodyBuilder.toString() + "}}")) + ", " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
routeMeta.getPath(),
routeMetaCn,
routeTypeCn,
ClassName.get((TypeElement) routeMeta.getRawType()),
routeMeta.getPath().toLowerCase(),
routeMeta.getGroup().toLowerCase());
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler);
其他注解的处理大致类似,中间可能加入了一些特别的处理逻辑。
通过类似的方式,结合各种注解,在代码编译阶段,可以预先生成路由关系等信息的类,生成的位置如下,然后这些类会被一同打包到 apk 中。
这些类中包含了路由信息(包括根结点和组的对应关系),拦截器,以及Provider的信息。
这是在ARouter 模块初始化时候所进行动作,主要是将注解编译器生成的映射文件中路由关系等信息,加载到内存中。然后在后续执行跳转操作时候,直接查询内存中的路由规则进行跳转。主要通过如下方法实现:
ARouter.init(getApplication());
这个方法实际调用了LogisticsCenter
层init
的静态方法。
主要流程是先通过包名去扫描注解编译器生成的映射文件,拿到一个类名对应的集合,然后通过反射的方式,实例化接口,通过loadinfo方法,将映射文件中,定义的路由关系信息,载入到warehouse
中数据结构中,完成了在内存中缓存一份的操作。
// These class was generate by arouter-compiler.
List classFileNames = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
//
for (String className : classFileNames) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
// This one of root elements, load root.
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
// Load interceptorMeta
((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
// Load providerIndex
((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
}
}
经过上述流程后,在Warehouse
对应静态变量中,就存在了一份对应的缓存数据。
ARouter.getInstance()
.build("/test/activity2")
.navigation();
上述是一个简单的跳转过程。结合源码分析下过程。
这里首先看下一个关键的类Postcard
// Base
private Uri uri;
private Object tag; // A tag prepare for some thing wrong.
private Bundle mBundle; // Data to transform
private int flags = -1; // Flags of route
private int timeout = 300; // Navigation timeout, TimeUnit.Second
private IProvider provider; // It will be set value, if this postcard was provider.
private boolean greenChannel;
// Animation
private Bundle optionsCompat; // The transition animation of activity
private int enterAnim;
private int exitAnim;
这个类继承了RouteMeta
这个基类。
private RouteType type; // Type of route
private Element rawType; // Raw type of route
private Class> destination; // Destination
private String path; // Path of route
private String group; // Group of route
private int priority = -1; // The smaller the number, the higher the priority
private int extra; // Extra data
private Map paramsType; // Param type
在这两个类中,有很多和跳转相关的成员变量。在跳转的过程中,通过Arouter
类中提供的Api完成参数填充,实际是先创建了一个Postcard对象,随后在_Arouter
这个类中进行逻辑处理,取到Postcard对象中参数,拿到映射关系,然后完成跳转。
具体看下这个过程:
ARouter.getInstance()
.build("/test/activity2")
.navigation();
Postcard
对象。调用Arouter
的build
Api,最后走到了如下流程,在这一步流程中,最重要的就是完成将path填入,方便后续流程通过path在映射关系中找到需要跳转的Activity。
protected Postcard build(String path, String group) {
if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
throw new HandlerException(Consts.TAG + "Parameter is invalid!");
} else {
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
return new Postcard(path, group);
}
}
在navigation
的流程中完成跳转,这是一个在Postcard
中的方法。最终会走到_ARouter
中的 navigation方法,在这个流程中,完成路由跳转的流程。
public Object navigation(Context mContext, Postcard postcard, int requestCode, NavigationCallback callback) {
return _ARouter.getInstance().navigation(mContext, postcard, requestCode, callback);
}
在这个方法中,会完成一步重要的操作,LogisticsCenter.completion(postcard);
完成Postcard
数据填入。主要是将之前的 path ,在之前的缓存关系中找到相应的Activity,填入到Postcard
的destination
变量中。
在随后的流程中,会处理一些降级,拦截,绿色通道等处理。最终走到Android原生的跳转流程, 在这个流程中,拿到destination
的值,即为可以跳转的对象,然后完成跳转。
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
// Set flags.
int flags = postcard.getFlags();
if (-1 != flags) {
intent.setFlags(flags);
} else if (!(currentContext instanceof Activity)) { // Non activity, need less one flag.
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
// Navigation in main looper.
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (requestCode > 0) { // Need start for result
ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
} else {
ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
}
if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) { // Old version.
((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
}
}
});
break;
至此一次简单的跳转就就结束了,当然在跳转过程中,还可以通过其他Api完成类似设置参数等复杂的操作,完成更多复杂的功能。