ARouter
ARouter 是阿里云出品的Android中间件,负责处理盅跳转页面时的逻辑,简化和优化之前跳转页面的方法。同时他也是组件化的基础之一,实现了模块间的解耦。
ARouter使用
项目的主页有提供ARouter的使用方法,主要就是
- 注解可以跳转的类(Activity,Service,ContentProvider,Fragment等)
- 要跳转页面的时候使用Arouter,类似于
ARouter.getInstance().build("/kotlin/test").withString("name", "老王").withInt("age", 23).navigation();
用法是不能再简单了,猜想是把一个String
与一个Activity
对应起来就可以了,然而实际代码应该比猜想复杂N多倍。下面一起分析一下这个中间,挖掘他的所有信息。
ARouter源码解析
下面分析源码分为几部分
- 跳转页面流程分析
- 在类上和成员上的注解都做了什么
- 解释在ARouter文档上的所有功能特点和典型应用是如何实现的,把这些特点抄到下面来,方便查看,我们会挨个把所有的特点分析一遍
一、功能介绍
- 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
- 支持多模块工程使用
- 支持添加多个拦截器,自定义拦截顺序
- 支持依赖注入,可单独作为依赖注入框架使用
- 支持InstantRun
- 支持MultiDex(Google方案)
- 映射关系按组分类、多级管理,按需初始化
- 支持用户指定全局降级与局部降级策略
- 页面、拦截器、服务等组件均自动注册到框架
- 支持多种方式配置转场动画
- 支持获取Fragment
- 完全支持Kotlin以及混编(配置见文末 其他#5)
- 支持第三方 App 加固(使用 arouter-register 实现自动注册)
二、典型应用
- 从外部URL映射到内部页面,以及参数传递与解析
- 跨模块页面跳转,模块间解耦
- 拦截跳转过程,处理登陆、埋点等逻辑
- 跨模块API调用,通过控制反转来做组件解耦
跳转页面流程分析
使用的方法是
ARouter.getInstance()
.build("/kotlin/test")
.withString("name", "老王")
.withInt("age", 23)
.navigation();
ARouter使用了单例,内部存储了页面的映射表,初始化状态,debug信息等,等一下都会用到的信息(其实在保存在_ARouter单例中),同时也方便在使用的时候直接使用ARouter的静态方法。
实际上除了ARouter之外还有一个_ARouter类,ARouter中几乎是把所有的方法都直接给了_ARouter处理,这一层的转换把_ARouter中复杂的方法转化为ARouter中简单的方法向外暴露,算是一种门面吧,值得学习一下。
build方法不例外地转给了_ARouter
public Postcard build(String path) {
return _ARouter.getInstance().build(path);
}
在_ARouter中构造了PostCard
/**
* 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 {
//这个Service里可以将传入的path处理,换在另外一个,也相当于一个拦截器
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
return build(path, extractGroup(path));
}
}
其中PathReplaceService相关处理逻辑是根据当前的path换成另外的path继续走下面的流程,所以主流程还是build方法,其中有有个extractGroup(path)方法调用,将path中第一部分抽取出来作为group,继续看主线
/**
* Build postcard by path and group
*/
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);
}
}
这里又找了一次PathReplaceService,出于什么目的,很简单,因为这个方法有可能不是上面那个路径调过来的,_ARouter还有两个参数的builde方法,直接调用了这个方法,但是不做任何区分直接再找一次也有点可以优化的空间🙃
返回的结果就是最后构造出来的一个PostCard,这个类上的解释是A container that contains the roadmap.
,包含这一次路由过程中所要的所有信息。PostCard的构造方法没有什么逻辑,就是另外构造了一个bundle放在了PostCard里面备下面使用。
到这里就返回到了最初调用build的地方,再往下是两个withXXX方法,就是向PostCard中放入几个跳转页面要带过去的信息,都是直接放到了bundle里面,当然这不是主线。
继续看下面的navigation方法。
/**
* Navigation to the route with path in postcard.
* No param, will be use application context.
*/
public Object navigation() {
//后面会用application的Context
return navigation(null);
}
给出的例子都是使用没有参数所navigation方法,后面拿application
的Context
,要设置intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
感觉有点误导的感觉,这里正正经经地传个activity过来才应该是最广泛的使用方法。
/**
* Navigation to the route with path in postcard.
*
* @param context Activity and so on.
*/
public Object navigation(Context context) {
//这里没有caback,这个callback有onFound,onLost,onArrival,和onInterrupt方法,都是跳转时的各种回调,单独的跳转降级就是通过这个callback完成的
return navigation(context, null);
}
/**
* Navigation to the route with path in postcard.
*
* @param context Activity and so on.
*/
public Object navigation(Context context, NavigationCallback callback) {
//这里又增加了一个-1的参数,不求不需求forResult
return ARouter.getInstance().navigation(context, this, -1, callback);
}
到这里方法调用就出了PostCard,到了ARouter中,当然又会委托给_ARouter进行真正的业务。
看_ARouter的方法。
/**
* Use router navigation.
*/
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());
if (debuggable()) { // Show friendly tips for user.
Toast...
}
if (null != callback) {
callback.onLost(postcard);
} else { // No callback for this invoke, then we use the global degrade service.
//统一降级逻辑
DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
if (null != degradeService) {
degradeService.onLost(context, postcard);
}
}
return null;
}
if (null != callback) {
callback.onFound(postcard);
}
//主流程,根据是否绿色通道走拦截的逻辑
//这里还有个友情提示: It must be run in async thread, maybe interceptor cost too mush time made ANR.
if (!postcard.isGreenChannel()) {
interceptorService.doInterceptions(postcard, new InterceptorCallback() {
@Override
public void onContinue(Postcard postcard) {
//走完拦截器并通过的,继续走跳转的逻辑
_navigation(context, postcard, requestCode, callback);
}
@Override
public void onInterrupt(Throwable exception) {
if (null != callback) {
callback.onInterrupt(postcard);
}
logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
}
});
} else {
//主流程,绿色通道的主要逻辑,此时所有的信息已经准备完成,下面就是与系统交互进行跳转了
return _navigation(context, postcard, requestCode, callback);
}
return null;
}
主流程没有几名话,先补全路由的信息,走拦截逻辑,然后真正的跳转,其中补全PostCard是重要过程,我们看一下方法详情
/**
* Completion the postcard by route metas
* @param postcard Incomplete postcard, should complete by this method.
*/
public synchronized static void completion(Postcard postcard) {
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
if (null == routeMeta) { // Maybe its does't exist, or didn't load.
//没有找到RouteMeta,检查他所有的组是否还没有加载,如果已经加载,则异常,没有加载去加载
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); // Load route meta.
if (null == groupMeta) {
throw new NoRouteFoundException(...);
} else {
// Load route and cache it into memory, then delete from metas.
try {
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
//加载所在的组,如果加载完成就把这个group从未加载列表中删除
iGroupInstance.loadInto(Warehouse.routes);
//将些组从未加载中移除,防止重复加载,
Warehouse.groupsIndex.remove(postcard.getGroup());
} catch (Exception e) {
throw new HandlerException(...);
}
// 加载了组信息,重新去重新去走补全的逻辑
completion(postcard); // Reload
} else {
将RouteMeta的信息放到PostCard中,
如果是通过uri跳转的话再将路径中的信息解析出来放到postCard的bound中
下面又对两种类型的跳转做的特殊处理
1. PROVIDER
从仓库中找到provider的实例,疳赋值给postCard
设置绿色通道,防止拦截
2. FRAGMENT
设置绿色通道,防止拦截
}
}
此时post的信息已经完全了,我们路过拦截的逻辑,直接看下面真正的跳转方法,感觉是没有必要把代码再拿出来,就是根据类型区分了一下,activity就直接new Intent进行跳转,如果是Privider,Fragment就返回实例。
到这里基本完成了一次跳转页面所走的全部路径。并没有高深难懂的逻辑,一个比较好玩的就是PathReplaceService,DegradeService,SerializationService等都是通过注册一个Service完成的,这就大大增加了这个框架的灵活性,而且框架向外提供的这个功能,自己内部已经先用起来了,这个也是挺有意思的。
ARouter中的注解有什么用,是怎么起作用的
@Route
作用:注解一个类,这个类就可以通过ARouter找到使用
Route主要有两个属性,path和group,在RouteProcessor中处理这个注解,在注解处理的方法中会根据注解的类型创建上面使用过的RouteMeta
for (Element element : routeElements) {
TypeMirror tm = element.asType();
Route route = element.getAnnotation(Route.class);
RouteMeta routeMeta = null;
if (types.isSubtype(tm, type_Activity)) { // Activity
logger.info(">>> Found activity route: " + tm.toString() + " <<<");
// Get all fields annotation by @Autowired
Map<String, Integer> paramsType = new HashMap<>();
for (Element field : element.getEnclosedElements()) {
if (field.getKind().isField() && field.getAnnotation(Autowired.class) != null && !types.isSubtype(field.asType(), iProvider)) {
// It must be field, then it has annotation, but it not be provider.
Autowired paramConfig = field.getAnnotation(Autowired.class);
paramsType.put(StringUtils.isEmpty(paramConfig.name()) ? field.getSimpleName().toString() : paramConfig.name(), typeUtils.typeExchange(field));
}
}
routeMeta = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType);
} else if (types.isSubtype(tm, iProvider)) { // IProvider
logger.info(">>> Found provider route: " + tm.toString() + " <<<");
routeMeta = new RouteMeta(route, element, RouteType.PROVIDER, null);
} else if (types.isSubtype(tm, type_Service)) { // Service
logger.info(">>> Found service route: " + tm.toString() + " <<<");
routeMeta = new RouteMeta(route, element, RouteType.parse(SERVICE), null);
} else if (types.isSubtype(tm, fragmentTm) || types.isSubtype(tm, fragmentTmV4)) {
logger.info(">>> Found fragment route: " + tm.toString() + " <<<");
routeMeta = new RouteMeta(route, element, RouteType.parse(FRAGMENT), null);
} else {
throw new RuntimeException("ARouter::Compiler >>> Found unsupported class type, type = [" + types.toString() + "].");
}
categories(routeMeta);
// if (StringUtils.isEmpty(moduleName)) { // Hasn't generate the module name.
// moduleName = ModuleUtils.generateModuleName(element, logger);
// }
}
分别构建出来RouteMeta,还构建出来一个分组的信息,下面将这些信息构建两个java文件。类似于这样
public class ARouter$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("service", ARouter$$Group$$service.class);
routes.put("test", ARouter$$Group$$test.class);
}
}
rootInfo,存放所有的组的信息,就是上面找不到RouteMeta的时候会从这里找到对应的组,再找到组信息对应的类,然后加载
还有一个组详细信息的类,类似开这样
public class ARouter$$Group$$test implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(){{put("key1", 8); }}, -1, -2147483648));
atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648));
atlas.put("/test/fragment", RouteMeta.build(RouteType.FRAGMENT, BlankFragment.class, "/test/fragment", "test", null, -1, -2147483648));
}
}
加载的时候把这些信息加载到map中,以后跳转使用
@Interceptor
作用:设置全局跳转的拦截器,可以设置优先级
处理注解基本和和@Route一样,得到类,得到属性,javapoet写出一个类似于这样的类:
public class ARouter$$Interceptors$$app implements IInterceptorGroup {
@Override
public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
interceptors.put(7, Test1Interceptor.class);
}
}
这个map是个特别的map,根据key的值自动排序,如果key重复会异常,也也是这个拦截器可以按优先级排序的原因
@Autowired
作用:自动装配,注解成员后,可以自动从Intent中解出数据并赋值给变量
实现也很相似,找到被注解的成员,生成一个helper,在需要将intent的数据解出来的时候使用helper的inject方法,ARouter又使用了一个AutowiredService专门做这个事,只要将要注入的类传过来就可以了
@Override
public void autowire(Object instance) {
String className = instance.getClass().getName();
try {
if (!blackList.contains(className)) {
// 只有一个inject方法
ISyringe autowiredHelper = classCache.get(className);
if (null == autowiredHelper) { // No cache.
autowiredHelper = (ISyringe) Class.forName(instance.getClass().getName() + SUFFIX_AUTOWIRED).getConstructor().newInstance();
}
// autowiredHelper就是根据注解生成的特定helper
autowiredHelper.inject(instance);
classCache.put(className, autowiredHelper);
}
} catch (Exception ex) {
blackList.add(className); // This instance need not autowired.
}
}
javaPoet实在是有点烦琐,真的不愿把他的代码拿来。有意的同学可以直接去arouter查看
解释所有官方列举的特点
- 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
在Manifast页面中注册了两个filter
<intent-filter>
<data
android:host="m.aliyun.com"
android:scheme="arouter"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
</intent-filter>
<!-- App Links -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:host="m.aliyun.com"
android:scheme="http"/>
<data
android:host="m.aliyun.com"
android:scheme="https"/>
</intent-filter>
第一个是处理特定的协议,如果协议中有arouter,则会用这个Acitity处理
还有一个是处理applink的,在网页上点击特定连接,也是在这个Activity中处理
在这个Activity主要代码就两行:
Uri uri = getIntent().getData();
ARouter.getInstance().build(uri).navigation(this, new NavCallback() {
@Override
public void onArrival(Postcard postcard) {
finish();
}
});
这里使用的是一个URI,在构造PostCard的时候会将URI后面挂的参数直接转化到bound中去,也就解释了第一个特征。
- 支持多模块工程使用
各模块只是依赖一个String,编译时会扫描整个所有的工程,所以直接就支持多模块的工程。但是有个问题就是别的页面要跳转的时候都要将字符串写死进去,如果定义常量的话会出现多个模块依赖一个常量类的情况。
- 支持添加多个拦截器,自定义拦截顺序
如果设置了这个优先级别,生成的java代码中会将这个优先级做为key,放到传过来的一个容器中,而这个窗口的定义在com.alibaba.android.arouter.core.Warehouse
中,是一个UniqueKeyTreeMap,保证key是唯一的,并且按key进行排序
这里也就解释了自定义拦截顺序的特点
- 支持依赖注入,可单独作为依赖注入框架使用
不知道讲的是什么。。
navigation方法返回的是一个Object
- 支持InstantRun
- 支持MultiDex(Google方案)
这里看到源码中找了一个所有的的dex文件,再从这些所有的dex中查找要找的router的类,应该就是处理这个问题。等于没有说。。就简单看一下调用链吧
Arouter.init() -> LogisticsCenter.init(mContext, executor)
-> ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);//这里就是是指定的ali的包名,就是生成的那些类包名
-> ClassUtils.getSourcePaths(context);//从多dex下找类
-> ClassUtils.tryLoadInstantRunDexFile(applicationInfo)//instantRun
-> 找到所有生成的注册router的类
- 映射关系按组分类、多级管理,按需初始化
路由的注解中可以注册一个group信息,如果不定义这个group信息,arouter会拿路径中的第一段做为group。处理注解的时候会生成两组信息,第一是组信息,其中有所有group的信息,每一组都会指向一个描述这个组中所有路径的类。
初始化时仅仅加载了组的信息,并没有加载每一组内的所有路由,使用路由时会先查找有没有这个路由信息,如果没有的话就去加载这一组所有的路由。做到了按需初始化。
这个过程上面路由过程已经用代码分析过了。
- 支持用户指定全局降级与局部降级策略
每一次使用路由时可以传入一个callback,作为单次路由失败的降级策略,其实也不仅仅是降级策略,callback提供了多个回调方法使用:
public interface NavigationCallback {
//找到路由
void onFound(Postcard postcard);
//没有找到,降级吧
void onLost(Postcard postcard);
//向android发出了startActivity的请求
void onArrival(Postcard postcard);
//使用拦截器时
void onInterrupt(Postcard postcard);
}
也可以注册一个IProvider,用来处理所有的降级策略。
- 页面、拦截器、服务等组件均自动注册到框架
使用注解,编译期处理,运行时直接无反射运行(多dex什么的还是要反射)
- 支持多种方式配置转场动画
支持,无特殊
- 支持获取Fragment
navigate的时候支持返回一个fragment,只要注册了路由的fragment,都可以通过路由来得到实例。
- 完全支持Kotlin以及混编(配置见文末 其他#5)
- 支持第三方 App 加固(使用 arouter-register 实现自动注册)