手写一个路由框架实现页面跳转

目录

背景:

方式:

在安卓开发中,实现页面跳转有哪几种方式呢?

路由框架实现页面跳转的原理

 让我们来一步一步的见证一个框架的成长

第一个版本

第二个版本

第三个版本

总结


背景:

        在组件化和模块化中,业务模块之间不能存在相互依赖,那它们如何交互呢?


方式:

  1. 使用EventBus,通过发送事件、订阅事件来完成
    1. 维护成本高:EventBus 使用事件来传递信息,这意味着你需要定义大量的事件类和处理这些事件的订阅方法。随着应用的复杂度增加,事件的数量会迅速增加,导致代码变得难以管理和理解。
    2. 不便于管理:EventBus 的事件机制是全局的,这意味着任何地方都可以发送和接收事件。这种全局性使得事件的管理变得困难,尤其是在大型项目中。可能会导致事件被意外接收或处理,造成逻辑混乱。
    3. 容易出现内存泄漏:EventBus 的订阅者需要注册和注销,如果开发者忘记在适当的时机注销订阅者,可能会导致内存泄漏。
  2. 使用广播,通过发送广播、接收广播来完成
    1. 安全性问题:广播是全局的,任何应用程序都可以发送和接收广播。这意味着广播消息可能会被恶意应用程序截获或篡改
    2. 维护成本高:与 EventBus 类似,广播机制也需要维护大量的广播接收器和广播消息。随着应用的复杂度增加,广播的数量会迅速增加,导致代码变得难以管理和理解。
    3. 难以调试:由于广播是全局的,发送和接收广播的代码可能分散在不同的模块和组件中,这使得调试变得困难。
  3. 使用隐式意图
    1. 安全性问题
    2. 难以调试
    3. 维护成本高
  4. 使用类加载方式
    1. 性能开销:类加载方式涉及到加载、链接和初始化类,这些操作都需要消耗 CPU 和内存资源。
    2. 容易写错:

  5. 使用路由框架
    1. 便于调试和错误处理:路由框架通常提供丰富的日志和错误处理机制,使得调试和排查问题更加容易
    2. 简化页面跳转:路由框架通常提供简洁的 API 来执行页面跳转,减少了编写大量 Intent 相关代码的需求
    3. 性能开销:路由框架在执行页面跳转时,需要进行额外的处理,例如参数解析、路由匹配等。这些操作可能会带来一定的性能开销。
    4. 潜在的滥用风险:路由框架提供了灵活的页面跳转机制,但也可能导致滥用,例如滥用动态跳转规则,导致页面跳转逻辑混乱。

在安卓开发中,实现页面跳转有哪几种方式呢?

  • 显示意图:
    • 在组件化中,不适用,因为组件之间不能相互依赖,无法导入跳转的目标类
    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);

路由框架实现页面跳转的原理

  1. 将所有模块的Activity都注册到一个路由表中,当某个模块需要进行跳转时,就到路由表中找到对应的class,进行跳转

 让我们来一步一步的见证一个框架的成长

  1. 在java中,一个home对应一个HomeActivity、一个product对应一个ProductActivity…这种一对一的关系叫做映射关系,一个key对应一个value,所以我们的路由表是一个map

  2. 必须只有一份路由表,所以使用单例

  3. 我们这里写个简单的单例,不考虑性能,偷懒一下,使用饿汉式

  4. 这段代码很简单,路由表的类型是map,startActivity是通过传进来的path查找对应的key,如果存在,则进行跳转,否则报错

第一个版本
  1. 创建一个方法,用来注册各个模块的信息

  2. 这个方法很简单,就是将信息直接存到map中
  3. 使用:在app壳工程中,进行注册各个模块的Activity

  4. 可以看到,这个方法是不是很麻烦,需要手动添加,如果有100个Activity,就需要在Application中写100行代码
  5. Application负担太重了,太臃肿了
  6. 接下来开始优化,Application减负,注册代码下沉(我们之前是将注册代码写在了app壳的Application中,现在我们让业务模块自己去注册,这就叫做代码下沉)
第二个版本
  1. 大家想一下下面这个方法可行吗?

  2. 这个代码的意思是当进入HomeActivity时,才会进行注册,根据我们之前写的代码(Router),如果AActivity想跳转到HomeActivity,是不是就会报错,这种属于注册不及时
  3. 可见这种方法是不行的
  4. 我们为每个模块创建一个注册类,专门负责路由信息数据注册的
  5. 在java中,我们怎么给很多的类制定一个统一的规则、统一的标准?使用接口
  6. 我们在arouter-api库中添加一个接口,这是由路由框架提供的,只要实现了该接口的类,就说明该类是专门负责路由信息数据注册的。

  7. 在各个模块中实现该接口

  8. 那么问题来了,我们该如何调用呢?
  9. 在Application中调用?

  10. 我们的路由表私有化了,外部无法访问
  11. 所以只能在Router中修改代码了。
  12. 提供一个init方法,让外部调用,表示初始化路由表

  13. 在Router如何获取到HomeRouter、PersonalRouter、OrderRouter....?
  14. 我们要搞清楚我们现在在干什么,在写一个框架,框架知道你的业务是什么吗?每个项目都一定有HomeRouter、PersonalRouter、OrderRouter这些吗?不会吧
  15. 那我们怎么做呢?
  16. 在安卓开发中,我们需要知道,我们写的java代码,会被编译成class文件,然后打成dex文件,放到apk文件,这说明什么?说明apk文件中有我们所有的类
  17. 所以我们需要获取当前程序在手机上的对应的apk文件,然后再从apk中查找我们需要的类
  18. 怎么获取当前程序在手机上的对应的apk文件?
    1. apk是包,那在安卓系统中,谁管理包?谁处理APK安装、校验、签名...? 是PackageManagerService
    2. 代码:

    3. ApplicationInfo是 Android 中的一个重要类,它提供了关于已安装应用程序的详细信息。这些信息包括应用程序的包名、图标、标签、安装路径、版本号等
    4. sourceDir,用于获取应用程序的主 APK 文件路径。
    5. splitSourceDirs用于获取应用程序的分包路径(在传统的 APK 格式中,一个 APK 包含了应用程序的所有资源和代码。然而,这种方法在某些情况下会导致 APK 文件过大,为了解决这个问题,Google 引入了 Android App Bundle (AAB) 格式。AAB 格式允许开发者将应用程序的不同部分打包成多个 APK 文件,这些 APK 文件可以根据用户设备的需求动态下载和安装,从而减少下载和安装的应用程序大小
  19. 最后就是遍历apk文件中的所有dex文件,查找需要的类
  20. 代码:

  21. 从安卓的类加载我们知道,可以使用DexFile这个类来遍历apk文件中的所有dex文件。
  22. 最终className返回的值是类全限定名:com.xxx.xxx.xxx...类名
  23. 但是我们怎么找到我们当前需要的类呢?
  24. 使用反射创建实例,然后判断类型?肯定不行吧,太好性能了,要创建那么多那么多的实例
  25. 那还有什么办法呢?
  26. 我们定义一个标准,如果开发者想用我们这个框架,就必须将类放到某个包下,这样子我们查找类的时候就方便许多了?
  27. 我现在需要使用者将类放到com.example.routers包下
  28. 然后我们在遍历dex文件时,判断类全限定名是否以com.example.routers开头的,就可以了。

  29. 然后在init方法中调用对应函数

  30. 这样第二个版本就完成了
  31. 大家可以想一下这个版本存在哪些问题?
    1. 标准太多了吧?我们开发框架为什么要为我们自己考虑呢?我们应该为使用者考虑吧,使用我们这个框架,要实现接口,要放到指定的包下面,使用者用着头疼。
    2. 耗性能吧?哪里呢?在Router的init方法中,我们需要先找到apk文件,然后遍历apk下的所有dex文件,查找我们想要的类。
  32. 我们先优化使用者的接入成本,最后再优化性能
第三个版本
  1. 我们来看看实现IRouteLoad接口的类,有什么相同点,有什么不同点

  2. 可以看到每个模块实现IRouteLoad接口的类的类结构都是相似的,也就是put的值不同,类名不同,就相当于一个模板代码。
    1. 在安卓开发中,大家应该听说过ButterKnife吧,它主要用于减少在Android开发中编写大量的样板代码,我们初次学习安卓时,使用findViewById来完成视图绑定,有20个控件,就要写20个findViewById,那么这20个findViewById有什么异同?方法都是相同的,也就是值不同,这种也叫做模板代码,所以ButterKnife就解决了这个问题,我们只需要添加一个注解,就可以完成视图绑定,它是如何做的呢?它使用了apt技术、自动生成代码
  3. 那我们也可以这样做,为使用者自动生成代码,降低使用者的接入成本
  4. 自动生成代码可以选择在编译阶段生成、在运行阶段生成
    1. 编译阶段:APT
    2. 运行阶段:动态代理
  5. 我们这里使用在编译阶段自动生成代码,因为它减少运行时的开销
  6. APT——注解处理器,是javac提供的一个工具,它就相当于一个监听器,当javac编译java文件时,有指定的注解时(代码中使用了@A注解,注解处理器处理@A注解),就会回调注解处理器代码。
  7. 我们这里就不细讲APT技术了,我们这里的主角是手写一个路由框架
  8. 那我们创建一个java library,定义为router-complier,创建一个注解模块router-annotations
  9. 在router-annotations下,创建一个注解

  10. 在router-complier下,创建一个注解器

  11. 在需要使用注解和注解处理器的模块中导入依赖

  12. 为各个模块的Activity添加注解,我们这里的value格式就设置为:/模块/Activity类名(随意设置,但是要保证唯一,不然后续跳转时,会出现跳转错乱、报错)

  13. 然后我们来分析一下我们的模板代码

  14. 只需要更改:类名、loadInto方法中put的值(还有可能会有多个put方法)
  15. 首先我们看看类名咋改,我们要知道我们现在是在写框架,是不知道业务的,所以我们需要各个模块将模块名传过来或者说我们定义一个标准,使用注解时,value的值必须是:/模块/xxxx
    1. 模块中如何传值给注解处理器呢?在各个模块的build.gradle下设置

  16. 我们使用第二种方式——定义一个标准,使用注解时,value的值必须是:/模块/xxxx
  17. 我们这里使用javapoet生成代码
  18. 首先是获取一些我们需要的数据:@Route的value值、从value中提取出模块名、获取使用@Route注解的元素

  19. 然后就是生成类、方法

  20. 然后使用javaPoet生成java文件,我们默认生成到com.example.routers包下面(为什么?这个路径其实是可以更改的,随便设置,但是要和Router中查找类的路径要相同,不然查找不到我们自动生成的类)

  21. 这样就完成了自动生成代码,使用者只需要添加一个注解并且value的格式没有问题,那么就可以使用了
  22. 但是当前这个框架是很耗性能的
  23. 大家知道哪个地方耗性能吗?
  24. 那就是找类的时候了,它会遍历所有dex文件

  25. 那如何优化呢?
  26. 使用字节码插桩,这个技术博主还没有学,等下次吧hhhhh
  27. 本文的内容在此告一段落

总结

  1. 相信大家现在已经知道框架是怎么做出来的吧,先完成需求,再慢慢优化
  2. 我们这个框架用到了:APT、反射、PMS、类加载、javapoet等技术
  3. 学完这个路由框架,我们要记住:apt != 生成文件(apt中什么都可以做)、路由 != apt(路由中使用到了apt技术,我们是使用apt技术优化框架,降低接入成本)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

b顶峰相见

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值