ARouter是阿里巴巴开源的组件化架构框架,能帮助组件化项目中实现不同模块间的跳转,以及AOP面向切面的编程,能对页面跳转的过程进行很好的干预。本文将从源码角度入手,对该框架的原理进行分析。
项目集成时会集成两个Library,也对应了ARouter的两个阶段。arouter-compiler是用于编译期的,而arouter-api是面向运行期的。下面就从这两个阶段开始讲起。
dependencies {
// Replace with the latest version
compile 'com.alibaba:arouter-api:?'
annotationProcessor 'com.alibaba:arouter-compiler:?'
...
}
编译阶段
ARouter是可以自动注册页面映射关系的,在每个目标页面上使用注解来标注一些参数,比方Path标注其路径。使用注解时会遇到的第一个问题就是需要找到处理注解的时机,如果在运行期处理注解则会大量地运用反射,而这在软件开发中是非常不合适的,因为反射本身就存在性能问题,如果大量地使用反射会严重影响APP的用户体验,而又因为路由框架是非常基础的框架,所以大量使用反射也会使得跳转流程的用户体验非常差。所以ARouter最终使用的方式是在编译期处理被注解的类,这样就可以做到在运行中尽可能不使用反射,这就是注解处理器的作用。
页面注册的整个流程首先通过注解处理器扫出被标注的类文件,然后按照不同种类的源文件进行分类,分别生成固定格式的类文件,命名规则是工程名ARouter+$$ +xxx+$ $ +模块名。可以看出这里面包含了Group、Interceptor、Providers以及Root。这部分完成之后就意味着编译期的工作已经结束了,之后的初始化其实是发生在运行期的,在运行期只需要通过固定的包名来加载映射文件就可以了,因为注解是由开发者自己完成的,所以了解其中的规则,就可以在使用的时候利用相应的规则反向地提取出来。这就是页面自动注册的整个流程。
自动生成的类:
编译期流程图:
运行阶段
运行阶段也分为两部分来分析,初始化和页面跳转。
初始化
// Initialize the SDK
ARouter.init(mApplication); // As early as possible, it is recommended to initialize in the Application
//代码流程
...
ARouter.init(mApplication) -> _ARouter.init(application) -> LogisticsCenter.init(mContext, executor)
...
沿着代码流程init会走到LogisticsCenter.init(mContext, executor),该函数通过ClassUtils.getFileNameByPackageName遍历包名"com.alibaba.android.arouter.routes"下的所有class存入routerMap中(即把所有编译期自动生成的class读取出来),然后通过for循环把这些class按照不同类型(Root,Interceptors, Providers)进行处理。通过class的路径,利用Class.forName的方式得到class实例,然后调用该class的loadInto方法把该class关联到Warehouse(Warehouse是整个项目的仓库,里面存有所有class的映射关系)的相应的结构体中,这样所有自动生成的映射关系就存储在内存中了,以便于后续查找时使用。
/**
* LogisticsCenter init, load all metas in memory. Demand initialization
*/
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
mContext = context;
executor = tpe;
try {
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
Set<String> routerMap;
// It will rebuild router map every times when debuggable.
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
// These class was generated by arouter-compiler.
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
if (!routerMap.isEmpty()) {
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
}
PackageUtils.updateVersion(context); // Save new version name when router map update finishes.
} else {
logger.info(TAG, "Load router map from cache.");
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}
logger.info(TAG, "Find router map finished, map size = " + routerMap.size() + ", cost " + (System.currentTimeMillis() - startInit) + " ms.");
startInit = System.currentTimeMillis();
for (String className : routerMap) {
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);
} e