ARouter使用与原理分析
一、使用入门
见官方文档:ARouter
二、技术原理分析
ARouter采用注解与APT技术,自动生成代码,在运行时调用,那么这里也就分为两部分,生成的代码和ARouter源代码,我们也分为两部分学习。
2.1 调用过程和代码探析
2.1.1 调用过程
根据官方文档,基本的使用方法为下面这行代码:
ARouter.getInstance().build("/test/second").navigation();
根据这行代码,从Activity出发,调用时许过程如下图:
第一步:构建Postcard
Postcard顾名思义,为“跳转卡片”,ARouter.getInstance().build("/xxx/xxxx")返回的就是Postcard对象,主要用来携带bundle跳转参数。而ARouter只是对外统一的api接口,实现基本由_ARouter完成,所以构建Postcard也是由_ARouter。
第二步:Postcard.navigation()实现跳转
Postcard.navigation()其实也是调用_ARouter.getInstance().navigation()完成的,即最后也是由_ARouter完成,在_ARouter.navigation时,首先调用LogisticsCenter.completion(postcard)完善postcard的信息,而completion方法,则完成了path到activity的转换关系。完善后,再调用_navigation完成最终跳转。
第三步:LogisticsCenter.completion(postcard)将path映射到activity
看到这一步时,会发现大致看不懂,没关系,大致明白这里的作用,待会换个思路看源码就可以了。
第四步:_ARouter._navigation进行跳转
关键代码如下:
private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
final Context currentContext = null == context ? mContext : context;
switch (postcard.getType()) {
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);
}
// Set Actions
String action = postcard.getAction();
if (!TextUtils.isEmpty(action)) {
intent.setAction(action);
}
// Navigation in main looper.
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode, currentContext, intent, postcard, callback);
}
});
break;
···············
return null;
}
return null;
}
可以很明显的看到,这一步是判断postcard的信息,然后建立intent进行跳转,也就是仍然还是通过startActivity()这个函数进行跳转,并没有太多的奥秘。重点是ARouter内部映射的实现原理和方法。
2.1.2 主体源码分析
刚才在调用过程中接触到了几个类,趁热打铁,先介绍下这几个类:
ARouter.java: 对外提供调用方法的类。
_ARouter.java: ARouter.java的代理类,ARouter.java中的关键方法都在_ARouter.java里实现。
RouteMeta.java: 包含了路由跳转所需要的信息,比如跳转类型,目标等。Postcard 继承 RouteMeta。
Postcard.java: 直译“跳转卡片”,ARouter.getInstance().build("/xxx/xxxx")返回的就是Postcard对象,主要用来携带bundle跳转参数。
-
Warehouse.java: 仅包含了几个集合类型(主要是Map类型)的静态变量,这几个静态集合用来保存activity和其path对应关系,以及其分组关系等。
如:
// Cache route and metas static Map
> groupsIndex = new HashMap<>(); static Map routes = new HashMap<>(); LogisticsCenter.java: 实现Arouter的初始化,以及对Warehouse类里的几个集合进行填充等。
ARouter同样也定义了好几个注解类,其中最关键的就是@Route注解,而@Route注解是使用了APT技术来发挥作用的。APT的核心类-AbstractProcessor类就是编译时用来生成代码的。ARouter源码中有个RouteProcessor.java类,继承自AbstractProcessor类,就是用来处理@Route注解的,重写的process()方法,实现了对@Route注解的处理和自动生成文件代码的逻辑。
项目编译后,会生成代码,结构如图:
这里先说明下,目前我们的项目一共有4个Activity,分别是登录用的LoginActivity和LoginWeb,以及项目主体的MainActivity和ProfileActivity,所以目前的路由分组,前两个Activity分为一组,后两个Activity分为一组,如下图:
ARouter默认用第一个斜杠后面的字符串表示分组信息。
于是ARouter生成的代码文件,Group有两个,后缀分别是我们的两个分组。我们分别学习这几个文件:
- 以"Arouter$$Root$$组件名"命名的文件,即我们的ARouter$$Root$$app.java,是用来保存页面路径的分组和路径文件间的对应关系的,甚至代码只有4行:
public class ARouter$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map> routes) {
routes.put("app", ARouter$$Group$$app.class);
routes.put("login", ARouter$$Group$$login.class);
}
}
这个类中的loadInto()方法,是在ARouter初始化的时候,即ARouter.init(mApplication) -> LogisticsCenter.init()中被调用的。传入的routes即为Warehouse类的静态变量Warehouse.groupsIndex。也就是为路由分组这个Map设置了两个键值关系。
- 以“Arouter$$Group$$分组”命名的文件,即我们的Arouter$$Group$$login.java和Arouter$$Group$$app.java,是用来保存该分组下,页面path和对应的页面关系的。有几个分组,就会有几个“Arouter$$Root$$分组”文件。
public class ARouter$$Group$$app implements IRouteGroup {
@Override
public void loadInto(Map atlas) {
atlas.put("/app/main", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/app/main", "app", null, -1, -2147483648));
atlas.put("/app/profile", RouteMeta.build(RouteType.ACTIVITY, ProfileActivity.class, "/app/profile", "app", new java.util.HashMap(){{put("login", 8); }}, -1, -2147483648));
}
}
public class ARouter$$Group$$login implements IRouteGroup {
@Override
public void loadInto(Map atlas) {
atlas.put("/login/index", RouteMeta.build(RouteType.ACTIVITY, LoginActivity.class, "/login/index", "login", null, -1, -2147483648));
atlas.put("/login/oauth", RouteMeta.build(RouteType.ACTIVITY, LoginWeb.class, "/login/oauth", "login", new java.util.HashMap(){{put("url", 8); }}, -1, -2147483648));
}
}
这个类的loadInto()方法,传入的是Warehouse类的另一个静态变量Warehouse.routes,但是此方法不是在ARouter初始化的时候调用,而是在页面跳转的时候,如果满足条件“要跳转的页面是其所在的分组首个要跳转的”才调用,加载完Warehouse.routes后继续跳转页面。这样也就实现了分组加载,避免加载过多,只加载该页面所属分组的映射关系,减少了不必要的内存耗费。
代码调用链:ARouter.getInstance().navigation() -> _ARouter.getInstance().navigation() -> LogisticsCenter.completion() -> IRouteGroup.loadInto(Warehouse.routes),这里也就接上了之前讲到的调用过程的第三步。
下面我们再来详细看一下_Arouter类的build()方法:
/**
* Build postcard by path and default group
*/
protected Postcard build(String path) {
if (TextUtils.isEmpty(path)) {
throw new HandlerException(Consts.TAG + "Parameter is invalid!");
} else {
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
return build(path, extractGroup(path), true);
}
}
一般情况下我们都不会自定义路径替换逻辑,即不会实现PathReplaceService,因此代码会走到最后一行:return build(path, extractGroup(path), true),extractGroup(path)这个方法的作用是提取出路径的分组部分,比如页面path为“/login/index”,则extractGroup(path)得到的结果是“login”,再继续看build(path, extractGroup(path), true)的代码:
/**
* Build postcard by path and group
*/
protected Postcard build(String path, String group, Boolean afterReplace) {
if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
throw new HandlerException(Consts.TAG + "Parameter is invalid!");
} else {
if (!afterReplace) {
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
}
return new Postcard(path, group);
}
}
afterReplace为true,因此ARouter.getInstance().build("/login/index")的运行结果就是new Postcard("/login/index", “/login”)。
那么ARouter.getInstance().build("/login/index")..navigation()就是:new Postcard("/login/index", “/login”).navigation()。
再来看Postcard做了什么处理:
public Postcard() {
this(null, null);
}
public Postcard(String path, String group) {
this(path, group, null, null);
}
public Postcard(String path, String group, Uri uri, Bundle bundle) {
setPath(path);
setGroup(group);
setUri(uri);
this.mBundle = (null == bundle ? new Bundle() : bundle);
}
可以看到new Postcard("/login/index", “/login”)是得到一个Postcard对象,其成员变量path,group和mBundle都已经赋值。
再看Postcard的navigation()方法:
public Object navigation() {
return navigation(null);
}
public Object navigation(Context context) {
return navigation(context, null);
}
public Object navigation(Context context, NavigationCallback callback) {
return ARouter.getInstance().navigation(context, this, -1, callback);
}
ARouter的navigation()方法是由_ARouter实现的,方法较长,这里贴出简化后的代码:
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
try {
LogisticsCenter.completion(postcard);
} catch (NoRouteFoundException ex) {
logger.warning(Consts.TAG, ex.getMessage());
xxx处理
return null;
}
xxxx处理
return _navigation(context, postcard, requestCode, callback);
}
可见,通过LogisticsCenter.completion(postcard)补充了postcard后,传入了_navigation()方法。而_navigation()在之前的2.1.1中就有说明,就是根据postcard携带的信息构造出Intent对象等,然后调用ActivityCompat.startActivity()方法启动页面。最后再来看看按需加载的实现方法:
public synchronized static void completion(Postcard postcard) {
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
if (null == routeMeta) {
// 首次跳转此分组的页面时,Warehouse.routes未加载此分组,因此会走到这里
// 此路由信息不存在,或者未加载,所以找不到
Class extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); // Load route meta.
// 加载此分组的path和页面映射关系到Warehouse.routes中
try {
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
iGroupInstance.loadInto(Warehouse.routes);
} catch (Exception e) {
throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
}
completion(postcard); // 再次调用本方法,补充postcard
} else {
// 非首次跳转此分组的页面,则会走到这里
// 已找到路由信息,直接填充给postcard
postcard.setDestination(routeMeta.getDestination());
postcard.setType(routeMeta.getType());
postcard.setPriority(routeMeta.getPriority());
postcard.setExtra(routeMeta.getExtra());
xxx其它信息的添加
}
}
2.1.3 映射和分组源码分析
刚才提到,Warehouse.java类包含了几个集合类型(主要是Map类型)的静态变量,这几个静态集合用来保存activity和其path对应关系,以及其分组关系等。
首先看如何分组的,Arouter在一层map之外,增加了一层map,我们看WareHouse这个类,里面有两个静态Map:
// Cache route and metas
static Map> groupsIndex = new HashMap<>();
static Map routes = new HashMap<>();
- groupsIndex 保存了group名字到IRouteGroup类的映射,这一层映射就是Arouter新增的一层映射关系。
- routes 保存了路径path到RouteMeta的映射,其中,RouteMeta是目标地址的包装。这一层映射关系跟我门自己方案里的map是一致的,我们路径跳转也是要用这一层来映射。
RouteMeta很简单,就是对跳转目标的封装,其内包含了目标组件的信息,比如Activity的Class。定义如下:
public class 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
private String name;
private Map injectConfig; // Cache inject config.
public RouteMeta() {
}
IRouteGroup是一个接口,只有一个方法loadInto。
public interface IRouteGroup {
/**
* Fill the atlas with routes in group.
*/
void loadInto(Map atlas);
}
至于实现了这个方法的类,就是之前提到的apt生成的以“Arouter$$Group$$分组”命名的类,他们创建了RouteMeta并填充到atlas中,形成Map映射文件,映射路径到具体的类。我们从上面拿一个下来看看:
public class ARouter$$Group$$app implements IRouteGroup {
@Override
public void loadInto(Map atlas) {
atlas.put("/app/main", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/app/main", "app", null, -1, -2147483648));
atlas.put("/app/profile", RouteMeta.build(RouteType.ACTIVITY, ProfileActivity.class, "/app/profile", "app", new java.util.HashMap(){{put("login", 8); }}, -1, -2147483648));
}
}
一目了然,这个类加载的映射关系,都是在一个组内。可以总结出:ARouter通过apt技术,为每个组生成了一个以Arouter$$Group开头的类,这个类负责向atlas这个Hashmap中填充组内路由数据。
那IRouteGroup外面的这一层HashMap呢?也就是groupsIndex呢?经过查阅,这个Map对应的正是以"Arouter$$Root$$组件名"命名的类,也就是我们的ARouter$$Root$$app.java:
public class ARouter$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map> routes) {
routes.put("app", ARouter$$Group$$app.class);
routes.put("login", ARouter$$Group$$login.class);
}
}
这个类实现了IRouteRoot,在loadInto方法中,他将组名和组对应的“组加载器”保存到了routes这个map中。也就是说,这个类将所有的“组加载器”给索引了下来,通过任意一个组名,可以找到对应的“组加载器”。
到此为止,Arouter只是完成了分组工作,而这个分组,也就是之前2.1.2讲到的分组加载的依仗。
下面用一张图来理清他们的关系:
左侧groupsIndex是“组映射”,右侧routes是“路由映射”。Arouter在初始化的时候,通过反射技术,将所有的“组加载器”索引到groupsIndex这个map中,而此时,右侧的routes还是空的。在用户调用navigation()进行跳转的时候,会根据路径提取组名,由组名根据groupsIndex获取到相应组的“组加载器”,由组加载器加载对应组内的路由信息,此时保存全局“路由目标映射的”routes这个map中就保存了刚才组的所有路由映射关系了。同样,当其他组请求时,其他组也会加载组对应的路由映射,这样就实现了整个App运行时,只有用到的组才会加到内存中,没有去过的组就不会加载到内存中,达到了节省内存的目的。