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);
} 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);
}
}
}
logger.info(TAG, "Load root element finished, cost " + (System.currentTimeMillis() - startInit) + " ms.");
if (Warehouse.groupsIndex.size() == 0) {
logger.error(TAG, "No mapping files were found, check your configuration please!");
}
if (ARouter.debuggable()) {
logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size()));
}
} catch (Exception e) {
throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
}
}
细心的读者可能发现了,这里加载了Root分组,却没有加载Group分组,那么为什么要这样设计呢,这两者又有什么关系呢?这里就涉及ARouter的一个加载理念:分组管理,按需加载。
如果一个App有上百个页面的时候,一次性将所有页面都加载到内存中本身对于内存的损耗是非常可怕的,同时对于性能的损耗也是不可忽视的。所以ARouter中提出了分组的概念,ARouter允许某一个模块下有多个分组,所有的分组最终会被一个root节点管理。如下图中所示,假设有4个模块,每个模块下面都有一个root结点,每个root结点都会管理整个模块中的group节点,每个group结点则包含了该分组下的所有页面,也就是说可以按照一定的业务规则或者命名规范把一部分页面聚合成一个分组,每个分组其实就相当于路径中的第一段,而每个模块中都会有一个拦截器节点就是Interceptor结点,除此之外每个模块还会有控制拦截反转的provider结点。
ARouter在初始化的时候只会一次性地加载所有的Root结点,而不会加载任何一个Group结点,这样就会极大地降低初始化时加载结点的数量。那么什么时候加载分组结点呢?其实就是当某一个分组下的某一个页面第一次被访问的时候,整个分组的全部页面都会被加载进去,这就是ARouter的按需加载。其实在整个APP运行的周期中,并不是所有的页面都需要被访问到,可能只有20%的页面能够被访问到,所以这时候使用按需加载的策略就显得非常重要了,这样就会减轻很大的内存压力。
至此初始化阶段就分析结束了,下面看跳转阶段。
我们也从跳转代码说起,通常的跳转代码如下。
ARouter.getInstance().build("/moudlea/activitya").navigation();
...
//代码流程
build("path") -> _ARouter.getInstance().build(path) -> build(String path, String group) -> new Postcard(path, group) ->
Postcard.navigation() -> LogisticsCenter.completion(postcard) -> _navigation(context, postcard, requestCode, callback);
...
_ARouter.getInstance().build(path)源码,构建并返回了一个Postcard结构,该结构的定义是A container that contains the roadmap(包含roadmap的一个容器)。
函数首先去查找PathReplaceService.class接口的实现类,该实现类的作用就是实现 “运行期动态修改路由”,如果找到则利用forString方法修改path。如没有此类则返回build(path, extractGroup(path)),其中extractGroup()是从路径中获取默认的分组信息。最后根据提供的path和group创建一个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 {
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
return build(path, extractGroup(path));
}
}
接下来会调用Postcard.navigation()方法。该函数首先拼装了postcard结构体,并判断是否需要拦截器功能,如果不需要则直接调用_navigation()进行跳转了。
/**
* Use router navigation.
*
* @param context Activity or null.
* @param postcard Route metas
* @param requestCode RequestCode
* @param callback cb
*/
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.
runInMainThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, "There's no route matched!\n" +
" Path = [" + postcard.getPath() + "]\n" +
" Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show();
}
});
}
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);
}
//绿色通道校验 需要拦截处理
if (!postcard.isGreenChannel()) { // It must be run in async thread, maybe interceptor cost too mush time made ANR.
//调用拦截器截面控制器,遍历内存仓库的自定义拦截器,并在异步线程中执行拦截函数
interceptorService.doInterceptions(postcard, new InterceptorCallback() {
/**
* Continue process
*
* @param postcard route meta
*/
@Override
public void onContinue(Postcard postcard) {
_navigation(context, postcard, requestCode, callback);
}
/**
* Interrupt process, pipeline will be destory when this method called.
*
* @param exception Reson of interrupt.
*/
@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;
}
其中两个主要的方法是LogisticsCenter.completion()和_navigation()。
completion()里面填充postcard里面剩下的信息,在这里可以看到,如果该class没有被加载(即上面说的延时加载),在这里会把该模块下的所有类加载起来。最终得到完整的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.
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); // Load route meta.
if (null == groupMeta) {
throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
} else {
// Load route and cache it into memory, then delete from metas.
try {
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
iGroupInstance.loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(postcard.getGroup());
if (ARouter.debuggable()) {
logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
}
} catch (Exception e) {
throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
}
completion(postcard); // Reload
}
} else {
postcard.setDestination(routeMeta.getDestination());
postcard.setType(routeMeta.getType());
postcard.setPriority(routeMeta.getPriority());
postcard.setExtra(routeMeta.getExtra());
Uri rawUri = postcard.getUri();
if (null != rawUri) { // Try to set params into bundle.
Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);
Map<String, Integer> paramsType = routeMeta.getParamsType();
if (MapUtils.isNotEmpty(paramsType)) {
// Set value by its type, just for params which annotation by @Param
for (Map.Entry<String, Integer> params : paramsType.entrySet()) {
setValue(postcard,
params.getValue(),
params.getKey(),
resultMap.get(params.getKey()));
}
// Save params name which need auto inject.
postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));
}
// Save raw uri
postcard.withString(ARouter.RAW_URI, rawUri.toString());
}
}
}
_navigation是真正实现跳转的地方,通过postcard.getDestination()获取目标class的实例(在初始化时已经加载到内存),用大家熟悉的startactivity方法进行了跳转。
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;
case PROVIDER:
return postcard.getProvider();
case BOARDCAST:
case CONTENT_PROVIDER:
case FRAGMENT:
Class fragmentMeta = postcard.getDestination();
try {
Object instance = fragmentMeta.getConstructor().newInstance();
if (instance instanceof Fragment) {
((Fragment) instance).setArguments(postcard.getExtras());
} else if (instance instanceof android.support.v4.app.Fragment) {
((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
}
return instance;
} catch (Exception ex) {
logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
}
case METHOD:
case SERVICE:
default:
return null;
}
return null;
}
至此整个源码分析结束。