一、ARouter
路由框架在大型项目中比较常见,特别是在项目中拥有多个 moudle 的时候。为了实现组件化,多个 module 间的通信就不能直接以模块间的引用来实现,此时就需要依赖路由框架来实现模块间的通信和解耦😎
而 ARouter 就是一个用于帮助 Android App 进行组件化改造的框架,支持模块间的路由、通信、解耦
1、支持的功能
- 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
- 支持多模块工程使用
- 支持添加多个拦截器,自定义拦截顺序
- 支持依赖注入,可单独作为依赖注入框架使用
- 支持InstantRun
- 支持MultiDex(Google方案)
- 映射关系按组分类、多级管理,按需初始化
- 支持用户指定全局降级与局部降级策略
- 页面、拦截器、服务等组件均自动注册到框架
- 支持多种方式配置转场动画
- 支持获取Fragment
- 完全支持Kotlin以及混编(配置见文末 其他#5)
- 支持第三方 App 加固(使用 arouter-register 实现自动注册)
- 支持生成路由文档
- 提供 IDE 插件便捷的关联路径和目标类
2、典型应用
- 从外部URL映射到内部页面,以及参数传递与解析
- 跨模块页面跳转,模块间解耦
- 拦截跳转过程,处理登陆、埋点等逻辑
- 跨模块API调用,通过控制反转来做组件解耦
以上介绍来自于 ARouter 的 Github 官网:README_CN
本文就基于其当前(2020/10/04)ARouter 的最新版本,对 ARouter 进行一次全面的源码解析和原理介绍,做到知其然也知其所以然,希望对你有所帮助😁😁
dependencies {
implementation 'com.alibaba:arouter-api:1.5.0'
kapt 'com.alibaba:arouter-compiler:1.2.2'
}
复制代码
二、前言
假设存在一个包含多个 moudle 的项目,在名为 user 的 moudle 中存在一个 UserHomeActivity
,其对应的路由路径是 /account/userHome
。那么,当我们要从其它 moudle 跳转到该页面时,只需要指定 path 来跳转即可
package github.leavesc.user
/**
* 作者:leavesC
* 时间:2020/10/3 18:05
* 描述:
* GitHub:https://github.com/leavesC
*/
@Route(path = RoutePath.USER_HOME)
class UserHomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_home)
}
}
//其它页面使用如下代码来跳转到 UserHomeActivity
ARouter.getInstance().build(RoutePath.USER_HOME).navigation()
复制代码
只根据一个 path,ARouter 是如何定位到特定的 Activity 的呢?
这就需要通过在编译阶段生成辅助代码来实现了。我们都知道,想要跳转到某个 Activity,那么就需要拿到该 Activity 的 Class 对象才行。在编译阶段,ARouter 会根据我们设定的路由跳转规则来自动生成映射文件,映射文件中就包含了 path 和 ActivityClass 之间的对应关系
例如,对于 UserHomeActivity
,在编译阶段就会自动生成以下辅助文件。可以看到,ARouter$$Group$$account
类中就将 path 和 ActivityClass 作为键值对保存到了 Map 中。ARouter 就是依靠此来进行跳转的
package com.alibaba.android.arouter.routes;
/**
* DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$account implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/account/userHome", RouteMeta.build(RouteType.ACTIVITY, UserHomeActivity.class, "/account/userhome", "account", null, -1, -2147483648));
}
}
复制代码
还有一个重点需要注意,就是这类自动生成的文件的包名路径都是 com.alibaba.android.arouter.routes
,且类名前缀也是有特定规则的。虽然 ARouter$$Group$$account
类实现了将对应关系保存到 Map 的逻辑,但是 loadInto
方法还是需要由 ARouter 在运行时来调用,那么 ARouter 就需要拿到 ARouter$$Group$$account
这个类才行,而 ARouter 就是通过扫描 com.alibaba.android.arouter.routes
这个包名路径来获取所有辅助文件的
ARouter 的基本实现思路就是:
- 开发者自己维护特定 path 和特定的目标类之间的对应关系,ARouter 只要求开发者使用包含了 path 的
@Route
注解修饰目标类 - ARouter 在编译阶段通过注解处理器来自动生成 path 和特定的目标类之间的对应关系,即将 path 作为 key,将目标类的 Class 对象作为 value 之一存到 Map 之中
- 在运行阶段,应用通过 path 来发起请求,ARouter 根据 path 从 Map 中取值,从而拿到目标类
三、初始化
ARouter 的一般是放在 Application
中调用 init
方法来完成初始化的,这里先来看下其初始化流程
/**
* 作者:leavesC
* 时间:2020/10/4 18:05
* 描述:
* GitHub:https://github.com/leavesC
*/
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
ARouter.openDebug()
ARouter.openLog()
}
ARouter.init(this)
}
}
复制代码
ARouter
类使用了单例模式,逻辑比较简单,因为 ARouter
类只是负责对外暴露可以由外部调用的 API,大部分的实现逻辑还是转交由 _ARouter
类来完成
public final class ARouter {
private volatile static ARouter instance = null;
private ARouter() {
}
/**
* Get instance of router. A
* All feature U use, will be starts here.
*/
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;
}
}
/**
* Init, it must be call before used router.
*/
public static void init(Application application) {
if (!hasInit) { //防止重复初始化
logger = _ARouter.logger;
_ARouter.logger.info(Consts.TAG, "ARouter init start.");
//通过 _ARouter 来完成初始化
hasInit = _ARouter.init(application);
if (hasInit) {
_ARouter.afterInit();
}
_ARouter.logger.info(Consts.TAG, "ARouter init over.");
}
}
···
}
复制代码
_ARouter
类是包私有权限,也使用了单例模式,其 init(Application)
方法的重点就在于 LogisticsCenter.init(mContext, executor)
final class _ARouter {
private volatile static _ARouter instance = null;
private _ARouter() {
}
protected static _ARouter getInstance() {
if (!hasInit) {
throw new InitException("ARouterCore::Init::Invoke init(context) first!");
} else {
if (instance == null) {
synchronized (_ARouter.class) {
if (instance == null) {
instance = new _ARouter();
}
}
}
return instance;
}
}
protected static synchronized boolean init(Application application) {
mContext = application;
//重点
LogisticsCenter.init(mContext, executor);
logger.info(Consts.TAG, "ARouter init success!");
hasInit = true;
mHandler = new Handler(Looper.getMainLooper());
return true;
}
···
}
复制代码
LogisticsCenter
就实现了前文说的扫描特定包名路径拿到所有自动生成的辅助文件的逻辑,即在进行初始化的时候,我们就需要加载到当前项目一共包含的所有 group,以及每个 group 对应的路由信息表,其主要逻辑是:
- 如果当前开启了 debug 模式或者通过本地 SP 缓存判断出 app 的版本前后发生了变化,那么就重新获取全局路由信息,否则就从使用之前缓存到 SP 中的数据
- 获取全局路由信息是一个比较耗时的操作,所以 ARouter 就通过将全局路由信息缓存到 SP 中来实现复用。但由于在开发阶段开发者可能随时就会添加新的路由表,而每次发布新版本正常来说都是会加大应用的版本号的,所以 ARouter 就只在开启了 debug 模式或者是版本号发生了变化的时候才会重新获取路由信息
- 获取到的路由信息中包含了在
com.alibaba.android.arouter.routes
这个包下自动生成的辅助文件的全路径,通过判断路径名的前缀字符串,就可以知道该类文件对应什么类型,然后通过反射构建不同类型的对象,通过调用对象的方法将路由信息存到Warehouse
的 Map 中。至此,整个初始化流程就结束了
public class LogisticsCenter {
/**
* 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 {
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 {
Set<String> routerMap;
//如果当前开启了 debug 模式或者通过本地 SP 缓存判断出 app 的版本前后发生了变化
//那么就重新获取路由信息,否则就从使用之前缓存到 SP 中的数据
// 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.
//获取 ROUTE_ROOT_PAKCAGE 包名路径下包含的所有的 ClassName
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
if (!routerMap.isEmpty()) {
//缓存到 SP 中
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
}
//更新 App 的版本信息
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) {
//通过 className 的前缀来判断该 class 对应的什么类型,并同时缓存到 Warehouse 中
//1.IRouteRoot
//2.IInterceptorGroup
//3.IProviderGroup
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);
}
}
}
···
} catch (Exception e) {
throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
}
}
}
复制代码
对于第三步,可以举个例子来加强理解。对于上文所讲的