路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。
我个人的理解是,在前端开发中,路由就是能够将一个字符串映射到对应的业务。 APP的路由盒首先可以收集各个组件的路由,并生成路由表。 然后,它可以根据外部输入的字符串匹配路由表中相应的页面或服务,进行跳转或调用,并提供返回值等,表示如下:
因此,一个基本的路由框架必须具备以下能力:
1、APP路由扫描注册逻辑。
2.路由跳转页面能力。
3、路由呼叫服务能力。
在APP中,路由页面时,往往需要判断是否登录等额外的认证逻辑。 因此,还需要提供拦截逻辑,比如登录。
2、第三方路由框架对APP的要求是否强烈?
答:不会,系统原生提供路由功能,但功能较少。 稍大的应用程序使用三方路由框架。
系统本身提供页面跳转能力:比如对于工具类APP或者独立APP来说,这种方式完全足够了,不需要专门的路由框架。 那么为什么很多APP仍然使用路由框架呢? 这与APP的本质以及路由框架的优势有关。 例如,淘宝、京东、美团等大型APP无论是APP功能还是研发团队规模都非常庞大。 不同的业务往往由不同的团队维护,并采用基于组件的开发方式。 ,最后集成到APK中。
多个团队往往涉及业务之间的互动,比如从电影票业务跳到食品业务。 但两个业务是两个独立的研发团队,代码实现完全隔离。 那么如何沟通呢? 首先想到的就是将其引入到代码中,但这会打破低耦合的初衷,还可能引入各种问题。
比如部分业务是外包团队完成的,这就涉及到代码安全问题,所以我们还是希望以黑匣子的方式来调用目标业务,这就需要中转路由支持,所以国内很多APP都采用框架式路由。 其次,我们的各种跳转规则不希望与具体的实现类相关。 比如当跳转比较详细的时候,我们并不想知道到底是实现了哪一个。 我们只需要将一个字符串映射到它即可。 这个适合H5,或者后端开发处理跳转的时候,非常标准。
3、原生路由的局限性:功能单一、扩展灵活性差、协作困难
传统的路由基本上仅限于路由或者启动服务。 比如传统路由有什么缺点:有两种用法,一种是显式的,一种是隐式的。 显式调用如下:
import com.snail.activityforresultexample.test.SecondActivity;
public class MainActivity extends AppCompatActivity {
void jumpSecondActivityUseClassName(){
Intent intent =new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
}
显式调用的缺点很明显,那就是必须严重依赖目标类的实现。 在某些场景下,尤其是开发大型APP组件时,出于安全考虑,某些业务逻辑不希望依赖于源码或aar。 在这种情况下,显式依赖就没有办法解决了。 我们来看看隐式调用方法。
步骤1:在配置中至少配置一个-。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.snail.activityforresultexample">
<application
...
<activity android:name=".test.SecondActivity">
<intent-filter>
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="com.snail.activityforresultexample.SecondActivity" />
intent-filter>
activity>
application>
manifest>
第二步:打电话。
void jumpSecondActivityUseFilter() {
Intent intent = new Intent();
intent.setAction("com.snail.activityforresultexample.SecondActivity");
startActivity(intent);
}
如果涉及到数据传输写入,那就比较复杂了。 隐式调用的缺点如下:
首先,定义复杂,会导致暴露的协议变得复杂,难以维护和扩展。
其次,需要不同的配置,每次添加或删除都会很麻烦。 这对于开发者来说非常不友好,并且使得协作变得更加困难。
最后,不建议将所有属性设置为 True。 这是降低风险的一种方法。 一般都是归为一类,统一处理跳转。 在这种场景下,它既具有路由功能,又具有隐式调用场景。 这种情况下,新增和删除必然需要每次调整路由表,从而导致开发效率降低、风险增加。
可见,系统原生的路由框架并没有过多考虑团队协作的开发模式。 主要限于模块内多个业务之间的直接相互引用。 它基本上需要代码级的依赖,这对代码和业务隔离非常不友好。 如果不考虑之前Dex方法树的过度限制,你可以认为三方路由框架完全是为了团队协作而创建的。
4、APP三方路由框架所需的能力
目前市面上的大部分路由框架都可以解决上述问题。 简单总结一下目前的三方路由能力可以总结如下:
路由表生成能力:业务组件【UI业务和服务】自动扫描注册逻辑,需要良好的扩展性,且不需要侵入原有代码逻辑。
逻辑与业务映射:无需依赖具体实现实现代码隔离。
基本路由跳转能力:支持页面跳转能力。
对服务组件的支持:比如去某个服务组件获取一些配置等。
[扩展] 路由拦截逻辑:如登录、统一认证等。
可定制的降级逻辑:找不到组件时的解决方案。
可以看到接下来的典型用法,第一步:在新页面中添加语句。
@Route(path = "/test/activity2")
public class Test2Activity extends AppCompatActivity {
...
}
在构建阶段,将根据注释收集路由并生成路由表。 第二步使用:
ARouter.getInstance()
.build("/test/activity2")
.navigation(this);
如上,在该框架下,只需要字符串,无需依赖任何东西就可以实现路由跳转。
5、APP路由框架实现
路由框架实现的核心是能够与组件[或其他服务]建立映射关系,即路由表,并根据路由表路由到相应的组件。 其实分为两部分,第一部分是路由表的生成,第二部分是路由表的查询。
自动生成路由表
生成路由表的方法有很多种。 最简单的是维护一个公共文件或类,其中映射每个实现组件。
但这种方式的缺点也很明显:每次增删改查都必须修改表,这对协作非常不友好,不符合解决协作问题的初衷。 然而,最终的路由表遵循这一路径,即将所有内容收集到一个对象中。 唯一的区别是实现方法。 目前三方路由框架几乎都采用注解+APT【Tool】工具+AOP(--,面向切面编程),基本流程如下:
涉及的技术包括注解、APT(工具)、AOP(面向切面编程)。 APT比较常用,主要是遍历所有类,找到带注释的Java类,然后聚合生成路由表。 由于可能有很多组件,因此可能有多个路由表。 之后,这些生成的辅助类将包含在源代码中。 它被编译成class文件,然后使用AOP技术[比如ASM或者]扫描这些生成的类,聚合路由表,填充到前面的占位符方法中,完成自动注册的逻辑。
如何收集并生成路由表集合?
以框架为例,首先定义框架所需的注解如:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
/**
* Path of route
*/
String path();
该注解用于标记需要路由的组件。 用法如下:
@Route(path = "/test/activity1", name = "测试用 Activity")
public class Test1Activity extends BaseActivity {
@Autowired
int age = 10;
然后使用APT扫描所有带注释的类并生成路由表。 实现参考如下:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (CollectionUtils.isNotEmpty(annotations)) {
Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
this.parseRoutes(routeElements);
...
return false;
}
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
...
// Generate groups
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);
产品如下:包括路由表和本地注册条目。
自动注册:ASM收集上述路由表并将其聚合到Init代码区域中。
为了能够插入到Init代码区,首先需要预留一个位置,通常是定义一个空函数,以便稍后填充:
public class RouterInitializer {
public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) {
...
loadRouterTables();
}
//自动注册代码
public static void loadRouterTables() {
}
}
首先,使用AOP工具遍历上述APT中间产品,聚合路由表,并将其注册到预留的初始化位置。 遍历过程涉及多个过程。
收集目标并聚合路由表
/**扫描jar*/
fun scanJar(jarFile: File, dest: File?) {
val file = JarFile(jarFile)
var enumeration = file.entries()
while (enumeration.hasMoreElements()) {
val jarEntry = enumeration.nextElement()
if (jarEntry.name.endsWith("XXRouterTable.class")) {
val inputStream = file.getInputStream(jarEntry)
val classReader = ClassReader(inputStream)
if (Arrays.toString(classReader.interfaces)
.contains("IHTRouterTBCollect")
) {
tableList.add(
Pair(
classReader.className,
dest?.absolutePath
)
)
}
inputStream.close()
} else if (jarEntry.name.endsWith("HTRouterInitializer.class")) {
registerInitClass = dest
}
}
file.close()
}
将路由表初始化代码注入到目标Class中
fun asmInsertMethod(originFile: File?) {
val optJar = File(originFile?.parent, originFile?.name + ".opt")
if (optJar.exists())
optJar.delete()
val jarFile = JarFile(originFile)
val enumeration = jarFile.entries()
val jarOutputStream = JarOutputStream(FileOutputStream(optJar))
while (enumeration.hasMoreElements()) {
val jarEntry = enumeration.nextElement()
val entryName = jarEntry.getName()
val zipEntry = ZipEntry(entryName)
val inputStream = jarFile.getInputStream(jarEntry)
//插桩class
if (entryName.endsWith("RouterInitializer.class")) {
//class文件处理
jarOutputStream.putNextEntry(zipEntry)
val classReader = ClassReader(IOUtils.toByteArray(inputStream))
val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
val cv = RegisterClassVisitor(Opcodes.ASM5, classWriter,tableList)
classReader.accept(cv, EXPAND_FRAMES)
val code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
//结束
jarOutputStream.close()
jarFile.close()
if (originFile?.exists() == true) {
Files.delete(originFile.toPath())
}
optJar.renameTo(originFile)
}
最后将.class修改为如下填充代码:
public static void loadRouterTables() {
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
...
}
这样就完成了路由表的收集和注册,这就是大致的流程。 当然,支持服务等方面略有差异,但大体相似。
服务组件的框架支持
通过路由获取服务是APP路由独有的能力。 比如有一个用户中心组件,我们可以通过路由查询用户是否登录。 这并不是狭义上的页面路由的概念。 如何通过a找到字符串中对应的组件并调用其方法? 实现这一点的方法有很多种,每种实现都有自己的优点和缺点。
一种是将服务抽象成接口下沉到底层,上层通过路由实现映射对象。
一种是直接通过路由来映射实现方法。
让我们看看第一种实现方法。 它的优点是所有对外暴露的服务都暴露接口类【沉到底】,这对于外部的调用者,也就是服务的使用者来说非常友好。 示例如下:
先定义抽象服务,下沉到底层
public interface HelloService extends IProvider {
void sayHello(String name);
}
实现服务并用注释标记它们。
@Route(path = "/yourservicegroupname/hello")
public class HelloServiceImpl implements HelloService {
Context mContext;
@Override
public void sayHello(String name) {
Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
}
使用方法:使用Add获取服务实例,映射到抽象类,然后直接调用方法。
((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");
这种实现方式对于用户来说其实是非常方便的,尤其是当一个服务有多种可操作方法的时候,但是缺点是可扩展性。 如果要扩展方法,则需要更改底层库。
我们看第二种:直接通过路由映射实现方法
服务调用必须落在方法上。 参考页面路由,也可以支持方法路由。 两者是并行关系,所以我们主要添加一个方法路由表。 实现原理与Page 类似。 与上面相比,不需要定义抽象层。 直接定义实现即可:
定义
public class HelloService {
@MethodRouter(url = {"arouter://sayhello"})
public void sayHello(String name) {
Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
}
只需使用它
RouterCall.callMethod("arouter://sayhello?name=hello");
上述缺点是外部调用有些复杂,尤其是处理参数时,需要严格按照协议处理。 优点是没有抽象层。 如果需要扩展服务方法,不需要改变底层。
以上两种方法各有优缺点。 不过,如果从制作服务组件的初衷出发,第一种方法更好:对调用者更友好。 另外,对于支持来说,处理方式可能更方便,可以更方便地留给服务提供商来定义。 如果是第二种,直接通过路由映射来处理服务会比较麻烦,尤其是里面的参数可能需要统一封装成JSON并维护解析的协议。 这可能不太好。
路由表匹配
路由表的匹配比较简单,就是根据全局映射中的输入来匹配目标组件,然后依靠反射等常用操作来定位目标。
6.组件化与路由的关系
组件化是一种开发集成模式,更像是一种开发规范,更方便团队协作开发。 组件化的最终实现是独立的业务和功能组件。 这些组件可能由不同的团队出于不同的目的进行维护,甚至需要代码隔离。 如果涉及到组件之间的调用和通信,就不能避免使用路由,因为要实现隔离,只能使用公共字符串进行通信。 这就是路由的功能范围。
组件化需要路由支持的根本原因:组件之间代码实现的隔离。
总结
路由不是APP的必备功能,但大型跨团队的APP基本需要。
路由框架基础能力:路由自动注册、路由表收集、服务和UI界面路由拦截等核心功能。
组件化与路由的关系:组件化的代码隔离使得路由框架成为必要。