前言
ARouter是阿里技术团队开源的组件化框架,用于组件化项目中实现跨模块调用。我会从两个方面对ARouter框架源码进行分析:运行期源码和编译期源码。编译期间,ARouter内部的注解处理器会根据相关注解进行辅助代码生成,这些生成的代码是ARrouter之所以能够提供跨模块调用的关键;运行期间,再借助于这些编译器产物来实现跨模块调用。本篇先进行运行时源码的分析,引出问题,读者先可以思考一下,编译器产物的实现逻辑,我会在下一篇中和大家一起进行分析。
demo项目结构
我们以ARouter提供的demo为例来进行ARouter源码的分析,在分析源码之前,我们先了解一下demo的架构组成,有助于我们对ARouter进行更全面的了解。
编译期产物
今天的重点是无参数传递时的Activity页面跳转,我们先了解一下编译期间产生的辅助代码。
module-java模块
module-java模块中被注解的类以及其参数配置很多,我们以Test2Activity作为分析的入口。
scala
@Route(path = "/test/activity2")
public class Test2Activity extends AppCompatActivity {
group
首先我们要知道一点,ARouter会按照module名为每个mudule生成一个名为ARouter$$Root$$modulejava
格式的类,其中modulejava表示当前模块名。如module-java模块生成的类为:
ruby
public class ARouter$$Root$$modulejava implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("m2", ARouter$$Group$$m2.class);
routes.put("module", ARouter$$Group$$module.class);
routes.put("test", ARouter$$Group$$test.class);
routes.put("yourservicegroupname", ARouter$$Group$$yourservicegroupname.class);
}
}
也就是说,有几个模块(配置了ARouter)就会生成几个ARouter$$Root$$xxx
类,从上边的示例可以看出,这个类是用来管理本模块中定义的所有group的,module-java模块中定义了四个group,分别是m2、module、test、yourservicegroupname。
借助这个辅助类,就可以通过group名找到该group的管理类ARouter$$Group$$xxx
,ARouter$$Group$$xxx
内部管理了该分组下的所有path信息,接下来我们看看这些path信息。
path
继续分析编译期的第二个产物,也就是ARouter$$Root$$modulejava
map映射中存储的value-Class<? extends IRouteGroup>
,如上边test分组对应的ARouter$$Group$$test.class
,它内部存储的是本分组下所有path的映射关系。
less
public class ARouter$$Group$$test implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/test/activity1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/activity1", "test", new java.util.HashMap<String, Integer>(){{put("ser", 9); put("ch", 5); put("fl", 6); put("dou", 7); put("boy", 0); put("url", 8); put("pac", 10); put("obj", 11); put("name", 8); put("objList", 11); put("map", 11); put("age", 3); put("height", 3); }}, -1, -2147483648));
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/activity3", RouteMeta.build(RouteType.ACTIVITY, Test3Activity.class, "/test/activity3", "test", new java.util.HashMap<String, Integer>(){{put("name", 8); 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/fragment", RouteMeta.build(RouteType.FRAGMENT, BlankFragment.class, "/test/fragment", "test", new java.util.HashMap<String, Integer>(){{put("ser", 9); put("pac", 10); put("ch", 5); put("obj", 11); put("fl", 6); put("name", 8); put("dou", 7); put("boy", 0); put("objList", 11); put("map", 11); put("age", 3); put("height", 3); }}, -1, -2147483648));
atlas.put("/test/webview", RouteMeta.build(RouteType.ACTIVITY, TestWebview.class, "/test/webview", "test", null, -1, -2147483648));
}
}
可以看到,方法体中执行时,是以path为key,以被注解类的相关信息封装出来的RouteMeta为value存入map中,有了这个类,就可以根据定义的path来找到被注解的类的相关信息。
group + path
了解了group和path的辅助类生成的形式之后,你是不是也能猜到ARouter跨模块调用的大概实现了?如A、B两个没有依赖关系的模块需要跨模块调用,A想要打开B中的某Activity页面,只需要在该Activity上添加@Route注解,指定该Activity的group和path,如@Route(path = "/login/loginApi")
然后编译过程中,注解处理器会根据B模块名生成该模块下的group管理类ARouter$$Root$$B
,有了ARouter$$Root$$B
再传入group名(login),就可以拿到本分组下的所有path信息管理类,ARouter$$Group$$login
,ARouter$$Group$$login
内存储的又是所有path的合集,再通过指定path就可以找到该path对应的被注解的类的信息。所以,即使没有依赖关系的两个模块,在ARouter的帮助下,也能实现调用到B模块中类的效果。
源码分析
了解了编译器产物后,接下来我们开始从下面两个方法入手,进行Activity页面跳转(无参数)源码分析。
- ARouter.init(getApplication())
- ARouter.getInstance().build("/test/activity2").navigation()
init
init是ARouter的初始化方法,跟进这个方法,会进入到LogisticsCenter的init方法中。
java
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
//通过插件加载
loadRouterMap();
if (registerByPlugin) {
//如果是通过插件注册,则不需要做任何处理,registerByPlugin默认为false,那么它是
//在哪被赋值的?loadRouterMap
} else {
//非插件加载
}
}
ARouter的初始化有两种方式,一种是通过插件在编译期生成代码,另一种是运行期遍历所有dex文件解析出注解类。我们先看看loadRouterMap方法做了什么。
插件注册
csharp
/**
* arouter-auto-register plugin will generate code inside this method
* call this method to register all Routers, Interceptors and Providers
*/
private static void loadRouterMap() {
registerByPlugin = false;
// auto generate register code by gradle plugin: arouter-auto-register
// looks like below:
// registerRouteRoot(new ARouter..Root..modulejava());
// registerRouteRoot(new ARouter..Root..modulekotlin());
}
可以看到这里除了将registerByPlugin赋值为false之外,再无任何代码,但是从方法注释上可以看出,ARouter插件会通过自动生成代码插入到这个方法中来注册所有的Routers, Interceptors and Providers,生成代码的形式如方法体中注释那样,通过插入代码调用registerRouteRoot、registerProvider、registerInterceptor方法进行注册(registerProvider、registerInterceptor注释上没体现出来,但实际会有),而传入的参数就是上边提到过的编译期产物如ARouter$$Root$$modulejava
。
scss
private static void registerRouteRoot(IRouteRoot routeRoot) {
markRegisteredByPlugin();
if (routeRoot != null) {
routeRoot.loadInto(Warehouse.groupsIndex);
}
}
registerRouteRoot被调用后会先调用markRegisteredByPlugin将registerByPlugin变量赋值为true,终于找到registerByPlugin赋值的地方了。紧接着将Warehouse.groupsIndex这个静态map作为参数,调用IRouteRoot实现类的loadInto方法,我们以ARouter$$Root$$modulejava
辅助类的实现为例,看下这个loadInto方法的实现,它会将modulejava模块下所有分组信息存入Warehouse.groupsIndex这个map集合中。
ruby
public class ARouter$$Root$$modulejava implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("m2", ARouter$$Group$$m2.class);
routes.put("module", ARouter$$Group$$module.class);
routes.put("test", ARouter$$Group$$test.class);
routes.put("yourservicegroupname", ARouter$$Group$$yourservicegroupname.class);
}
}
也就是说,所有的Routers, Interceptors and Providers分组信息都会被一次性在loadRouterMap方法中被存入到Warehouse.groupsIndex、Warehouse.interceptorsIndex、Warehouse.providersIndex等map集合中,这就是初始化时做的准备工作。
代码注册
接下来我们再看下非插件类型的注册。前边在说插件类型初始化时,是通过调用registerRouteRoot方法直接创建各模块下的形如ARouter$$Root$$modulejava
的对象,将其new出来存入map。插件当然是可以直接在编译期获取到各模块下生成的这个对象,但是若不使用插件生成的方式,又该怎么拿到这些类并创建对象呢?ARouter的做法就是遍历所有dex文件。
scss
Set<String> routerMap;
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
//debug状态下或者新版本时,需要重新扫描dex并将所有包名以"com.alibaba.android.arouter.routes"
//开头的类获取到,getFileNameByPackageName方法实现就不再深入了
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
if (!routerMap.isEmpty()) {
//拿到一次后,以json字符串的形式保存到SharedPreferences,下次同一版本不再进行扫描,因为扫描本身是很耗时的
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
}
PackageUtils.updateVersion(context);
} else {
//不需要重新扫描的情况,直接从sp中读取
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}
//逻辑走到这里,routerMap中存储的全部是IRouteRoot、IInterceptorGroup、IProviderGroup类型的类,
//分别通过反射的方式将对象创建出来,并调用loadInto填充三个map集合
for (String className : routerMap) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
}
}
方法的最后,拿到各个IRouteRoot、IInterceptorGroup、IProviderGroup实现类后,通过反射创建对象,并调用各自的loadInto方法,这样一来,初始化完成后,三个静态map:Warehouse.groupsIndex、Warehouse.interceptorsIndex、Warehouse.providersIndex中就有信息了。
至此,我们就把ARouter初始化的逻辑分析完了。接下来开始看看,它是怎么借助这些准备好的信息实现跨模块调用的,以下面一行代码调用为例。
scss
ARouter.getInstance().build("/test/activity2").navigation();
getInstance
创建出ARouter单例。
csharp
public static ARouter getInstance() {
if (!hasInit) {
throw new InitException("ARouter::Init::Invoke init(context) first!");
} else {
if (instance == null) {
synchronized (ARouter.class) {
if (instance == null) {
instance = new ARouter();
}
}
}
return instance;
}
}
build
typescript
public Postcard build(String path) {
return _ARouter.getInstance().build(path);
}
ARouter的build方法会进入_ARouter的build方法。
scss
protected Postcard build(String path) {
//这里有一个获取PathReplaceService的逻辑,与本次分析无关,我们先不关注,默认为null
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
//extractGroup从path上解析出group,如"/test/activity2"的group就是test
return build(path, extractGroup(path), true);
}
同样,这里的PathReplaceService跳过,默认为null。可以看到,最终build方法就是构建了一个Postcard,并将group和path两个值保存为其成员变量。
typescript
protected Postcard build(String path, String group, Boolean afterReplace) {
......
return new Postcard(path, group);
}
navigation
因为build方法的返回值是Postcard对象,那么navigation自然也是Postcard的方法了。不断跟进此方法,你会发现最终又来到了_ARouter中,_ARouter的navigation方法中,将postcard对象传了进去,所以_ARouter中也就包含了postcard的信息。
typescript
public Object navigation(Context mContext, Postcard postcard, int requestCode, NavigationCallback callback) {
return _ARouter.getInstance().navigation(mContext, postcard, requestCode, callback);
}
_ARouter的navigation方法,这里的内容较多,我们只关注和本次分析相关的,去掉一些非主线代码。
java
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
try {
//填充postcard信息
LogisticsCenter.completion(postcard);
} catch (NoRouteFoundException ex) {
return null;
}
//这里是一个拦截器机制,跟本次分析无关,看完LogisticsCenter.completion直接进入它的onContinue方法中
interceptorService.doInterceptions(postcard, new InterceptorCallback() {
@Override
public void onContinue(Postcard postcard) {
_navigation(postcard, requestCode, callback);
}
});
return null;
}
LogisticsCenter.completion,看看这个方法做了什么。
java
public synchronized static void completion(Postcard postcard) {
if (null == postcard) {
throw new NoRouteFoundException(TAG + "No postcard!");
}
//首先根据传入的路径获取到RouteMeta对象。RouteMeta对象是什么?
//还记得我们前面分析path生成的辅助类时提到的下面这一行代码,
//RouteMeta就是封装了被@Route注解的类的信息。
//如RouteMeta.build(RouteType.ACTIVITY, TestModule2Activity.class, "/module/2", "m2", null, -1, -2147483648)
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
//拿到这个信息后,又分两种情况处理,为空和不为空,为空的情况不在我们今天分析的范围内,所以就不考虑了,我们来看非空的逻辑。
if (null == routeMeta) {
......
} else {
//给postcard填充信息,将routeMeta保存的目标信息设置进去
postcard.setDestination(routeMeta.getDestination());
postcard.setType(routeMeta.getType());
postcard.setPriority(routeMeta.getPriority());
postcard.setExtra(routeMeta.getExtra());
//本次分析不涉及uri,所以直接跳过
Uri rawUri = postcard.getUri();
if (null != rawUri) { // Try to set params into bundle.
......
}
//这里也都不满足,因为我们的routeMeta.getType()是RouteType.ACTIVITY,跳过
switch (routeMeta.getType()) {
case PROVIDER:
break;
case FRAGMENT:
default:
break;
}
}
}
所以这个方法对于本次分析来说,也只是填充Postcard信息,前边Postcard先存储了group和path信息,现在又将目标类的信息存入进去,可见此类的命名为"明信片"还是很有意思的,明信片自然要包含目的地的信息,我们看一下代码执行到这里,Postcard已经包含了哪些信息,先有一个直观的了解。
继续回到navigation方法中,我们继续来看onContinue方法的执行,来到这里,才算是真正要开始进行页面跳转了,方法体中我们只保留了ACTIVITY类型,其他类型不在本次分析之内。
java
private Object _navigation(final Postcard postcard, final int requestCode, final NavigationCallback callback) {
final Context currentContext = postcard.getContext();
switch (postcard.getType()) {
case ACTIVITY:
// 首先构建intent对象,从上边的截图中我们可以看到,getDestination得到的
//是目标路径Test2Activity的class对象,正好用来构建intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
//传递的intent参数,本次我们分析的是不带参数的跳转,所以这个暂不关注
intent.putExtras(postcard.getExtras());
// 设置flag类型
int flags = postcard.getFlags();
if (0 != flags) {
intent.setFlags(flags);
}
// 如果currentContext不是Activity的上下文context对象,就设置FLAG_ACTIVITY_NEW_TASK
if (!(currentContext instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
// 暂不关注
String action = postcard.getAction();
if (!TextUtils.isEmpty(action)) {
intent.setAction(action);
}
// 切换到主线程执行startActivity,看到这里是不是觉得很奇妙,原来ARouter
//最终还是使用了最常规的activity打开方式进行跳转的
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode, currentContext, intent, postcard, callback);
}
});
break;
}
return null;
}
总结
至此我们就已经将ARouter跨模块实现页面跳转的逻辑分析完了,我们回顾一下ARouter到底是怎么做到的?我们在非组件化开发中,页面跳转只要拿到目标页面Activity的class对象,就可以直接调用startActivity进行跳转,我们也发现ARouter最终的实现也是如此,所以这个框架所做的逻辑就很清晰了,一句话形容,整个框架所做的核心就是在协助我们去拿到目标Activity页面的class对象。知道这一点之后,我们还要知道它是怎么拿到的?ARouter通过编译器生成辅助代码的方式来实现,辅助代码中保存好开发者定义的path和目标页面的映射关系,这样一来,开发者就可以通过path拿到目标页面的信息,实现跳转。