-
- APP路由的扫描及注册逻辑
-
- 路由跳转target页面能力
-
- 路由调用target服务能力
APP中,在进行页面路由的时候,经常需要判断是否登录等一些额外鉴权逻辑所以,还需要提供拦截逻辑等,比如:登陆。
答案:不是,系统原生提供路由能力,但功能较少,稍微大规模的APP都采用三方路由框架。
Android系统本身提供页面跳转能力:如startActivity,对于工具类APP,或单机类APP,这种方式已经完全够用,完全不需要专门的路由框架,那为什么很多APP还是采用路由框架呢?这跟APP性质及路由框架的优点都有关。比如淘宝、京东、美团等这些大型APP,无论是从APP功能还是从其研发团队的规模上来说都很庞大,不同的业务之间也经常是不同的团队在维护,采用组件化的开发方式,最终集成到一个APK中。多团队之间经常会涉及业务间的交互,比如从电影票业务跳转到美食业务,但是两个业务是两个独立的研发团队,代码实现上是完全隔离的,那如何进行通信呢?首先想到的是代码上引入,但是这样会打破了低耦合的初衷,可能还会引入各种问题。例如,部分业务是外包团队来做,这就牵扯到代码安全问题,所以还是希望通过一种类似黑盒的方式,调用目标业务,这就需要中转路由支持,所以国内很多APP都是用了路由框架的。其次我们各种跳转的规则并不想跟具体的实现类扯上关系,比如跳转商详的时候,不希望知道是哪个Activity来实现,只需要一个字符串映射过去即可,这对于H5、或者后端开发来处理跳转的时候,就非常标准。
传统的路由基本上就限定在startActivity、或者startService来路由跳转或者启动服务。拿startActivity来说,传统的路由有什么缺点:startActivity有两种用法,一种是显示的,一种是隐式的,显示调用如下:
import com.snail.activityforresultexample.test.SecondActivity;
public class MainActivity extends AppCompatActivity {
void jumpSecondActivityUseClassName(){
Intent intent =new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
}
显示调用的缺点很明显,那就是必须要强依赖目标Activity的类实现,有些场景,尤其是大型APP组件化开发时候,有些业务逻辑出于安全考虑,并不想被源码或aar依赖,这时显式依赖的方式就无法走通。再来看看隐式调用方法。
第一步:manifest中配置activity的intent-filter,至少要配置一个action
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android=“http://schemas.android.com/apk/res/android”
package=“com.snail.activityforresultexample”>
<application
…
第二步:调用
void jumpSecondActivityUseFilter() {
Intent intent = new Intent();
intent.setAction(“com.snail.activityforresultexample.SecondActivity”);
startActivity(intent);
}
如果牵扯到数据传递写法上会更复杂一些,隐式调用的缺点有如下几点:
-
首先manifest中定义复杂,相对应的会导致暴露的协议变的复杂,不易维护扩展。
-
其次,不同Activity都要不同的action配置,每次增减修改Activity都会很麻烦,对比开发者非常不友好,增加了协作难度。
-
最后,Activity的export属性并不建议都设置成True,这是降低风险的一种方式,一般都是收归到一个Activity,DeeplinkActivitiy统一处理跳转,这种场景下,DeeplinkActivitiy就兼具路由功能,隐式调用的场景下,新Activitiy的增减势必每次都要调整路由表,这会导致开发效率降低,风险增加。
可以看到系统原生的路由框架,并没太多考虑团队协同的开发模式,多限定在一个模块内部多个业务间直接相互引用,基本都要代码级依赖,对于代码及业务隔离很不友好。如不考虑之前Dex方法树超限制,可以认为三方路由框架完全是为了团队协同而创建的。
目前市面上大部分的路由框架都能搞定上述问题,简单整理下现在三方路由的能力,可归纳如下:
-
路由表生成能力:业务组件**[UI业务及服务]**自动扫描及注册逻辑,需要扩展性好,无需入侵原有代码逻辑
-
scheme与业务映射逻辑 :无需依赖具体实现,做到代码隔离
-
基础路由跳转能力 :页面跳转能力的支持
-
服务类组件的支持 :如去某个服务组件获取一些配置等
-
[扩展]路由拦截逻辑:比如登陆,统一鉴权
-
可定制的降级逻辑:找不到组件时的兜底
可以看下一个典型的Arouter用法,第一步:对新增页面添加Router Scheme 声明,
@Route(path = “/test/activity2”)
public class Test2Activity extends AppCompatActivity {
…
}
build阶段会根据注解搜集路由scheme,生成路由表。第二步使用
ARouter.getInstance()
.build("/test/activity2")
.navigation(this);
如上,在ARouter框架下,仅需要字符串scheme,无需依赖任何Test2Activity就可实现路由跳转。
路由框架实现的核心是建立scheme和组件**[Activity或者其他服务]**的映射关系,也就是路由表,并能根据路由表路由到对应组件的能力。其实分两部分,第一部分路由表的生成,第二部分,路由表的查询
路由表的自动生成
生成路由表的方式有很多,最简单的就是维护一个公共文件或者类,里面映射好每个实现组件跟scheme,
不过,这种做法缺点很明显:每次增删修改都要都要修改这个表,对于协同非常不友好,不符合解决协同问题的初衷。不过,最终的路由表倒是都是这条路,就是将所有的Scheme搜集到一个对象中,只是实现方式的差别,目前几乎所有的三方路由框架都是借助注解+APT[Annotation Processing Tool]工具+AOP(Aspect-Oriented Programming,面向切面编程)来实现的,基本流程如下:
其中牵扯的技术有注解、APT(Annotation Processing Tool)、AOP(Aspect-Oriented Programming,面向切面编程)。APT常用的有JavaPoet,主要是遍历所有类,找到被注解的Java类,然后聚合生成路由表,由于组件可能有很多,路由表可能也有也有多个,之后,这些生成的辅助类会跟源码一并被编译成class文件,之后利用AOP技术【如ASM或者JavaAssist】,扫描这些生成的class,聚合路由表,并填充到之前的占位方法中,完成自动注册的逻辑。
JavaPoet如何搜集并生成路由表集合?
以ARouter框架为例,先定义Router框架需要的注解如:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
/**
- Path of rout
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
e
*/
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中间产物,聚合路由表,并注册到预留初始化位置,遍历的过程牵扯是gradle transform的过程,
- 搜集目标,聚合路由表
/*扫描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)