Android手写自己的路由SDK

news2024/11/25 16:50:10

实现自己的路由框架

​ 在较大型的Android app中常会用到组件化技术,针对不同的业务/基础功能对模块进行划分,从上到下为壳工程、业务模块、基础模块。其中业务模块依赖基础模块,壳工程依赖业务模块。同级的横向模块(比如多个业务模块)因为不能相互依赖,怎样实现它们之间的路由跳转呢?

​ 我尝试使用kotlin实现一下自己的路由框架,由简到繁、由浅入深。刚开始只求有功能,不求完美,一步步最终优化到比较完善的样子。

1.菜鸟版

在这里插入图片描述

​ 工程中只包含上图中的5个模块,其中main、businessa、businessb、routersdk均为Android Library,只有app是可运行的application。

​ app作为壳工程依赖着剩下的4个模块,main、businessa、businessb作为业务模块依赖着routersdk这个基础模块。

​ 此时依据模块之间的依赖关系,想要实现路由其实只需要在基础模块routersdk中创建一个Router类维护一个映射表并实现两个关键方法。

​ 一个方法register()用来注册路由跳转的键值对,键为path字符串,value为跳转的XXXActivity的class即可。

​ 另一个方法jumpTo()用来具体跳转,实现的时候传入对应的路由path参数,当path在映射表中时直接调用Intent的startActivity()方法完成跳转即可。

‘’

package com.lllddd.routersdk

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Toast

/**
 * author: lllddd
 * created on: 2024/5/2 14:34
 * description:路由类
 */
object Router {

    private val routerMap: MutableMap<String, Class<out Activity>> = mutableMapOf()

    fun register(key: String, value: Class<out Activity>) {
        routerMap[key] = value
    }

    fun jumpTo(activity: Activity, path: String, params: Bundle? = null) {
        if (!routerMap.containsKey(path)) {
            Toast.makeText(activity, "找不到路由目的页面!!!", Toast.LENGTH_LONG).show()
            return
        }

        val destinationActivity = routerMap[path]
        val intent = Intent(activity, destinationActivity)
        if (params != null) {
            intent.putExtras(params)
        }

        activity.startActivity(intent)
    }
}

​ 接着在Application的子类MyRouterApp中调用Router的注册方法,将3个业务模块的页面路由分别注册进Router中的路由表,那么路由的注册就已完成。

​ ‘’

package com.lllddd.myrouter.app

import android.app.Application
import com.lllddd.businessa.BusinessAMainActivity
import com.lllddd.businessb.BusinessBMainActivity
import com.lllddd.main.MainActivity
import com.lllddd.routersdk.Router

/**
 * author: lllddd
 * created on: 2024/5/2 15:06
 * description:
 */
class MyRouterApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Router.register("businessa/main", BusinessAMainActivity::class.java)
        Router.register("businessb/main", BusinessBMainActivity::class.java)
        Router.register("app/main", MainActivity::class.java)
    }
}

​ 上方我们注册了3条路由关系。businessa/main对应BusinessAMainActivity,businessb/main对应BusinessBMainActivity,app/main对应MainActivity。

​ 此时假如要想在app模块中的MainActivity页面路由到businessa模块的BusinessAMainActivity页面或businessb模块的BusinessBMainActivity页面,只需要如下这样写。

​ ‘’

package com.lllddd.main

import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import com.lllddd.routersdk.Router

class MainActivity : AppCompatActivity() {

    private lateinit var mBtnJumpA: Button
    private lateinit var mBtnJumpB: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initWidgets()

    }

    private fun initWidgets() {
        mBtnJumpA = findViewById(R.id.btn_jump_a)
        mBtnJumpA.setOnClickListener {
            val bundle = Bundle()
            bundle.putString("param", "好好学习")
            Router.jumpTo(this, "businessa/main", bundle)
        }

        mBtnJumpB = findViewById(R.id.btn_jump_b)
        mBtnJumpB.setOnClickListener {
            val bundle = Bundle()
            bundle.putString("param", "天天向上")
            Router.jumpTo(this, "businessb/main", bundle)
        }

    }
}

​ 此时我们只需要将path传给Router.jumpTo()作为参数就能正确跳转到同级别的业务模块中。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.进阶版

​ 菜鸟版方式很容易就让我实现了一个菜鸟版的路由框架。但是它存在一些很明显的问题,首先就是注册关系必须要在MyRouterApp(Application)类中去维护,那么在模块众多多人协作开发时完全没有解耦,造成了MyRouterApp难以维护,模块职责不清的问题。其次,app模块中实际上没有任何页面,只是一个壳工程,但是它也要依赖routersdk,这样的情况也不合理。

​ 我就在想能不能让路由关系注册这样的操作分散在各自的业务模块中,这样就能很好地解决上面两个问题。

​ 很自然的想到在routersdk模块中定义一个接口用来约束装载路由的方法。

‘’

package com.lllddd.routersdk

import android.app.Activity

/**
 * author: lllddd
 * created on: 2024/5/2 20:12
 * description:各个业务模块装载路由信息的接口
 */
interface ILoadRouters {
    /**
     * 装载路由信息
     *
     * @param loadRouters 路由表
     */
    fun loadRouters(routerMap: MutableMap<String, Class<out Activity>>)
}

​ 之后在每个业务模块中新建一个路由类实现该接口。

​ main模块中的实现类如下。

‘’

package com.lllddd.main.router

import android.app.Activity
import com.lllddd.main.MainActivity
import com.lllddd.routersdk.ILoadRouters

/**
 * author: lllddd
 * created on: 2024/5/2 20:21
 * description:业务main模块的路由装载类
 */
class MainRouter : ILoadRouters {
    override fun loadRouters(routerMap: MutableMap<String, Class<out Activity>>) {
        routerMap["main/main"] = MainActivity::class.java
    }
}

​ businessa模块中的实现类如下。

‘’

package com.lllddd.businessa.router

import android.app.Activity
import com.lllddd.businessa.BusinessAMainActivity
import com.lllddd.routersdk.ILoadRouters

/**
 * author: lllddd
 * created on: 2024/5/2 20:16
 * description:业务模块A的路由装载类
 */
class BusinessARouter : ILoadRouters {
    override fun loadRouters(routerMap: MutableMap<String, Class<out Activity>>) {
        routerMap["/businessa/main"] = BusinessAMainActivity::class.java
    }
}

​ businessb模块中的实现类如下。

‘’

package com.lllddd.businessb.router

import android.app.Activity
import com.lllddd.businessb.BusinessBMainActivity
import com.lllddd.routersdk.ILoadRouters

/**
 * author: lllddd
 * created on: 2024/5/2 20:19
 * description:业务模块B的路由装载类
 */
class BusinessBRouter : ILoadRouters {
    override fun loadRouters(routerMap: MutableMap<String, Class<out Activity>>) {
        routerMap["businessb/main"] = BusinessBMainActivity::class.java
    }
}

​ 这样一来,我们只需要在Router类中增加一个init()方法,在该方法中调用各模块的loadRouters()方法即可。

‘’

package com.lllddd.routersdk

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Toast

/**
 * author: lllddd
 * created on: 2024/5/2 14:34
 * description:路由类
 */
object Router {

    private val routerMap: MutableMap<String, Class<out Activity>> = mutableMapOf()

    fun init() {
        ABusinessRouter().loadRouters(routerMap)
        BBusinessRouter().loadRouters(routerMap)
        MainRouter().loadRouters(routerMap)
    }

//    fun register(key: String, value: Class<out Activity>) {
//        routerMap[key] = value
//    }

    fun jumpTo(activity: Activity, path: String, params: Bundle? = null) {
        if (!routerMap.containsKey(path)) {
            Toast.makeText(activity, "找不到路由目的页面!!!", Toast.LENGTH_LONG).show()
            return
        }

        val destinationActivity = routerMap[path]
        val intent = Intent(activity, destinationActivity)
        if (params != null) {
            intent.putExtras(params)
        }

        activity.startActivity(intent)
    }
}

​ 此时MyRouterApp中只需要直接调用Router的init()初始化方法即可。

‘’

package com.lllddd.myrouter.app

import android.app.Application
import com.lllddd.routersdk.Router

/**
 * author: lllddd
 * created on: 2024/5/2 15:06
 * description:
 */
class MyRouterApp : Application() {
    override fun onCreate() {
        super.onCreate()

        // 初始化路由SDK
        Router.init()
    }
}

​ 思路虽然没错,但是Router中的init()方法却是飘红的,这里作为基础模块,怎么可能拿到业务模块的类引用从而实例化出ABusiniessRouter、BBusinissRouter、MainRouter呢?

​ 所以当前的init()方法一定是行不通的,既然基础模块不能直接使用上层业务模块中的类,那我们只能重新想办法。

​ 此处突然想到了一个好办法,那就是反射,我在这里反射出来ABusiniessRouter、BBusinissRouter、MainRouter这3个对象不就好了。说干就干,改造后的init()代码是这样的。

‘’

fun init() {
//        ABusinessRouter().loadRouters(routerMap)
//        BBusinessRouter().loadRouters(routerMap)
//        MainRouter().loadRouters(routerMap)

        val aBizClazz = Class.forName("com.lllddd.businessa.router.BusinessARouter")
        val aBizRouter = aBizClazz.newInstance()
        val methodABiz = aBizClazz.methods.find { it.name == "loadRouters" }
        methodABiz?.invoke(aBizRouter, routerMap)


        val bBizClazz = Class.forName("com.lllddd.businessb.router.BusinessBRouter")
        val bBizRouter = bBizClazz.newInstance()
        val methodBBiz = bBizClazz.methods.find { it.name == "loadRouters" }
        methodBBiz?.invoke(bBizRouter, routerMap)

        val mainClazz = Class.forName("com.lllddd.main.router.MainRouter")
        val mainRouter = mainClazz.newInstance()
        val methodMain = mainClazz.methods.find { it.name == "loadRouters" }
        methodMain?.invoke(mainRouter, routerMap)
    }

​ 看起来确实不再报错了,demo也正常运行。但是造成的问题是每次增减一个业务模块,就需要在基础模块routersdk的Router类的init()方法中增删代码,而且对应业务模块的路由类是通过反射在此逐一实例化的,用起来也不方便。

​ 那么有没有更好的办法呢?

​ 当然是有,这里需要结合类加载、PMS、反射的知识来综合处理。

​ 我只要想办法遍历整个应用apk,想办法找到满足规则com.lllddd.xxx.router.xxx.kt的kotlin文件完整包路径即可。

​ 此时就需要借助PMS拿到我们的apk。当应用安装后运行时,对应的apk文件其实是在下面这个路径中的

​ /data/app/com.lllddd.myrouter-m-SApQoUtVytou1_nl1aUA==/base.apk

​ 之后可以利用类加载技术中的DexFile匹配正则规则来遍历apk找到符合规则的类路径,即

​ com.lllddd.businessa.router.BusinessARouter
​ com.lllddd.businessb.router.BusinessBRouter
​ com.lllddd.main.router.MainRouter

​ 之后还是在Router的init()方法中利用反射调用每个XXXRouter的loadRouters()方法就能实现路由注册。

​ 我将相关的关键操作封装进ClassHelper类。

‘’

package com.lllddd.routersdk

import android.app.Application
import android.content.Context
import dalvik.system.DexFile
import java.util.regex.Pattern

/**
 * author: lllddd
 * created on: 2024/5/2 21:43
 * description:类帮助者
 */
class ClassHelper {
    companion object {
        /**
         * 获取当前的apk文件
         *
         * @param context 应用上下文
         * @return apk文件路径列表
         */
        private fun getSourcePaths(context: Context): List<String> {
            val applicationInfo = context.applicationInfo

            val pathList = mutableListOf<String>()

            // /data/app/com.lllddd.myrouter-m-SApQoUtVytou1_nl1aUA==/base.apk
            pathList.add(applicationInfo.sourceDir)

            if (applicationInfo.splitSourceDirs != null) {
                val array = applicationInfo.splitSourceDirs

                for (ele in array) {
                    pathList.add(ele)
                }
            }

            return pathList
        }

        /**
         * 根据Router类所在包名的正则规则,拿到所有Router的完整包名路径,以便后期反射调用
         *
         * @param context 应用上下文
         * @param packageRegex Router类所在包名的正则规则
         * @return 所有Router的完整包名路径
         */
        fun getFileNameByPackageName(context: Application, packageRegex: String): Set<String> {
            val set = mutableSetOf<String>()
            val pathList = getSourcePaths(context)

            val pattern = Pattern.compile(packageRegex)

            for (path in pathList) {
                var dexFile: DexFile? = null
                try {
                    dexFile = DexFile(path)

                    val entries = dexFile.entries()

                    if (entries != null) {
                        while (entries.hasMoreElements()) {
                            val className = entries.nextElement()
                            val matcher = pattern.matcher(className)
                            if (matcher.find()) {
                                set.add(className)
                            }
                        }
                    }
                } finally {
                    dexFile?.close()
                }
            }

            return set
        }
    }
}

​ 之后Router中的init()方法直接调用ClassHelper中的方法并遍历反射即可。

‘’

 fun init(application: Application) {
        // 方案1:飘红
//        ABusinessRouter().loadRouters(routerMap)
//        BBusinessRouter().loadRouters(routerMap)
//        MainRouter().loadRouters(routerMap)

        // 方案2:并不优雅
//        val aBizClazz = Class.forName("com.lllddd.businessa.router.BusinessARouter")
//        val aBizRouter = aBizClazz.newInstance()
//        val methodABiz = aBizClazz.methods.find { it.name == "loadRouters" }
//        methodABiz?.invoke(aBizRouter, routerMap)
//
//        val bBizClazz = Class.forName("com.lllddd.businessb.router.BusinessBRouter")
//        val bBizRouter = bBizClazz.newInstance()
//        val methodBBiz = bBizClazz.methods.find { it.name == "loadRouters" }
//        methodBBiz?.invoke(bBizRouter, routerMap)
//
//        val mainClazz = Class.forName("com.lllddd.main.router.MainRouter")
//        val mainRouter = mainClazz.newInstance()
//        val methodMain = mainClazz.methods.find { it.name == "loadRouters" }
//        methodMain?.invoke(mainRouter, routerMap)

        // 方案3:自动扫包
        val set = ClassHelper.getFileNameByPackageName(
            application,
            "com.lllddd.[a-zA-Z0-9]+\\.router\\.[a-zA-Z0-9]+"
        )
        for (fileName in set) {
            val clazz = Class.forName(fileName)
            val router = clazz.newInstance()
            val method = clazz.methods.find { it.name == "loadRouters" }
            method?.invoke(router, routerMap)
        }
    }

3.完善版

未完待续…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1639747.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

BeanFactory 源码浅析

BeanFactory 功能介绍 BeanFactory 是核心容器&#xff0c;负责管理 Bean 对象 BeanFactory 接口的功能只有一个 getBean() 方法BeanFactory 的实现类&#xff08;DefaultListableBeanFactory&#xff09;包含&#xff1a;控制反转、基本的依赖注入、Bean 生命周期的各种功能…

【Python】函数设计

1.联系函数的设计 2.找质数 3.找因子 4.判断水仙花数 5.斐波拉契数列递归调用&#xff0c;并用数组存储已计算过的数&#xff0c;减少重复计算 1、计算利息和本息 编写两个函数分别按单利和复利计算利息,根据本金、年利率、存款年限得到本息和和利息。调用这两个函数计算1…

【算法刷题日志】吸氧羊的StarryCoding之旅 - 贡献法计算

题目链接&#xff1a;https://www.starrycoding.com/problem/3 题目描述 吸氧羊终于注册了一个StarryCoding账号&#xff01;&#xff08;她很开心&#xff09; 但是吸氧羊忘记了它的密码&#xff0c;她想起你是计算机大师&#xff0c;于是就来请教你。 她虽然不记得密码了…

java版数据结构:深入理解栈和队列:数据结构与应用(vector,stack,queue)

目录 前言 动态数组类&#xff08;vector&#xff09; 特点&#xff1a; 应用&#xff1a; 栈&#xff08;Stack&#xff09; 栈的基础概念&#xff1a; 栈的常用方法&#xff1a; 模拟栈操作&#xff1a; 队列&#xff08;Queue&#xff09; 队列的基础概念 队列的常…

golang学习笔记(协程的基础知识)

golang的协程 协程是一种轻量级的线程&#xff0c;它可以实现并发执行的并行操作。协程是Go语言中的一个核心特性&#xff0c;它使得程序能够以并发的方式运行&#xff0c;并且非常高效。与传统的线程相比&#xff0c;协程的创建和销毁成本非常低&#xff0c;可以方便地启动大…

三维坐标点按剖面分类

一、写在前面 ①配套文件&#xff1a;根据剖面对三维坐标点&#xff08;X,Y,Z&#xff09;分类资源-CSDN文库 ②脱敏处理&#xff1a;蚀变数据已采用随机数生成覆盖 ③剖面坐标按顺序排列在“剖面坐标点.xlsx”文件中 二、3点确定空间中平面方程 原理&#xff1a; 设3点A&…

C++深度解析教程笔记2

C深度解析教程笔记2 第3课 - 进化后的 const 分析实验-C与C的const区别实验-C与C的const区别&const作用域 第4课 - 布尔类型和引用小结 本文学习自狄泰软件学院 唐佐林老师的 C深度解析教程&#xff0c;图片全部来源于课程PPT&#xff0c;仅用于个人学习记录 第3课 - 进化后…

列转行(spark 与presto语法)

一、Presto 语法 原始数据&#xff1a; 期望数据&#xff1a; 代码&#xff1a; SELECT info, value FROM ( select 张三 as name,18 as age,男 as gender,清华 as schoolunion allselect 李四 as name,18 as age,男 as gender,清华 as school ) as a CROSS JOIN UNNEST(…

Unreal 编辑器工具 批量重命名资源

右键 - Editor Utilities - Editor Utility Blueprint&#xff0c;基类选择 Asset Action Utility 在类默认值内&#xff0c;可以添加筛选器&#xff0c;筛选指定的类型 然后新建一个函数&#xff0c;加上4个输入&#xff1a;ReplaceFrom&#xff0c;ReplaceTo&#xff0c;Add…

使用Android Studio 搭建AOSP FrameWork 源码阅读开发环境

文章目录 概述安装Android Studio编译源码使用Android Studio打开源码制作ipr文件直接编译成功后自动打开Android Studio 修改SystemUI验证开发环境 概述 我们都知道Android的系统源码量非常之大&#xff0c;大致有frameworka层源码&#xff0c;硬件层(HAL)源码&#xff0c;内…

机器学习笔记-18

异常检测问题 异常检测虽然主要用于无监督学习问题上&#xff0c;但是和监督学习问题很相似。 异常检测(Anomaly Detection)&#xff1a;给定正确样本集{ x ( 1 ) , x ( 2 ) . . . x ( n ) x^{(1)},x^{(2)}...x^{(n)} x(1),x(2)...x(n)}&#xff0c;记新样本即要检测的样本为…

堆排序以及TOP-K问题

片头 嗨&#xff01;小伙伴们&#xff0c;大家好&#xff01;今天我们来深入理解堆这种数据结构&#xff0c;分析一下堆排序以及TOP-K问题&#xff0c;准备好了吗&#xff1f;我要开始咯&#xff01; 一、堆排序 这里我们先假设要排成升序&#xff0c;也就是从左到右&#xf…

JSP简介——[JSP]1

希望你开心&#xff0c;希望你健康&#xff0c;希望你幸福&#xff0c;希望你点赞&#xff01; 最后的最后&#xff0c;关注喵&#xff0c;关注喵&#xff0c;关注喵&#xff0c;大大会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的…

基于php+mysql+html图书管理系统(含实训报告)

博主介绍&#xff1a; 大家好&#xff0c;本人精通Java、Python、Php、C#、C、C编程语言&#xff0c;同时也熟练掌握微信小程序、Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验&#xff0c;能够为学生提供各类…

【C++】命名冲突了怎么办?命名空间来解决你的烦恼!!!C++不同于C的命名方式——带你认识C++的命名空间

命名空间 导读一、什么是C?二、C的发展三、命名空间3.1 C语言中的重名冲突3.2 什么是命名空间&#xff1f;3.3 命名空间的定义3.4 命名空间的使用环境3.5 ::——作用域限定符3.6 命名空间的使用方法3.6.1 通过作用域限定符来指定作用域3.6.2 通过关键字using和关键字namespace…

如何用 Redis 实现延迟队列?

延迟队列是一种常见的消息队列模式&#xff0c;用于处理需要延迟执行的任务或消息。Redis 是一种快速、开源的键值对存储数据库&#xff0c;具有高性能、持久性和丰富的数据结构&#xff0c;因此很适合用于实现延迟队列。在这篇文章中&#xff0c;我们将详细讨论如何使用 Redis…

51单片机两个中断及中断嵌套

文章目录 前言一、中断嵌套是什么&#xff1f;二、两个同级别中断2.1 中断运行关系2.2 测试程序 三、两个不同级别中断实现中断嵌套3.1 中断运行关系3.2 测试程序 总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 课程需要&#xff1a; 提示&#x…

Mysql基础(四)DML之insert语句

一 insert 语句 强调&#xff1a; 本文介绍的内容很基础,仅做记录用,参考价值较少 ① 总述 目的&#xff1a; 增加rows记录1、完整格式insert [into] 表名[字段名1[, 字段名2]] value[s](值1, 值2);备注&#xff1a;指定部分字段添加,没有被指定的字段要么会自动增长,要…

微信小程序demo-----制作文章专栏

前言&#xff1a;不管我们要做什么种类的小程序都涉及到宣传或者扩展其他业务&#xff0c;我们就可以制作一个文章专栏的页面&#xff0c;实现点击一个专栏跳转到相应的页面&#xff0c;页面可以有科普类的知识或者其他&#xff0c;然后页面下方可以自由发挥&#xff0c;添加联…

ensp 配置s5700 ssh登陆

#核心配置 sys undo info-center enable sysname sw1 vlan 99 stelnet server enable telnet server enable int g 0/0/1 port lin acc port de vlan 99 q user-interface vty 0 4 protocol inbound ssh authentication-mode aaa q aaa local-user admin0 password cipher adm…