前篇回顾
- Android组件化 —— 基础(一) - 组件化与集成化
- Android组件化 —— 基础(二) - 组件间通讯
上篇文章,我们了解到“路由”是如何进行工作的,并尝试手写了实现代码;
本篇,以ARouter为例,我们将剖析它是如何完成工作的,并通过源码了解其原理。
还是以Activity跳转、Fragment获取、跨模块功能调用三个方向为出发点,看看ARouter将如何实现这些功能。
集成ARouter
咱们先快速将ARouter集成到项目中,当然你也可以参照官方文档来操作。
在第一篇文章中,我们将业务模块的依赖关系集中到了config.gradle文件中,现修改如下:
- 1、config.gradle中管理依赖版本
ext {
// ARouter 版本号
arouter_api_version = "1.5.2"
arouter_compiler_version = "1.5.2"
//各模块引入的第三方公共库
dependenciesImport = [
...
...
arouter_api : "com.alibaba:arouter-api:$arouter_api_version"
]
//注解处理器
dependenciesKapt = [
...
...
arouter: "com.alibaba:arouter-compiler:$arouter_compiler_version"
]
}
- 2、子模块的build.gradle中,传递APT所需参数,并引入依赖
def dependenciesImport = rootProject.ext.dependenciesImport
def dependenciesKapt = rootProject.ext.dependenciesKapt
android {
defaultConfig {
...
...
// ARouter APT 传参
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":lib_comm")
// 导入依赖库
dependenciesImport.each { k, v -> implementation(v) }
// 导入依赖的注解处理器
dependenciesKapt.each { k, v -> kapt(v) }
}
- 3、初始化ARouter
class App : Application() {
override fun onCreate() {
super.onCreate()
initRouter()
}
private fun initRouter() {
if (BuildConfig.DEBUG) {
ARouter.openLog() // 打印日志
ARouter.openDebug() // 开启调试模式
}
ARouter.init(this) // 初始化
}
}
- 4、运行查看Log记录:
2021-09-13 17:47:07.911 10088-10088/com.ljb.component I/ARouter::: ARouter init success![ ]
ARouter - Activity跳转
根据官方文档,只需2步便可完成功能:
- 1、注解标记需要跳转的Activity
@Route(path = "/user/UserMainActivity" , group = "user")
class UserMainActivity : AppCompatActivity() {
...
}
- 2、发起跳转
ARouter.getInstance().build("/user/UserMainActivity").navigation()
很神奇!与前篇我们实现的路由相比,ARouter的代码要简洁很多,完全不需要手动注册路由就可完成跳转,它是怎么做到的呢?
通过跟进navigation()函数调用过程,我们把目光聚焦到两个容器中:
// ARouter源码
class Warehouse {
// Cache route and metas
static Map> groupsIndex = new HashMap<>();
static Map routes = new HashMap<>();
...
}
- Warehouse.groupsIndex: 用于存储所有的路由组
public interface IRouteGroup {
/**
* Fill the atlas with routes in group.
* atlas用于存储当前组里的所有路由,实际传入的就是Warehouse.routes
*/
void loadInto(Map atlas);
}
- Warehouse.routes:用于存储已注册的所有路由
// 路由包装类,路由目标的Class对象就存储在这里面
public class RouteMeta {
private RouteType type; // Type of route
private Element rawType; // Raw type of route
private Class> destination; // Destination
private String path; // Path of route
private String group; // Group of route
private int priority = -1; // The smaller the number, the higher the priority
private int extra; // Extra data
private Map paramsType; // Param type
private String name;
...
}
ARouter对路由提出了分组概念,上面 UserMainActivity就属于user组下,当路由path存在2级及以上时,group字段也可以省略,ARouter默认会使用第一个反斜杠后面的path作为组名。
// group可省略不写
@Route(path = "/user/UserMainActivity")
class UserMainActivity : AppCompatActivity() {
...
}
一般情况下,我们会将同一模块的路由划分在同一个组下,例如App模块下的所有路由都在“app”这个分组下 , user模块的路由都在“user”分组下;当然,同一模块拥有多个分组也是完全可行的,只要保证与其它模块中的路由分组不重名即可。
通过翻阅源码,分析这两个容器的作用,大致如下:
- 1、当传入path进行跳转时,优先从Warehouse.routes中直接获取路由对象;
- 2、路由对象不存在,就需要通过Warehouse.groupsIndex路由组来完成注册功能;
- 3、注册成功后,当前path所在组的所有路由都将存储到Warehouse.routes中;
- 4、回到第1步,获取路由对象;
- 5、读取路由对象信息;
- 6、完成跳转。
下方是ARouter源码中的实现代码,我把不重要的部分进行了删减:
//ARouter源码
public class LogisticsCenter {
...
public synchronized static void completion(Postcard postcard) {
//1、当传入path进行跳转时,优先从Warehouse.routes中直接获取路由包装对象;
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
if (null == routeMeta) {
//2、包装对象不存在,就需要通过Warehouse.groupsIndex路由组来完成注册功能;
addRouteGroupDynamic(postcard.getGroup(), null);
//4、回到第1步,获取包装对象;
completion(postcard);
} else {
// 5、读取包装对象信息;
postcard.setDestination(routeMeta.getDestination());
postcard.setType(routeMeta.getType());
...
}
public synchronized static void addRouteGroupDynamic(String groupName, IRouteGroup group) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
if (Warehouse.groupsIndex.containsKey(groupName)){
//3、注册成功后,当前path所在组的所有路由都将存储到Warehouse.routes中;
Warehouse.groupsIndex.get(groupName).getConstructor().newInstance().loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(groupName);
}
if (null != group) {
// 注册本组路由
group.loadInto(Warehouse.routes);
}
}
...
}
final class _ARouter {
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
...
LogisticsCenter.completion(postcard);
...
// 6、完成跳转。
switch (postcard.getType()) {
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
//...
// Navigation in main looper.
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode, currentContext, intent, postcard, callback);
}
});
break;
...
}
...
}
}
整个跳转的源代码过程不难理解,但关键点在于:
- Warehouse.groupsIndex容器是何时将路由组加载至内存的?
- group.loadInto(Warehouse.routes)又是如何完成注册的?
回答这两个问题,需回过头来看ARouter初始化都做了什么。
ARouter.init(this) // ARouter初始化
随着代码的跟进,我们会发现下面这几行代码:
//ARouter源码
public final class Consts {
public static final String SDK_NAME = "ARouter";
public static final String SEPARATOR = "$$";
public static final String SUFFIX_ROOT = "Root";
public static final String DOT = ".";
public static final String ROUTE_ROOT_PAKCAGE = "com.alibaba.android.arouter.routes";
}
public class LogisticsCenter {
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
// 获取com.alibaba.android.arouter.routes下的所有class文件
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
for (String className : routerMap) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
// 将名为ARouter$$Root开头的Class对象创建出来,并调用loadInto(),将分组信息加载进Warehouse.groupsIndex中
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
}
...
}
}
}
源码中会从com.alibaba.android.arouter.routes包下读取所有的Class文件,并将文件名以ARouter$$Root为开头的Class文件挑选出来,最后通过反射创建对象,并调用其loadInto()函数,注意这里传入参数正是Warehouse.groupsIndex,我们的路由组容器,从而将分组信息载入Warehouse.groupsIndex中。
果不其然,我们在user模块的build文件夹下找到了这些文件:
- 关于ARouter$$Root 前缀文件,可以看到其后缀是当前模块名module_user,而其loadInto()函数主要是将user模块下涉及到的所有路由组存储到Warehouse.groupsIndex中;
public class ARouter$$Root$$module_user implements IRouteRoot {
@Override
public void loadInto(Map> routes) {
routes.put("user", ARouter$$Group$$user.class);
}
}
- 关于ARouter$$Group前缀文件,其后缀是当前路由所属的的分组名user,前面在梳理Activity跳转的过程中,我们知道当路由未注册时,会通过分组名获取到组,并创建组对象调用其loadInto()函数完成对Warehouse.routes的注册,此文件中编写的既是具体的注册过程。
//3、注册成功后,当前path所在组的所有路由都将存储到Warehouse.routes中;
Warehouse.groupsIndex.get(groupName).getConstructor().newInstance().loadInto(Warehouse.routes);
public class ARouter$$Group$$user implements IRouteGroup {
@Override
public void loadInto(Map atlas) {
// loadInto被调用时,完成对Warehouse.routes的注册
atlas.put("/user/AppMainActivity", RouteMeta.build(RouteType.ACTIVITY, AppMainActivity.class, "/user/appmainactivity", "user", null, -1, -2147483648));
}
}
ARouter - APT技术
通过上述分析,我们知道路由的注册离不开ARouter生成的这两个文件,那这两文件又是怎么来的呢?
这里不得不提到APT技术( Annotation Processing Tool),译:注解处理器。如果对该技术有所耳闻,你应该知道在Android中我们常用的EventBus、Butterknife、Dagger2等框架都有它的身影。如果你是第一次听说也没关系,暂时只需知道通过这门技术可以做到“在编译期时,可使编译器为我们生成具有一定规则的模板性代码”即可。
显然,这两个文件就是ARouter通过APT生成的模板代码,至于具体的实现过程,本篇中不做过多阐述,后续处理自定义路由时,我们会尝试编写一个自己的注解处理器,到时再细聊APT技术的相关细节。
不过我把ARouter注解处理器中生成这两个文件的核心代码贴在下方,看懂它的思路即可:
//ARouter 注解处理器源码
@AutoService(Processor.class)
@SupportedAnnotationTypes({ANNOTATION_TYPE_ROUTE, ANNOTATION_TYPE_AUTOWIRED})
public class RouteProcessor extends BaseProcessor {
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 获取所有被 @Route 注解标记的类元素
Set extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
//解析这些类元素
this.parseRoutes(routeElements);
}
private void parseRoutes(Set extends Element> routeElements) throws IOException {
// 解析元素
for (Element element : routeElements) {
if (types.isSubtype(tm, type_Activity)) {
// Activity
routeMeta = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType);
Set routeMetas = groupMap.get(routeMete.getGroup());
routeMetaSet.add(routeMete);
groupMap.put(routeMete.getGroup(), routeMetaSet);
}
}
// 根据解析元素生成Java文件
for (Map.Entry> entry : groupMap.entrySet()) {
// 生成ARouter$$Group$$前缀文件
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler);
}
// 生成ARouter$$Root$$前缀文件
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler);
}
}
至此,整个Activity跳转涉及到的源码大致就是这些,总结下ARouter都做了什么:
- 1、在编译期,ARouter注解处理器将所有@Route注解标记的类查找出来,并根据这些类的相关信息生成ARouter$$相关文件;
- 2、ARouter.init()初始化时,将ARouter$$Group前缀文件涉及到的路由组存储到Warehouse.groupsIndex中;
- 3、调用ARouter...navigation()跳转页面时,通过Warehouse.groupsIndex查询路由所在组,将路由组中的所有路由注册到Warehouse.routes中,从Warehouse.routes中取出路由信息(包含Activity.class对象),完成跳转。
Fragment获取
依旧根据文档,获取Fragment歩奏也只需2步:
- 1、注解标记需要获取的Fragemnt
@Route(path = "/user/UserFragment")
class UserFragment : Fragment() {
...
}
- 2、获取Fragment
val f = ARouter.getInstance().build("/user/UserFragment").navigation() as Fragment
有了对Activity跳转源码分析的铺垫,Fragment的获取原理基本是一样的,只是在最后一步发生跳转的代码,变为返回Fragment对象:
final class _ARouter {
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
...
// 6、完成跳转。
switch (postcard.getType()) {
// Activity跳转
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
//...
// Navigation in main looper.
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode, currentContext, intent, postcard, callback);
}
});
break;
// Fragment获取
case FRAGMENT:
Class> fragmentMeta = postcard.getDestination();
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;
...
}
...
}
}
跨模块功能调用
ARouter的方案和我们之前的实现的方案原理是一样的,都需将对外的服务暴露到lib_comm中,只是如Activity和Fragment一样注册的功能通过APT技术来实现。
- 1、lib_comm中定义暴露的接口
interface IUserService2 : IProvider {
/**
* 是否登录
*/
fun isLogin(): Boolean
}
- 2、业务模块实现接口,并通过@Route注解标记
@Route(path = "/user/UserService")
class IUserServiceImpl2 : IUserService2 {
override fun init(context: Context?) {
}
override fun isLogin(): Boolean {
return UserUtils.isLogin()
}
}
- 3、获取服务
ARouter.getInstance().build("/user/UserService").navigation() as IUserService2
和Fragment类似,只不过在获取时,多了一步缓存操作,缓存相关的核心代码我贴在了下方:
class Warehouse {
// Cache provider
static Map providers = new HashMap<>();
}
// 创建服务,并缓存
switch (routeMeta.getType()) {
case PROVIDER: // if the route is provider, should find its instance
// Its provider, so it must implement IProvider
Class extends IProvider> providerMeta = (Class extends IProvider>) routeMeta.getDestination();
IProvider instance = Warehouse.providers.get(providerMeta);
if (null == instance) { // There's no instance of this provider
IProvider provider;
try {
provider = providerMeta.getConstructor().newInstance();
provider.init(mContext);
// 进行缓存
Warehouse.providers.put(providerMeta, provider);
instance = provider;
} catch (Exception e) {
logger.error(TAG, "Init provider failed!", e);
throw new HandlerException("Init provider failed!");
}
}
postcard.setProvider(instance);
postcard.greenChannel(); // Provider should skip all of interceptors
break;
}
获取时,只需从postcard.getProvider()中取即可:
final class _ARouter {
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
...
// 6、完成跳转。
switch (postcard.getType()) {
// Activity跳转
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
//...
// Navigation in main looper.
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode, currentContext, intent, postcard, callback);
}
});
break;
// Fragment获取
case FRAGMENT:
Class> fragmentMeta = postcard.getDestination();
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;
// 获取Service
case PROVIDER:
return postcard.getProvider();
...
}
...
}
}
OK,ARouter路由核心三剑客功能已摸了个底朝天,与我们自己实现的路由框架相比,其核心思想是一样的,但在细节上ARouter的处理明显优于我们,例如:
- 通过APT来实现路由注册,省掉手动注册的繁琐,且无需在App启动时注册所有路由(与我们startup方案相比),仅在使用到某组路由时才进行注册,避免不必要的内存开销。
- 对外提供功能的Service对象,在使用时进行缓存,避免重复创建。
小结
本篇对ARouter的Activity跳转、Fragment获取、扩模块功能调用花费了大幅篇章来讲解源码,是希望读者能了解其基本原理,后续我在处理自定义路由时,会参考ARouter实现来造轮子,也方便读者对后续篇章的理解。
ARouter还支持很多功能,例如:拦截器、标准Scheme URL跳转、全局降级策略、动态路由注册等,这些功能后续篇章中一部分会讲到,一部分也可能使用不到,但并不代表这些功能没用,每个项目或团队对组件化架构的理解都会有所不同,最后落地的项目结构、路由交互方案也就各具特色。无论是怎样的项目,个人认为:
- 组件化,它是由实际业务开发过程中,开发人员通过经验积累,设计出的一套适用于多人协作开发的架构模式;
- 具备可复用性、可替代性、热插拔、可独立调试特性的模块,就是组件化模块(组件);
- 由多个组件化模块组成的项目架构,就是组件化架构。
下篇,聊聊组件通讯之Scheme URL,我们下篇再见!
Android组件化架构 —— 基础(四) URL Scheme