Android手写自己的路由SDK

实现自己的路由框架

​ 在较大型的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.完善版

未完待续…

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用Nuxt3框架搭建基础项目

Nuxt3安装 基础配置: Node.js** - v18.0.0版本以上 , 可以结合fnm工具切换node版本 安装nuxt3命令 打开vscode或者控制台去到项目文件夹输入: npx nuxilatest init <project-name> 国内执行这行代码&#xff0c;即使科学上网也会有问题 ⚠️ 安装Nuxt3报错 安装过程…

数据分析--客户价值分析RFM(K-means聚类/轮廓系数)

原数据 import os import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn import metrics ### 数据抽取&#xff0c;读⼊数据 df pd.read_csv("customers1997.csv") #相对路径读取数据 print(df.info()) pr…

如何定时打开网站

首先&#xff0c;需要用到的这个工具&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 1、打开工具按下Ctrl3&#xff0c;切换到定时器模块&#xff0c;左侧右键&#xff0c;选择新建 2、标题叫百度&#xff0c;等下就让它打开百度&#xff0c…

Spring - 6 ( 9000 字 Spring 入门级教程 )

一&#xff1a; SpringBoot 配置文件 1.1 配置文件作用 配置文件通常是一个文本文件&#xff0c;其中包含了程序或系统的各种设置、选项和参数。比如C:\Users, C:\Windows 文件夹, 以及各种 .config, .xml 文件 配置文件主要是为了解决硬编码&#xff08;代码写死&#xff0…

排序算法--希尔排序

前提&#xff1a; 排序算法——直接插入排序-CSDN博客 希尔排序(Shell Sort)是插入排序的一种。是直接插入排序算法的Plus版。该方法又称缩小增量排序&#xff0c;是D.L.Shell于1959年提出。要想学好希尔排序&#xff0c;直接插入排序一定要学好&#xff0c;没学过的&#xff0…

chrome extension插件替换网络请求中的useragent

感觉Chrome商店中的插件不能很好的实现自己想要的效果,那么就来自己动手吧。 本文以百度为例: 一般来说网页请求如下: 当前使用的useragent是User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safar…

【Flask 系统教程 4】Jinjia2模版和语法

Jinjia2 模板 模板的介绍 Jinja2 是一种现代的、设计优雅的模板引擎&#xff0c;它是 Python 的一部分&#xff0c;由 Armin Ronacher 开发。Jinja2 允许你在 HTML 文档中嵌入 Python 代码&#xff0c;以及使用变量、控制结构和过滤器来动态生成内容。它的语法简洁清晰&#…

java发送请求2次开发-get请求json

因为你请求参数不为空&#xff0c;接口都会把这个参数带上 所以借鉴HttpPost类 继承这个类&#xff0c; 这个类是可以带消息的 httpgetwithentity&#xff0c;httpget请求带上消息 复写 构造方法复制过来进行使用 二次开发类让其get请求时可以发送json

IOS上线操作

1、拥有苹果开发者账号 2、配置证书&#xff0c;进入苹果开发者官网&#xff08;https://developer.apple.com/&#xff09; 3、点击账户&#xff08;account&#xff09;&#xff0c;然后创建一个唯一的标识符 4、点击"Identifiers"&#xff0c;然后点击"&qu…

SpringBoot的ProblemDetails

1.RFC 7807 之前的项目如果出现异常&#xff0c;默认跳转到error页面。或者是抛出500 异常。 但是对于前后端分离的项目&#xff0c;Java程序员不负责页面跳转&#xff0c;只需要 把错误信息交给前端程序员处理即可。而RFC 7807规范就是将异常 信息转为JSON格式的数据。这个…

android init进程启动流程

Android系统完整的启动流程 android 系统架构图 init进程的启动流程 init进程启动服务的顺序 bool Service::Start() {// Starting a service removes it from the disabled or reset state and// immediately takes it out of the restarting state if it was in there.flags_…

每天五分钟深度学习框架pytorch:如何创建多维Tensor张量元素?

本文重点 上节课程我们学习了如何创建Tensor标量,我们使用torch.tensor。本节课程我们学习如何创建Tensor向量,我们即可以使用torch.Tensor又可以使用torch.tensor,下面我们看一下二者的共同点和不同点。 Tensor张量 tensor张量是一个多维数组,零维就是一个点(就是上一…

llama-factory/peft微调千问1.5-7b-chat

目标 使用COIG-CQIA数据集和通用sft数据集对qwen1.5-7b-chat进行sft微调,使用公开dpo数据集进行dpo对齐。学习千问的长度外推方法。 一、训练配置 使用Lora方式, 将lora改为full即可使用全量微调。 具体的参数在 该框架将各个参数、训练配置都封装好了,直接使用脚本,将数…

毫米波雷达多人呼吸心跳检测MATLAB仿真

本文基于TI的IWR1642毫米波雷达 2T4R MIMO阵列&#xff0c;通过实际采集数据算法仿真&#xff0c;实现多人呼吸心跳检测。 文章末尾给出了本文的仿真代码。 主要内容包含&#xff1a; &#xff08;1&#xff09;雷达参数设定 &#xff08;2&#xff09;ADC数据导入 &#xff08…

Windows Server 安全策略配置

前言 Windows Server是由微软开发的一种操作系统&#xff0c;主要用于在企业或机构的服务器上运行。它提供了一系列的功能和工具&#xff0c;旨在提高服务器的性能、可靠性、安全性和管理性。 特点 强大的性能&#xff1a;Windows Server具有高度优化的内核和资源管理&#x…

【MySQL | 第十篇】重新认识MySQL索引匹配过程

文章目录 10.重新认识MySQL索引匹配过程10.1匹配规则10.2举例&#xff1a;联合索引遇到范围查询&#xff08;>、<、between、like&#xff09;10.2.1例子一&#xff1a;>10.2.2例子二&#xff1a;>10.2.3例子三&#xff1a;between10.2.4例子四&#xff1a;like 10…

SQL数据库

一.什么是数据库 数据库&#xff1a;存储数据的仓库&#xff0c;数据是有组织的进行存储。&#xff08;database 简称DB&#xff09; 数据库管理系统&#xff1a;管理数据库的大型软禁&#xff08;DataBase Management System 简称DBMS&#xff09; SQL&#xff1a;操作关系…

Deep Learning Part Seven基于RNN生成文本--24.5.2

不存在什么完美的文章&#xff0c;就好像没有完美的绝望。 ——村上春树《且听风吟》 本章所学的内容 0.引子 本章主要利用LSTM实现几个有趣的应用&#xff1a; 先剧透一下&#xff1a;是AI聊天软件&#xff08;现在做的ChatGPT&#xff08;聊天神器&#xff0c;水论文高手…

Windows Server安装DHCP和DNS

前言 本期将教大家如何在Windows server上部署DHCP服务和DNS服务&#xff0c;用于模拟给内网主机分配IP地址。虽然用于演示的系统比较老&#xff0c;如果在新版本如Windows server2016、19、22上部署&#xff0c;操作基本一致。在此之前先给大家科普一波理论&#xff0c;需略过…

【docker 】push 镜像到私服

查看镜像 docker images把这个hello-world 推送到私服 docker push hello-world:latest 报错了。不能推送。需要标记镜像 标记Docker镜像 docker tag hello-world:latest 192.168.2.1:5000/hello-world:latest 将Docker镜像推送到私服 docker push 192.168.2.1:5000/hello…