本篇文章默认读者已经会使用 ARouter,我们通过使用代码及源码详解其原理。
ARouter是一个路由框架。使用AndroidStudio 开发环境,我们知道在Coding阶段,没有依赖关系的Module,它们的类是无法被对方直接引用。这是AS 的项目组织方式。但是不管所属哪一个Moudle的代码,最终都会打包进apk,在软件运行时,我们可以拿得所有的类。
现在有一个问题:两个没有依赖关系的Moudle,它们之间的页面该如何跳转呢?
既然Coding阶段无法拿到对象,那我们就运行时获取到进行页面跳转所需的信息。
假如有 module1 和 module2,之间没有依赖关系。AActivity(包名:com.test) 属于 moudle1;BActivity(包名:con.test) 属于 module2。从 A 页面跳转到 B 页面,我们可以使用如下方式:
startActivity(new Intent(this, Class.forName("com.test.BAcctivity")))
或者
Intent i = new Intent();
i.setComponent(new ComponentName("com.test", "com.test.BActivity"));
startActivity(i);
ARouter的解决方案是,全局维护一个Map集合,key是一段用户定义的字符串(本例中的a,b),value是对应的类对象。类似这种(概念上):
Map map = new HashMap();
map.put("a", AActivity.class);
map.put("b", BActivity.class);
我们在开发阶段只用使用a,b就可以代表 AActivity,BActivity,然后在运行阶段通过映射关系进行转换后创建 Intent 对象进行跳转。(Intent(Context packageContext, Class cls)
)
使用的时候类似(概念上):
ARouter.getInstance().build("b").navigation();
接下来探寻一下,ARouter的具体实现。看ARouter的源码能学到:框架的设计、APT、Gradle-Plugin。
一、项目目录结构概述
arouter-annotation
:定义注解类
arouter-api
: 核心库,封装了开发人员使用的API
arouter-compiler
:定义编译时注解处理器,通过扫描项目,生成辅助Java类
arouter-gradle-plugin
:gradle插件(可选),用于在编译时通过Transform处理字节码
二、注解
/**
* Mark a page can be route by router.
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
/**
* Path of route
*/
String path();
/**
* Used to merger routes, the group name MUST BE USE THE COMMON WORDS !!!
*/
String group() default "";
...
}
Route
注解类,是一个标记,被标记的实体能够被路由进行分发,例如:Activity。 path就是将来路由表中的 "key"
,group 代表由Route标记的实体 所属的路由组。
路由组的概念:把所有的路由表信息,分成N组,这样在加载路由表的时候,可以分组加载,提高效率。即按需加载,有延迟加载的特性,同时又可以减少路由表加载时间。
三、注解处理器
项目编译时通过扫描注解,根据这些有用的信息,动态生成辅助的Java类文件.java
。RouteProcessor.java
注解处理器就是为了处理那些被 Route
标注的类。因为需要动态生成 .java
文件,项目中使用了 javapoet
作为辅助工具,以提高生产效率。
注解处理器生成的 “辅助的文件”,也可以我们自己手动编写。但通常情况是,这些java文件中的逻辑单一简单,且有一定的共同特性(这需要大家在开发过程中去观察),所以通过注解信息去生成相关的
.java
可以大大提高生产力。 ARouter、ButterKnife 中大量使用了此技术。
这里不打算对 RouteProcessor.java
的细节进行介绍,有兴趣的可以去看代码。过度讲解细节,会让主线模糊不清。(当然这些细节也很重要,可以考虑再开一节来深入细节)
我们结合具体的代码,看一下帮助我们生成了哪些 “辅助类”
新建一个 空的Android项目,只有一个默认的module:app
,创建两个页面:FirstActivity
和 SecondActivity
(这里省略 ARouter 依赖的引用以及库的初始化)
package com.daddyno1.projectmoduledemo;
...
@Route(path = "/group1/first")
public class FirstActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
}
}
package com.daddyno1.projectmoduledemo;
...
@Route(path = "/group2/second")
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main3);
}
}
这两个 Activvity 的path设定,特意分属两个路由组,group1 和 group2 (会截取path 前两个/ 的字符串作为组)
编译一下,我们可以看看生成了哪些辅助文件。(辅助文件的位置 在 app/build/generated/ap_generated_sources
下),生成的路由辅助文件包名:com.alibaba.android.arouter.routes
ARouter$$Root$$xxx
:xxx 为本module的名称。本例中module名称是app
,此类记录了 app
中所有路由组信息。
ARouter$$Group$$xxx
:xxx为本module中路由组的名称。本例中共有2个分组:group1 和 group2。此类记录了 某个路由组中 所有的路由表信息。
package com.alibaba.android.arouter.routes;
...
/**
* DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map> routes) {
routes.put("group1", ARouter$$Group$$group1.class);
routes.put("group2", ARouter$$Group$$group2.class);
}
}
package com.alibaba.android.arouter.routes;
...
/**
* DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$group1 implements IRouteGroup {
@Override
public void loadInto(Map atlas) {
atlas.put("/group1/first", RouteMeta.build(RouteType.ACTIVITY, FirstActivity.class, "/group1/first", "group1", new java.util.HashMap(){{put("paramInt", 3); }}, -1, -2147483648));
}
}
package com.alibaba.android.arouter.routes;
...
/**
* DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$group2 implements IRouteGroup {
@Override
public void loadInto(Map atlas) {
atlas.put("/group2/second", RouteMeta.build(RouteType.ACTIVITY, SecondActivity.class, "/group2/second", "group2", null, -1, -2147483648));
}
}
根据代码我们可以看到 ARouter$$Group$$xxx
中路由表信息集合的 key
就是 之前用@Route
注解的 path 参数,如 /group1/first 和 /group/second。 value
就是一个 RouteMeta 对象,即路由的元数据,此对象记录了本次路由映射需要的最基本必要的信息。
public class RouteMeta {
...
private RouteType type; // Type of route
private Class destination; // Destination
private String path; // Path of route
private String group; // Group of route
...
至此,至此我们了解到这些辅助文件的生成规则。
四、核心库的设计
接下来就是核心库的部分,之前的动态生成了那么多 “辅助类”,现在看一下该怎么使用。
4.1 初始化
ARouter.init(this);
在Application中调用 init 方法进行初始化路由。
public final class ARouter {
/**
* 初始化。 在使用路由之前必须先调用
*/
public static void init(Application application) {
if (!hasInit) {
...
hasInit = _ARouter.init(application);
...
}
}
}
final class _ARouter {
protected static synchronized boolean init(Application application) {
...
LogisticsCenter.init(mContext, executor);
...
}
}
以上就是最基本的 门面设计模式 的应用。核心的初始化工作在LogisticsCenter
中
public class LogisticsCenter {
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
Set routerMap;
// 遍历dex文件,找到 com.alibaba.android.arouter.routes 下被 arouter-compiler 创建的辅助类
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
...
for (String className : routerMap) {
//com.alibaba.android.arouter.routes.ARouter$$Root
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);
}
}
}
}
以上代码是简化版本,核心逻辑是扫描dex文件中 APT 生成的辅助类,因为本篇讲的是 Activity 跳转,所以会忽略其他部分的内容。类的全类名 以com.alibaba.android.arouter.routes.ARouter$$Root
开头的,将会被通过反射构建对象实体,然后调用其 loadInto
方法把路由分组信息注入到Warehouse.groupsIndex
。 Warehouse 是缓存了所有路由信息的工具类。
class Warehouse {
//路由组信息缓存
static Map> groupsIndex = new HashMap<>();
//路由表信息缓存
static Map routes = new HashMap<>();
...
}
经过 init
初始化以后, Warehouse.groupsIndex
缓存了项目中所有路由组信息:
1、这里让大家思考一个问题,既然路由初始化时,会扫描 dex 文件,获取指定包下的类信息,假如APK被加固了, 正确的dex文件被隐藏,就无法找到相关的辅助类,该如何处理?
2、LogisticsCenter#init
初始化中,也使用到了缓存,从dex文件扫描获取到的类信息会缓存在SP里,下次就直接从SP里获取了,不必扫描 dex文件。而且这里还有版本控制,如果是新版本的话,就会重新扫描dex文件,之后更新SP里的信息。
4.2 使用路由进行页面跳转
ARouter.getInstance().build("/group1/first").navigation();
这行代码就是 跳转到 /group1/first
在路由表中所标识的真实地址: FirstActivity
(参考之前APT 生成的辅助类)。其实很好想明白怎么实现,执行 navigation
方法时,根据路由表的信息,获取跳转的目标地址:FirstActivity.class
,然后构造 Intent,执行startActivity方法,即可完成跳转。
在分析代码之前,我们先来了解一个类 Postcard
,中文名字叫明信片,继承自 RouteMeta,除了包含最基本的路由信息外,还有一些其他信息,比如参数传递的 Bundle
对象、动画、IntentFlag等等,这个类是进行路由分发的基本数据组成单元。(注意区分 Postcard 和 RouteMeta)
/**
* A container that contains the roadmap.
*/
public final class Postcard extends RouteMeta {
// Base
private Uri uri;
private Object tag; // A tag prepare for some thing wrong.
private Bundle mBundle; // Data to transform
private int flags = -1; // Flags of route
private int timeout = 300; // Navigation timeout, TimeUnit.Second
private IProvider provider; // It will be set value, if this postcard was provider.
private boolean greenChannel;
private SerializationService serializationService;
// Animation
private Bundle optionsCompat; // The transition animation of activity
private int enterAnim;
private int exitAnim;
}
ARouter.getInstance().build("/group1/first")
此时就会构造一个 Postcard对象,此时对象属性只有 path
和 group
是确定的,之后初始化Postcard对象(LogisticsCenter#completion
),然后使用 Postcard
进行导航,如果 RouteType
是 ACTIVITY
,则会构建Intent,调用ActivityCompat#startActivity
启动页面:
final class _ARouter {
...
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;
}
if (null != callback) {
callback.onFound(postcard);
}
if (!postcard.isGreenChannel()) { // It must be run in async thread, maybe interceptor cost too mush time made ANR.
... //拦截器相关处理
} else {
//导航
return _navigation(context, postcard, requestCode, callback);
}
return null;
}
// 进行导航
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: //如果是 Activity
// 构造Intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras()); //设置bundle
// 设置 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);
}
// Navigation in main looper.
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (requestCode > 0) { // Need start for result
ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
} else {
ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
}
//设置出入动画
if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) { // Old version.
((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
}
//导航回调
if (null != callback) { // Navigation over.
callback.onArrival(postcard);
}
}
});
break;
...
}
return null;
}
}
从上边的分析可知,每次进行 navigation的时候都要创建一个 Postcard,然后根据路由表的信息,对Postcard对象进行初始化,之后进行真正的 navigation()
。我们接下来看一下,如何对 Postcard 对象进行初始化:LogisticsCenter#completion
public synchronized static void completion(Postcard postcard) {
...
//根据path获取路由信息
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
//如果路由信息为空,则可能此分组的路由表还从未加载过;当然也可能是压根就找不到路由信息。
if (null == routeMeta) {
// 获取分组信息
Class groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());
//如果分组信息不存在,则抛出异常。有可能是开始就没有分组信息,也有可能是 路由分组信息加载过了之后会被清理。
if (null == groupMeta) {
throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
} else {
try {
//加载此分组的路由信息到路由表,然后把此路由分组删除。
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
iGroupInstance.loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(postcard.getGroup());
//如果不删除,假设navigatoin一个分组中没有的路由表信息,就会无限循环找下去了。其实只要路由分组表被加载过一次,路由表(Warehouse.routes)就会包含此分组的路由表。
}
} catch (Exception e) {
throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
}
completion(postcard); // 路由分组的路由表被加载以后,调用初始化方法
}
} else {
//找到路由信息,初始化Postcard
postcard.setDestination(routeMeta.getDestination());
postcard.setType(routeMeta.getType());
postcard.setPriority(routeMeta.getPriority());
postcard.setExtra(routeMeta.getExtra());
Uri rawUri = postcard.getUri();
...
}
}
至此, 一次完整的页面跳转的处理过程分析完成。
五、Gradle-Plugin(可选)
之前有一个问题是,加固以后APK找不到,路由信息的这些类了,怎么办?通过以上流程分析,我们可知 ARouter.init
初始化路由的时候,会去扫描 dex 文件,找到路由信息的类,然后装载 路由分组类 到内存。gradle-plugin 的作用 在编译期间动态修改字节码文件,动态插入到 LogisticsCenter 类中,改变init的逻辑。当然这是一个可选的模块。
使用gradle-plugin之前,init 的代码逻辑一定会执行 dex 扫描的逻辑,因为 loadRouterMap
方法为空, registerByPlugin
永远为false。代码详情:
/**
* arouter-auto-register plugin will generate code inside this method
* call this method to register all Routers, Interceptors and Providers
* @author billy.qi Contact me.
* @since 2017-12-06
*/
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());
}
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
try {
long startInit = System.currentTimeMillis();
//billy.qi modified at 2017-12-06
//load by plugin first
loadRouterMap();
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
//扫描 dex,初始化路由分组信息
}
}
使用了 gradle-plugin 之后,我们把生成的apk反编译后,看一下真实的代码是什么样的:
会在执行 loadRouterMap 方法的时候,去装载所有路由分组信息,之后 registerByPlugin
被设置成true,接下来逻辑判断的时候就不会继续使用 dex 扫描的方式。
至此,ARouter 的全貌已浮出水面,对它也有了一个更清晰的认识。