前言
组件化开发的会实现代码隔离,在开发时访问不到模块的代码,降低代码耦合度。那么如何跳转组件的页面、如何进行组件间的通信是个问题。这通常会使用到 ARouter、TheRouter、WMRouter 等路由框架。可能有不少人只知道怎么去调用,并不知道其中的实现原理。其实了解路由原理后再进行组件化开发会更加得心应手。
网上很多讲路由原理的文章都是直接分析 ARouter 源码,这对于刚入门组件化开发的人来说比较晦涩难懂。所以个人尝试用一种手写迭代路由框架的方式,让大家能从中学习到:
- 如何从零开始设计一个路由框架;
- 路由框架需要考虑怎么样的使用场景;
- 了解路由框架的核心实现原理;
对这些有一定了解后,我们就能更容易地去阅读其它路由框架的源码了。
搭建简易的路由框架
先了解下什么是路由,维基百科 的介绍是:路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。这是互联网中的路由概念,其思想是通过一样东西找到另一样我们需要的东西。将路由的思想运用到 Android 的组件化中,就是希望能用某个东西能找到对应的类去实现对应的功能。
那我们就来实现一下,首先要有一个 String 和 Class 的映射表,可以用一个 Map 来保存。写一个 SimpleRouter 提供一个设置映射关系和导航的方法。
object SimpleRouter {
private val routes = HashMap<String, Class<*>>()
fun putRoute(path: String, clazzName: String) = routes.apply {
put(path, Class.forName(clazzName))
}
fun navigation(path: String): Class<*>? {
return routes[path]
}
}
这就实现了一个简易的路由框架,有人可能会说:就这?一个完善好用的路由框架肯定不止这些代码,但是最核心的代码就是路由表。
我们先来用一下,比如将登录页面和 /account/sign_in
字符串映射起来。
SimpleRouter.putRoute("/account/sign_in", "com.dylanc.simplerouter.user.SignInActivity")
后面即使我们做了代码隔离,不能直接访问到对应的 Class,那么我们能通过一个字符串去找到 Class 对象,有了 Class 对象就能跳转页面或实例化。
val clazz = SimpleRouter.navigation("/account/sign_in")
if (clazz != null) {
startActivity(Intent(this, clazz))
}
手动给路由表的 Map 添加类的信息看起来有点蠢,后面可以改成用注解配置:
@Route(path = "/account/sign_in")
class SignInActivity : AppCompatActivity() {
// ...
}
APT 解析注解生成文件,其实是最终做的事情是一样的,都是往一个 Map 保存类的信息,只是 put 的代码是自己手写还是注解自动生成而已。本文主要还是了解路由的实现原理,就先用简单的方式实现。
小结一下,第一版我们实现了路由框架的核心功能,建立映射表后就能通过一个字符串去找到类对象去实现想要的功能。
完善页面导航跳转
通过第一版的路由框架能得到所需的类对象了,想跳转一个 Activity 也没有问题,但是跳转的代码总是要判断类对象非空后调用 startActivity(intent)。
val clazz = SimpleRouter.navigation("/account/sign_in")
if (clazz != null) {
startActivity(Intent(this, clazz))
}
每次跳转页面都要这么写挺繁琐的,我们可以优化一下调用 SimpleRouter.navigation(path) 时会把 startActivity(intent) 给执行了,这样就只需写一行代码就能跳转页面了。
SimpleRouter.navigation("/account/sign_in")
那么问题又来了,怎么传参呢?仅仅只有一个 path 字符串是不够的,需要有另外一个类来设置更多的信息。我们创建一个 Postcard 类,Postcard 是明信片的意思,类似我们能往明信片上写东西,我们能给 Postcard 对象设置 path 和 Bundle 相关的数据。
class Postcard(
val path: String,
var bundle: Bundle = Bundle(),
) {
fun with(bundle: Bundle): Postcard = apply {
this.bundle = bundle
}
fun withString(key: String, value: String): Postcard = apply {
bundle.putString(key, value)
}
fun withInt(key: String, value: Int): Postcard = apply {
bundle.putInt(key, value)
}
fun withLong(key: String, value: Long): Postcard = apply {
bundle.putLong(key, value)
}
fun withFloat(key: String, value: Float): Postcard = apply {
bundle.putFloat(key, value)
}
fun withDouble(key: String, value: Double): Postcard = apply {
bundle.putDouble(key, value)
}
fun withChar(key: String, value: Char): Postcard = apply {
bundle.putChar(key, value)
}
fun withBoolean(key: String, value: Boolean): Postcard = apply {
bundle.putBoolean(key, value)
}
fun withByte(key: String, value: Byte): Postcard = apply {
bundle.putByte(key, value)
}
fun withCharSequence(key: String, value: CharSequence): Postcard = apply {
bundle.putCharSequence(key, value)
}
// ...
}
把前面 navigation() 函数的 String 参数改成 Postcard 类型,可以顺便把 startActivityForResult() 适配了。
object SimpleRouter {
// ...
fun navigation(context: Context, postcard: Postcard, requestCode: Int = -1) {
val destination = routes[postcard.path]
?: throw IllegalStateException("There is no route match the path [${postcard.path}]")
val intent = Intent(context, destination).putExtras(postcard.bundle)
if (context !is Activity) {
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
if (requestCode >= 0) {
if (context is Activity) {
context.startActivityForResult(intent, requestCode)
}
} else {
context.startActivity(intent)
}
}
}
之后就可以创建 Postcard 对象去带参跳转页面了。
val postcard = Postcard("/uset/login").withString("email", email)
SimpleRouter.navigation(this, postcard)
// SimpleRouter.navigation(this, postcard, REQUEST_CODE_SIGN_IN)
不过总是要创建个 Postcard 对象也不是方便,最好是能改成链式调用。实现起来也挺简单,我们给 SimpleRouter 增加一个 build(path) 函数返回一个 Postcard 对象。
object SimpleRouter {
// ...
fun build(path: String): Postcard {
return Postcard(path)
}
}
然后再给 Postcard 类增加两个 navigation() 函数 。
class Postcard(
val path: String,
var bundle: Bundle = Bundle(),
) {
// ...
fun navigation(context: Context) {
return SimpleRouter.navigation(context, this, -1)
}
fun navigation(activity: Activity, requestCode: Int) {
return SimpleRouter.navigation(activity, this, requestCode)
}
}
这样就能用一行链式代码跳转页面。
SimpleRouter.build("/user/login")
.withString("email","xxxxx@gmail.com")
.navigation(this)
// .navigation(this, REQUEST_CODE_SIGN_IN)
至此我们就把路由工具核心的用法给实现了,ARouter 或其它路由框架都是类似的用法。
小结一下,第二版在第一版的基础上,增加了一个 Postcard 类,保存跳转页面时所需的信息。简化了路由工具跳转页面的用法,只需一行代码就能跳转,并且支持传参,支持调用 startActivityForResult(intent)。
支持创建 Fragment
虽然前面把路由工具的核心用法实现了,但是只处理了 Activity 一种情况,还可能会有其他的 Class,比如很常见的 Fragment,我们这就来适配一下。
通常我们是会创建 Fragment 对象来使用,那么可以在获得 Class 后判断一下是不是 Fragment 类型,是的话就实例化无参的构造函数。
目前只是简单地用 String 和 Class 做映射,有时候还是不太够用,最好能区分下类型。所以我们再增加一个 RouteMeta 类来保存更多的路由信息,并且添加一个 RouteType 的路由类型枚举,来区分一下是 Activity 或 Fragment,要支持其它类型只需再添加。
class RouteMeta(
val destination: Class<*>,
val type: RouteType,
)
enum class RouteType {
ACTIVITY, FRAGMENT, UNKNOWN
}
我们把原来路由表的 HashMap<String, Class<*>> 缓存改成 HashMap<String, RouteMeta>,初始化路由表的代码也要更改一下。
object SimpleRouter {
private val routes = HashMap<String, RouteMeta>()
fun putRoute(path: String, clazzName: String) = routes.apply {
val clazz = Class.forName(clazzName)
val type = when {
Activity::class.java.isAssignableFrom(clazz) -> RouteType.ACTIVITY
Fragment::class.java.isAssignableFrom(clazz) -> RouteType.FRAGMENT
else -> RouteType.UNKNOWN
}
put(path, RouteMeta(clazz, type))
}
// ...
}
这样我们就能从映射表中查出对应的类型,然后在 navigation() 函数根据类型做不同的事,是 Activity 就跳转页面,是 Fragment 就实例化 Fragment 对象,并且把 Bundle 参数给设置了。
object SimpleRouter {
// ...
private lateinit var application: Application
fun init(application: Application) {
this.application = application
}
fun navigation(ctx: Context? , postcard: Postcard, requestCode: Int = -1): Any? {
val context = ctx ?: application
val routeMeta = routes[postcard.path]
?: throw IllegalStateException("There is no route match the path [${postcard.path}]")
val destination = routeMeta.destination
return when (routeMeta.type) {
RouteType.ACTIVITY -> {
val intent = Intent(context, destination).putExtras(postcard.bundle)
if (context !is Activity) {
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
if (requestCode >= 0) {
if (context is Activity) {
context.startActivityForResult(intent, requestCode)
}
} else {
context.startActivity(intent)
}
null
}
RouteType.FRAGMENT -> {
val fragmentMeta: Class<*> = destination
try {
val instance = fragmentMeta.getConstructor().newInstance()
if (instance is Fragment) instance.arguments = postcard.bundle
instance
} catch (e: Exception) {
null
}
}
else -> null
}
}
}
我们来测试一下,配置个人页面 Fragment 的路由信息。
// 以后会改成用 @Router 注解来初始化路由表
SimpleRouter.putRoute("/account/me", "com.dylanc.simplerouter.user.MeFragment")
然后就能在没有直接依赖其它模块代码的情况下,通过路由去创建 Fragment。
val fragment = SimpleRouter.build("/account/me").navigation() as? Fragment
虽然跳转 Activity 和创建 Fragment 都用了同一个 navigation() 函数,但是一个会用到返回值,一个没有使用返回值,用法上会有点差异,因为根据不同的类型做了不同的事情。
小结,第三版代码在第二版的基础上,支持了 Fragment 的使用场景,如果路由表对应的 Class 是 Fragment,那么会实例化该 Fragment。增加了 RouteType 类和 RouteMeta 类,用于区分类型和保存更多的路由信息。
支持模块间通信
目前我们适配了跳转 Activity 和创建 Fragment,但是组件化开发还有一个很常见的使用场景,就是模块间的通信,获取一个模块的某些信息或者让一个模块做某些事。
这个接口的实现类在哪我们不用关心,因为我们可以用路由工具去得到实例对象。但是怎么区分这是用于模块间通信的类呢?
首先我们要区分出哪些类用于模块间的通信的,定义一个 IProvider 接口,如果有类是实现了该接口,那么该类是会用于模块间通信。
interface IProvider {
fun init(context: Context)
}
给 RouteType 再加个 PROVIDER 类型,IProvider 子类对应的是 PROVIDER 类型。
enum class RouteType {
ACTIVITY, FRAGMENT, PROVIDER, UNKNOWN
}
判断是 PROVIDER 类型,我们就用类似 Fragment 的处理方式反射无参的构造函数,但是又有些不一样,没必要每次都实例化 IProvider 对象,可以缓存起来。那么现在缓存的东西开始变多了,我们可以创建一个 Warehouse 类专门来持有需要缓存的数据,把之前的路由表也放到该类中。Warehouse 和 Repository 一样是仓库的意思。
object Warehouse {
val routes = HashMap<String, RouteMeta>()
val providers = HashMap<Class<*>, IProvider>()
}
这样我们就可以在 navigation() 函数增加 PROVIDER 类型的逻辑了,不过路由工具类的代码就越来越多了,我们可以优化一下,将初始化缓存和读取缓存的逻辑交给另一个类处理。
我们再创建一个 LogisticsCenter 类,把之前的初始化路由表的函数放到该类处理。还增加一个 completion() 函数去给 Postcard 补充信息,从仓库的路由表查出对应的类信息设置到 Postcard 中。如果路由的类型是 PROVIDER 类型,要获取仓库的 IProvider 对象缓存,没缓存就先实例化存到 Warehouse 中。
object LogisticsCenter {
private lateinit var context: Application
fun init(application: Application) {
context = application
}
fun putRoute(path: String, clazzName: String) {
val clazz = Class.forName(clazzName)
val type = when {
Activity::class.java.isAssignableFrom(clazz) -> RouteType.ACTIVITY
Fragment::class.java.isAssignableFrom(clazz) -> RouteType.FRAGMENT
IProvider::class.java.isAssignableFrom(clazz) -> RouteType.PROVIDER
else -> RouteType.UNKNOWN
}
Warehouse.routes[path] = RouteMeta(clazz, type)
}
@Suppress("UNCHECKED_CAST")
fun completion(postcard: Postcard) {
val routeMeta = Warehouse.routes[postcard.path]
?: throw IllegalStateException("There is no route match the path [${postcard.path}]")
postcard.destination = routeMeta.destination
postcard.type = routeMeta.type
if (routeMeta.type == RouteType.PROVIDER) {
val providerClazz = routeMeta.destination as Class<IProvider>
var instance = Warehouse.providers[providerClazz]
if (instance == null) {
try {
val provider = providerClazz.getConstructor().newInstance()
provider.init(context)
Warehouse.providers[providerClazz] = provider
instance = provider
} catch (e: Exception) {
throw IllegalStateException("Init provider failed!")
}
}
postcard.provider = instance
}
}
}
修改一下 navigation() 函数,先执行 LogisticsCenter.completion(postcard) 补充信息,如果是 PROVIDER 类型就返回 postcard.provider。
object SimpleRouter {
// ...
fun putRoute(path: String, clazzName: String){
LogisticsCenter.putRoute(path, clazzName)
}
fun navigation(context: Context, postcard: Postcard, requestCode: Int = -1): Any? {
LogisticsCenter.completion(postcard)
return when (postcard.type) {
RouteType.ACTIVITY -> {
// ...
}
RouteType.FRAGMENT -> {
// ...
}
RouteType.PROVIDER -> postcard.provider
else -> null
}
}
}
我们在一个公共模块新建一个 AccountService 接口提供登录组件的功能,需要继承 IProvider 接口。
interface AccountService : IProvider {
val isSignIn: Boolean
fun logout()
}
然后在用户模块写一个 AccountService 接口的实现类,把功能实现出来。
class AccountServiceProvider : UserService {
override val isSignIn: Boolean
get() = // ...
override fun logout() {
// ...
}
override fun init(context: Context) = Unit
}
在路由表注册该类的路由信息。
// 以后会改成用 @Router 注解来初始化路由表
SimpleRouter.putRoute("/account/service", "com.dylanc.simplerouter.account.AccountServiceProvider")
之后就能通过路由工具得到该接口的实例。
val accountService = SimpleRouter.build("/account/service").navigation() as? AccountService
if (accountService?.isSignIn == true) {
// 已登录
} else {
// 未登录
}
小结一下,第四版在第三版的基础上,支持了模块间通信,能获取一个模块的某些信息或者让一个模块做某些事。增加了 IProvider 接口来区分类型,增加了 Warehouse 类持有缓存信息,增加了 LogisticsCenter 类初始化和读取缓存。
路由原理分析
目前我们已经实现了一个简易的路由框架并实现了常用的路由功能,代码虽然比较简单,但其实都是路由框架的核心代码,我们可以从中了解到路由的实现原理。
首先路由框架一般会用个 Map 作为路由表,初始化的时候会给路由表添加每个 path 对应的路由信息,包含 Class 对象、类型等。目前的简易路由框架是手动 put 数据,改成使用注解也只是能优化了用法,实际上做的事是一样的。
然后就是通过 navigation() 函数进行路由导航,会用 path 到路由表查出对应的路由信息。虽然可以直接返回 Class 对象,但是开发起来不太方便,所以会根据不同的类型去做对应的事:
- 如果 Class 对象是 Activity 类型,就给 intent 设置参数并调用 startActivity() 或者 startActivityForResult() 跳转页面;
- 如果 Class 对象是 Fragment 类型,就反射无参的构造函数实例化 Fragment 对象,并且给 arguments 设置参数;
- 如果 Class 对象是 IProvider 类型,也会反射无参的构造函数实例化对象,但是不需要多次实例化,所以会缓存起来;
大部分路由框架都是这样的路由流程,只是实现上会有些区别,大家可以将这个流程作为线索去阅读源码,看看完善的路由框架除了这些功能之外还会额外做些什么处理。
另外前面迭代的简易路由框架所实现的类和函数其实都尽量与 ARouter 的源码保持了一致,让大家顺便了解一下 ARouter 的整体结构可能是怎么样设计出来的,后续阅读源码也更加容易理解,ARouter 在面试中还是比较常会问到的。
总结
本文带着大家迭代了一个路由框架,从一个简单的工具类慢慢完善常见的路由场景,实现模块间跳转页面、创建 Fragment、通信。我们可以从中了解到路由的实现原理,其中最核心的是路由表,初始化的时候给路由表添加路由信息,然后路由导航的时候就能查出路由信息,再根据不同的类型去做不同的事情。大多数路由框架都是这样的流程,我们对此了解后就能更容易去阅读路由框架的源码。
下面整理了《Android 架构学习手册》+《深入理解Gradle框架》学习笔记,根据自己学习中所做的一些笔录来整的,主要也是方便后续好复习翻阅,省掉在去网上查找的时间,以免在度踩坑,如果大家有需要的可以直接通过 →点击此处← 进行参考学习:https://qr21.cn/CaZQLo?BIZ=ECOMMERCE