目录
背景:
在组件化和模块化中,业务模块之间不能存在相互依赖,那它们如何交互呢?
方式:
- 使用EventBus,通过发送事件、订阅事件来完成
- 维护成本高:EventBus 使用事件来传递信息,这意味着你需要定义大量的事件类和处理这些事件的订阅方法。随着应用的复杂度增加,事件的数量会迅速增加,导致代码变得难以管理和理解。
- 不便于管理:EventBus 的事件机制是全局的,这意味着任何地方都可以发送和接收事件。这种全局性使得事件的管理变得困难,尤其是在大型项目中。可能会导致事件被意外接收或处理,造成逻辑混乱。
- 容易出现内存泄漏:EventBus 的订阅者需要注册和注销,如果开发者忘记在适当的时机注销订阅者,可能会导致内存泄漏。
- 使用广播,通过发送广播、接收广播来完成
- 安全性问题:广播是全局的,任何应用程序都可以发送和接收广播。这意味着广播消息可能会被恶意应用程序截获或篡改
- 维护成本高:与 EventBus 类似,广播机制也需要维护大量的广播接收器和广播消息。随着应用的复杂度增加,广播的数量会迅速增加,导致代码变得难以管理和理解。
- 难以调试:由于广播是全局的,发送和接收广播的代码可能分散在不同的模块和组件中,这使得调试变得困难。
- 使用隐式意图
- 安全性问题
- 难以调试
- 维护成本高
- 使用类加载方式
- 性能开销:类加载方式涉及到加载、链接和初始化类,这些操作都需要消耗 CPU 和内存资源。
- 容易写错:
- 使用路由框架
- 便于调试和错误处理:路由框架通常提供丰富的日志和错误处理机制,使得调试和排查问题更加容易
- 简化页面跳转:路由框架通常提供简洁的 API 来执行页面跳转,减少了编写大量 Intent 相关代码的需求
- 性能开销:路由框架在执行页面跳转时,需要进行额外的处理,例如参数解析、路由匹配等。这些操作可能会带来一定的性能开销。
- 潜在的滥用风险:路由框架提供了灵活的页面跳转机制,但也可能导致滥用,例如滥用动态跳转规则,导致页面跳转逻辑混乱。
在安卓开发中,实现页面跳转有哪几种方式呢?
- 显示意图:
- 在组件化中,不适用,因为组件之间不能相互依赖,无法导入跳转的目标类
import com.xxxx.xxxx.XXXActivity Intent intent = new Intent(this,XXXActivity.classs); startActivity(intent);
- 隐式意图:
-
维护成本高、难以调试
在AndroidMenifest.xml中,设置Activity的action 使用: Intent intent = new Intent(action:"xxxxxxxx"); startActivity(intent);
-
路由框架实现页面跳转的原理
- 将所有模块的Activity都注册到一个路由表中,当某个模块需要进行跳转时,就到路由表中找到对应的class,进行跳转
让我们来一步一步的见证一个框架的成长
-
在java中,一个home对应一个HomeActivity、一个product对应一个ProductActivity…这种一对一的关系叫做映射关系,一个key对应一个value,所以我们的路由表是一个map
-
必须只有一份路由表,所以使用单例
-
我们这里写个简单的单例,不考虑性能,偷懒一下,使用饿汉式
-
这段代码很简单,路由表的类型是map,startActivity是通过传进来的path查找对应的key,如果存在,则进行跳转,否则报错
第一个版本
- 创建一个方法,用来注册各个模块的信息
- 这个方法很简单,就是将信息直接存到map中
- 使用:在app壳工程中,进行注册各个模块的Activity
- 可以看到,这个方法是不是很麻烦,需要手动添加,如果有100个Activity,就需要在Application中写100行代码
- Application负担太重了,太臃肿了
- 接下来开始优化,Application减负,注册代码下沉(我们之前是将注册代码写在了app壳的Application中,现在我们让业务模块自己去注册,这就叫做代码下沉)
第二个版本
- 大家想一下下面这个方法可行吗?
- 这个代码的意思是当进入HomeActivity时,才会进行注册,根据我们之前写的代码(Router),如果AActivity想跳转到HomeActivity,是不是就会报错,这种属于注册不及时
- 可见这种方法是不行的
- 我们为每个模块创建一个注册类,专门负责路由信息数据注册的
- 在java中,我们怎么给很多的类制定一个统一的规则、统一的标准?使用接口
- 我们在arouter-api库中添加一个接口,这是由路由框架提供的,只要实现了该接口的类,就说明该类是专门负责路由信息数据注册的。
- 在各个模块中实现该接口
- 那么问题来了,我们该如何调用呢?
- 在Application中调用?
- 我们的路由表私有化了,外部无法访问
- 所以只能在Router中修改代码了。
- 提供一个init方法,让外部调用,表示初始化路由表
- 在Router如何获取到HomeRouter、PersonalRouter、OrderRouter....?
- 我们要搞清楚我们现在在干什么,在写一个框架,框架知道你的业务是什么吗?每个项目都一定有HomeRouter、PersonalRouter、OrderRouter这些吗?不会吧
- 那我们怎么做呢?
- 在安卓开发中,我们需要知道,我们写的java代码,会被编译成class文件,然后打成dex文件,放到apk文件,这说明什么?说明apk文件中有我们所有的类
- 所以我们需要获取当前程序在手机上的对应的apk文件,然后再从apk中查找我们需要的类
- 怎么获取当前程序在手机上的对应的apk文件?
- apk是包,那在安卓系统中,谁管理包?谁处理APK安装、校验、签名...? 是PackageManagerService
- 代码:
- ApplicationInfo是 Android 中的一个重要类,它提供了关于已安装应用程序的详细信息。这些信息包括应用程序的包名、图标、标签、安装路径、版本号等
- sourceDir,用于获取应用程序的主 APK 文件路径。
- splitSourceDirs用于获取应用程序的分包路径(在传统的 APK 格式中,一个 APK 包含了应用程序的所有资源和代码。然而,这种方法在某些情况下会导致 APK 文件过大,为了解决这个问题,Google 引入了 Android App Bundle (AAB) 格式。AAB 格式允许开发者将应用程序的不同部分打包成多个 APK 文件,这些 APK 文件可以根据用户设备的需求动态下载和安装,从而减少下载和安装的应用程序大小)
- 最后就是遍历apk文件中的所有dex文件,查找需要的类
- 代码:
- 从安卓的类加载我们知道,可以使用DexFile这个类来遍历apk文件中的所有dex文件。
- 最终className返回的值是类全限定名:com.xxx.xxx.xxx...类名
- 但是我们怎么找到我们当前需要的类呢?
- 使用反射创建实例,然后判断类型?肯定不行吧,太好性能了,要创建那么多那么多的实例
- 那还有什么办法呢?
- 我们定义一个标准,如果开发者想用我们这个框架,就必须将类放到某个包下,这样子我们查找类的时候就方便许多了?
- 我现在需要使用者将类放到com.example.routers包下
- 然后我们在遍历dex文件时,判断类全限定名是否以com.example.routers开头的,就可以了。
- 然后在init方法中调用对应函数
- 这样第二个版本就完成了
- 大家可以想一下这个版本存在哪些问题?
- 标准太多了吧?我们开发框架为什么要为我们自己考虑呢?我们应该为使用者考虑吧,使用我们这个框架,要实现接口,要放到指定的包下面,使用者用着头疼。
- 耗性能吧?哪里呢?在Router的init方法中,我们需要先找到apk文件,然后遍历apk下的所有dex文件,查找我们想要的类。
- 我们先优化使用者的接入成本,最后再优化性能
第三个版本
- 我们来看看实现IRouteLoad接口的类,有什么相同点,有什么不同点
- 可以看到每个模块实现IRouteLoad接口的类的类结构都是相似的,也就是put的值不同,类名不同,就相当于一个模板代码。
- 在安卓开发中,大家应该听说过ButterKnife吧,它主要用于减少在Android开发中编写大量的样板代码,我们初次学习安卓时,使用findViewById来完成视图绑定,有20个控件,就要写20个findViewById,那么这20个findViewById有什么异同?方法都是相同的,也就是值不同,这种也叫做模板代码,所以ButterKnife就解决了这个问题,我们只需要添加一个注解,就可以完成视图绑定,它是如何做的呢?它使用了apt技术、自动生成代码
- 那我们也可以这样做,为使用者自动生成代码,降低使用者的接入成本
- 自动生成代码可以选择在编译阶段生成、在运行阶段生成
- 编译阶段:APT
- 运行阶段:动态代理
- 我们这里使用在编译阶段自动生成代码,因为它减少运行时的开销
- APT——注解处理器,是javac提供的一个工具,它就相当于一个监听器,当javac编译java文件时,有指定的注解时(代码中使用了@A注解,注解处理器处理@A注解),就会回调注解处理器代码。
- 我们这里就不细讲APT技术了,我们这里的主角是手写一个路由框架
- 那我们创建一个java library,定义为router-complier,创建一个注解模块router-annotations
- 在router-annotations下,创建一个注解
- 在router-complier下,创建一个注解器
- 在需要使用注解和注解处理器的模块中导入依赖
- 为各个模块的Activity添加注解,我们这里的value格式就设置为:/模块/Activity类名(随意设置,但是要保证唯一,不然后续跳转时,会出现跳转错乱、报错)
- 然后我们来分析一下我们的模板代码
- 只需要更改:类名、loadInto方法中put的值(还有可能会有多个put方法)
- 首先我们看看类名咋改,我们要知道我们现在是在写框架,是不知道业务的,所以我们需要各个模块将模块名传过来或者说我们定义一个标准,使用注解时,value的值必须是:/模块/xxxx
- 模块中如何传值给注解处理器呢?在各个模块的build.gradle下设置
- 模块中如何传值给注解处理器呢?在各个模块的build.gradle下设置
- 我们使用第二种方式——定义一个标准,使用注解时,value的值必须是:/模块/xxxx
- 我们这里使用javapoet生成代码
- 首先是获取一些我们需要的数据:@Route的value值、从value中提取出模块名、获取使用@Route注解的元素
- 然后就是生成类、方法
- 然后使用javaPoet生成java文件,我们默认生成到com.example.routers包下面(为什么?这个路径其实是可以更改的,随便设置,但是要和Router中查找类的路径要相同,不然查找不到我们自动生成的类)
- 这样就完成了自动生成代码,使用者只需要添加一个注解并且value的格式没有问题,那么就可以使用了
- 但是当前这个框架是很耗性能的
- 大家知道哪个地方耗性能吗?
- 那就是找类的时候了,它会遍历所有dex文件
- 那如何优化呢?
- 使用字节码插桩,这个技术博主还没有学,等下次吧hhhhh
- 本文的内容在此告一段落
总结
- 相信大家现在已经知道框架是怎么做出来的吧,先完成需求,再慢慢优化
- 我们这个框架用到了:APT、反射、PMS、类加载、javapoet等技术
- 学完这个路由框架,我们要记住:apt != 生成文件(apt中什么都可以做)、路由 != apt(路由中使用到了apt技术,我们是使用apt技术优化框架,降低接入成本)