当前位置: 首页 > news >正文

Android 宿主启动插件中的Activity和Service

在宿主App中加载插件App中的四大组件,需要以下几个步骤:

1. 预先在宿主的AndroidManifest文件中声明插件中的四大组件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"package="com.chinatsp.zeusstudy1"><applicationandroid:name=".MyApplication"android:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.ZeusStudy1"tools:targetApi="31"><activityandroid:name=".MainActivity"android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><activity android:name="com.chinatsp.zeusstudy1.ActivityA"/><!-- 插件中的类 --><service android:name="com.chinatsp.plugin1.TestService1"/><activity android:name="com.chinatsp.plugin1.TestActivity1"/></application></manifest>

2. 宿主加载插件的类

2.1 合并所有插件的dex 来解决插件的类的加载问题

把插件dex都合并到宿主的dex中,那么宿主App对应的ClassLoader就可以而加载插件中的任意类了

static void mergeDexs(String apkName, String dexName) {File dexFile = mBaseContext.getFileStreamPath(apkName);File optDexFile = mBaseContext.getFileStreamPath(dexName);try {BaseDexClassLoaderHookHelper.pathClassLoader(mBaseContext.getClassLoader(), dexFile, optDexFile);} catch (Exception e) {e.printStackTrace();}}
/*** 由于应用程序使用的ClassLoader为PathClassLoader* 最终继承自 BaseDexClassLoader* 查看源码得知,这个BaseDexClassLoader加载代码根据一个叫做* dexElements的数组进行, 因此我们把包含代码的dex文件插入这个数组* 系统的classLoader就能帮助我们找到这个类** 这个类用来进行对于BaseDexClassLoader的Hook* @author weishu* @date 16/3/28*/
public final class BaseDexClassLoaderHookHelper {public static void pathClassLoader(ClassLoader classLoader, File apkFile,File dexFile) throws IllegalAccessException,NoSuchMethodException, IOException, InvocationTargetException,InstantiationException,NoSuchFieldException {// 获取BaseDexClassLoader 中的字段 pathListObject pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(),classLoader,"pathList");// 获取PathList中的字段 Element[] dexElementsObject[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj,"dexElements");// Element类型Class<?> elementClass = dexElements.getClass().getComponentType();// 创建一个数组, 用来替换原始的数组Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);// 构造插件ElementClass[] params = {DexFile.class,File.class,};DexFile dexFile1 = DexFile.loadDex(apkFile.getCanonicalPath(),dexFile.getAbsolutePath(),0);Object[] values = {dexFile1,apkFile};Object dexObj = RefInvoke.createObject(elementClass,params,values);Object[] toAddNewElementArray = new Object[]{dexObj};// 把原始的elements复制进去System.arraycopy(dexElements,0,newElements,0,dexElements.length);// 插件的那个element复制进去System.arraycopy(toAddNewElementArray,0,newElements,dexElements.length,toAddNewElementArray.length);// 替换RefInvoke.setFieldObject(pathListObj,"dexElements",newElements);}
}

在Applciation的attachBaseContext 方法中调用该方法将插件的dex合并进宿主的dexElements中就可以了。

通过以上两步,我们就可以正常的打开插件App中的Service和Activity类了

 public void startService1InPlugin1(View view) {try {Intent intent = new Intent();String serviceName = PluginManager.plugins.get(0).packageInfo.packageName + ".TestService1";intent.setClass(this, Class.forName(serviceName));startService(intent);} catch (Exception e) {e.printStackTrace();}}public void startActivityInPlugin1(View view){try {Intent intent = new Intent();String activityName = PluginManager.plugins.get(0).packageInfo.packageName + ".TestActivity1";intent.setClass(this, Class.forName(activityName));startActivity(intent);}catch (Exception e){e.printStackTrace();}}
2.2 修改app原生的ClassLoader

直接把系统的ClassLoader替换为我们自己的ZeusClassLoaderZeusClassLoader 的构造函数中将会传递进宿主的ClassLoader,除此之外,其内部有一个mClassLoaderList变量,保存着所有插件ClassLoader的集合。于是ZeusClassLoaderloadClass方法,会先尝试使用宿主ClassLoader加载类,如果不能加载,就遍历mClassLoaderList,直到找到一个能加载类的ClassLoader

/**** 这是一个空ClassLoader,主要是个容器* <p>* Created by huangjian on 2016/6/21.*/
class ZeusClassLoader extends PathClassLoader {private List<DexClassLoader> mClassLoaderList = null;public ZeusClassLoader(String dexPath, ClassLoader parent) {super(dexPath, parent);mClassLoaderList = new ArrayList<DexClassLoader>();}/*** 添加一个插件到当前的classLoader中*/protected void addPluginClassLoader(DexClassLoader dexClassLoader) {mClassLoaderList.add(dexClassLoader);}@Overrideprotected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {Class<?> clazz = null;try {//先查找parent classLoader,这里实际就是系统帮我们创建的classLoader,目标对应为宿主apkclazz = getParent().loadClass(className);} catch (ClassNotFoundException ignored) {}if (clazz != null) {return clazz;}//挨个的到插件里进行查找if (mClassLoaderList != null) {for (DexClassLoader classLoader : mClassLoaderList) {if (classLoader == null) continue;try {//这里只查找插件它自己的apk,不需要查parent,避免多次无用查询,提高性能clazz = classLoader.loadClass(className);if (clazz != null) {return clazz;}} catch (ClassNotFoundException ignored) {}}}throw new ClassNotFoundException(className + " in loader " + this);}
}
private static void composeClassLoader() {// 传入宿主的ClassLoaderZeusClassLoader classLoader = new ZeusClassLoader(mBaseContext.getPackageCodePath(), mBaseContext.getClassLoader());// 添加插件的ClassLoaderFile dexOutputDir = mBaseContext.getDir("dex", Context.MODE_PRIVATE);final String dexOutputPath = dexOutputDir.getAbsolutePath();for(PluginItem plugin: plugins) {DexClassLoader dexClassLoader = new DexClassLoader(plugin.pluginPath,dexOutputPath, null, mBaseClassLoader);classLoader.addPluginClassLoader(dexClassLoader);}// 替换PackgeInfo和当前线程的mClassLoaderRefInvoke.setFieldObject(mPackageInfo, "mClassLoader", classLoader);Thread.currentThread().setContextClassLoader(classLoader);mNowClassLoader = classLoader;}

经过这样Hook,所有插件的ClassLoader都在一起了。但是原先启动Activity/Service的方式需要改变:

 public void startService1InPlugin1(View view) {try {Intent intent = new Intent();String serviceName = PluginManager.plugins.get(0).packageInfo.packageName + ".TestService1";//  intent.setClass(this, Class.forName(serviceName)); intent.setClass(this, getClassLoader().loadClass(serviceName));startService(intent);} catch (Exception e) {e.printStackTrace();}}

我们之前是使用Class.forName方法来启动Servcie,会抛出找不到宿主Apk或找不到插件Service类的异常,这是因为Class.forName方法会使用BootClassLoader来加载类,这个类并没有被Hook,所以自然也就加载不到插件中的类了。
getClassLoader方法获取到的是我们Hook过的新ClassLoader,就可以加载到插件中的类了。

3. 加载插件中的资源

四大组件除了Activity,其他都是没有界面的,因此不涉及到资源。Activity则严重依赖资源文件,所以要想正确的显示插件中的Activity,必须解决加载插件中资源的问题。

宿主想要加载插件中的资源,我们是怎么做的呢?

生成一个新的AssetManager对象newAssetManager,发射调用这个newAssetManageraddAssetPath方法把插件Apk的路径加载进来,然后根据这个newAssetManager生成一个新的Resource对象newResource,然后在宿主Activity中重写getResourcesgetAssets 返回newAssetManagernewResource,这样宿主Activity就可以查找到插件Apk中的资源了。

这是一种分离宿主和插件资源的方式。还有另一种合并宿主和插件资源的方式。

创建一个新的 AssetManager 对象,并将宿主和插件的资源都通过addAssetPath方法塞入;通过新的AssetManager对象来创建出一个新的Resources对象;将新的Resources对象替换ContextImpl中的mResources变量、LoadedApk变量里的mResources变量 以及 置空mThem变量

所以按照上述步骤通过代码实现如下:

public static void reloadInstalledPluginResources(ArrayList<String> pluginPaths) {try {AssetManager assetManager = AssetManager.class.newInstance();Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);// 塞入宿主的资源addAssetPath.invoke(assetManager, mBaseContext.getPackageResourcePath());for(String pluginPath: pluginPaths) {// 塞入插件的资源addAssetPath.invoke(assetManager, pluginPath);}mAssetManager = assetManager;Resources newResources = new Resources(assetManager,mBaseContext.getResources().getDisplayMetrics(),mBaseContext.getResources().getConfiguration());// 获取 ContextImpl 中的 Resources 类型的 mResources 变量,并替换它的值为新的 Resources 对象RefInvoke.setFieldObject(mBaseContext, "mResources", newResources);//这是最主要的需要替换的,如果不支持插件运行时更新,只留这一个就可以了RefInvoke.setFieldObject(mPackageInfo, "mResources", newResources);mNowResources = newResources;//需要清理mTheme对象,否则通过inflate方式加载资源会报错//如果是activity动态加载插件,则需要把activity的mTheme对象也设置为nullRefInvoke.setFieldObject(mBaseContext, "mTheme", null);} catch (Throwable e) {e.printStackTrace();}}

上述方法的调用时机就是在执行完插件dex合并后调用。如果此时要在宿主中调用插件中的Activity,还需要做一件事情,就是将上面方法中的mNowResources对象传递到插件中的Activity中去,并重写插件Activity的getResources方法,使getResources方法返回mNowResources对象即可。

但是这样完成之后会发现,怎么明明代码中启动的是插件中的Activity,但显示的确是宿主中的Activity呢?
如果你把宿主的Apk和插件的Apk打开( 使用AS中的Build ==》Analazy Apk ),查看其中的resources.arsc文件,对比后会发现两个APK中的资源文件有相同的id,这就是资源id冲突引起的问题,可以通过 配置aapt2 的参数来修改插件Apk生成的资源id:

# build.gradle
android {...aaptOptions {additionalParameters '--allow-reserved-package-id','--package-id','0x70'}
}

这样,插件Apk中生成的资源id就会以0x70开头,而我们的宿主Apk生成的资源id默认是0x7f开头。

资源id的生成过程建议查看这个

合并dex版本源码
ZeusClassLoader版本源码

相关文章:

Android 宿主启动插件中的Activity和Service

在宿主App中加载插件App中的四大组件&#xff0c;需要以下几个步骤&#xff1a; 1. 预先在宿主的AndroidManifest文件中声明插件中的四大组件 <?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android"http://schemas.android.co…...

00后卷王自述,我真的很卷吗?

前段时间我去面试了一个软件测试公司&#xff0c;成功拿到了offer&#xff0c;薪资也从10k涨到了18k&#xff0c;对于工作都还没两年的我来说&#xff0c;还是比较满意的&#xff0c;毕竟有些工作了3到4年的可能还没有我的高。 在公司一段时间后大家都说我是卷王&#xff0c;其…...

真题详解(树的结点)-软件设计(八十四)

真题详解&#xff08;汇总&#xff09;-软件设计&#xff08;八十三)https://blog.csdn.net/ke1ying/article/details/130856130?spm1001.2014.3001.5501 COCOMOII估算不包括_____。 对象点 B.功能点 C.用例数 D.源代码行 答案&#xff1a;C 语法翻译是一种&#xff…...

LDA算法实现鸢尾花数据集降维

目录 1. 作者介绍2. LDA降维算法2.1 基本概念2.2 算法流程 3. LDA算法实现3.1 数据集介绍3.2 代码实现3.3 结果展示 1. 作者介绍 唐杰&#xff0c;男&#xff0c;西安工程大学电子信息学院&#xff0c;2022级研究生 研究方向&#xff1a;机器视觉与人工智能 电子邮件&#xff…...

深入理解Linux虚拟内存管理

系列文章目录 Linux 内核设计与实现 深入理解 Linux 内核&#xff08;一&#xff09; 深入理解 Linux 内核&#xff08;二&#xff09; Linux 设备驱动程序&#xff08;一&#xff09; Linux 设备驱动程序&#xff08;二&#xff09; Linux 设备驱动程序&#xff08;三&#xf…...

自动化测试框架、Python面向对象以及POM设计模型简介

目录 1 自动化测试框架概述 2 自动化测试框架需要的环境 3 自动化测试框架设计思想&#xff1a;Python面向对象 4 自动化测试框架设计思想&#xff1a;POM&#xff08;Page Object Model&#xff09;页面对象模型 1 自动化测试框架概述 所谓的框架其实就是一个解决问题…...

【CSSpart4--盒子模型】

CSSpart4--盒子模型 网页布局的三大核心&#xff1a;盒子模型&#xff0c;浮动&#xff0c;定位网页布局的过程&#xff08;本质&#xff09;&#xff1a;盒子模型的组成四部分&#xff1a;边框&#xff0c;内容&#xff0c;内边距&#xff0c;外边距 一 、盒子边框border:1.1 …...

Linux - Java 8 入门安装与重装教程集锦

一、入门初始安装 1. 具体安装教程 1. linux 系统中如何安装java环境&#xff08;通过tar.gz文件&#xff09; 安装包下载链接 Java 的 tar.gz 安装包下载链接传送门 Linux 系统的 Java 环境变量配置教程 1. linux查看java版本&#xff0c;以及配置java home 2. Linux环…...

2023年最新企业网盘排行榜出炉

随着云计算技术的不断发展&#xff0c;企业日常工作中大量的资料、文档等信息需要实现集中管理&#xff0c;此时企业网盘工具就应运而生。企业网盘是一种可用于企业内部管理、团队协作及文件共享的云存储平台&#xff0c;能够极大提高企业办公效率和安全性。 一、企业网盘的帮助…...

C++内存分类

内存分配方式(内存布局)&#xff1a; 内存5分类 堆、栈、自由存储区、全局/静态存储区、常量存储区 &#xff08;1&#xff09;栈&#xff1a;内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数&#xff0c;函数调用后返回的地址。&#xff08;为运行函数而…...

不是说00后已经躺平了吗,怎么还是这么卷.....

都说00后已经躺平了&#xff0c;但是有一说一&#xff0c;该卷的还是卷。 前段时间我们部门就来了个00后&#xff0c;工作都还没两年&#xff0c;跳到我们公司起薪20K&#xff0c;都快接近我了。 后来才知道人家是个卷王&#xff0c;从早干到晚就差搬张床到工位睡觉了。最近和…...

国内免费版ChatGPT

目录 前言&#xff1a;网站大全 1. ChatGPT是什么 2. ChatGPT的发展历程 3. ChatGPT对程序员的影响 4. ChatGPT对普通人的影响 5. ChatGPT的不足之处 前言&#xff1a;网站大全 AI文本工具站 (laicj.cn) ——gpt-3.5 功能强大(推荐&#xff09; Chatgpt在线网页版-…...

常用本地事务和分布式事务解决方案模型

目录 1 DTP模型2 2PC2.1 方案简介2.2 处理流程2.2.1 阶段1&#xff1a;准备阶段2.2.2 阶段2&#xff1a;提交阶段 2.3 方案总结 3 3PC3.1 方案简介3.2 处理流程3.2.1 阶段1&#xff1a;canCommit3.2.2 阶段2&#xff1a;preCommit3.3.3 阶段3&#xff1a;do Commit 3.3 方案总结…...

无代码玩转GIS应用,我也在行【文末送书】

您好&#xff0c;我是码农飞哥&#xff08;wei158556&#xff09;&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。&#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精通…...

xlsx是什么格式

xlsx是什么格式? xlsx是Excel文档的扩展名&#xff0c;其基于Office Open XML标准的压缩文件格式&#xff0c;取代了其以前专有的默认文件格式&#xff0c;在传统的文件名扩展名后面添加了字母x&#xff0c;即.xlsx取代.xls。 xlsx文件是什么格式? xlsx是Excel表格的文件格…...

将 Maven 配置为使用阿里云镜像

将 Maven 配置为使用阿里云镜像的步骤如下&#xff1a; 打开 Maven 的 settings.xml 文件&#xff1a;在 Maven 安装目录下的 conf 文件夹中&#xff0c;找到 settings.xml 文件&#xff0c;并打开它。 添加镜像配置&#xff1a;在 settings.xml 文件中&#xff0c;找到 <m…...

行业报告 | 2022文化科技十大前沿应用趋势(下)

原创 | 文 BFT机器人 04 商业创新 趋势7&#xff1a;区块链技术连接传统文化&#xff0c;数字藏品市场在探索中发展 核心内容&#xff1a; 2022年&#xff0c;数字藏品在区块链技术的助力下应运而生。狭义的数字藏品是指使用区块链技术、基于特定的文化资源所生成唯一的数字凭…...

ASEMI代理韩景元可控硅C106M参数,C106M封装,C106M尺寸

编辑-Z 韩景元可控硅C106M参数&#xff1a; 型号&#xff1a;C106M 断态重复峰值电压VDRM&#xff1a;600V 通态电流IT(RMS)&#xff1a;4A 通态浪涌电流ITSM&#xff1a;30A 平均栅极功耗PG(AV)&#xff1a;0.2W 峰值门功率耗散PGM&#xff1a;1W 工作接点温度Tj&…...

ChatGPT资料汇总学习

&#x1f9e0; Awesome-ChatGPT ChatGPT资料汇总学习&#xff0c;持续更新… ChatGPT再一次掀起了AI的热潮&#xff0c;是否还会像BERT一样成为AI进程上的里程碑事件&#xff0c;还是噱头炒作&#xff0c;持续关注&#xff0c;让时间流淌~ ChatGPT免费体验入口网址 http://c…...

什么是垂直扩容和水平扩容

垂直扩容和水平扩容是架构设计中常用的两种扩容方式&#xff0c;它们各有优势&#xff0c;应根据具体场景选择合适的扩容方式。 1.垂直扩容 垂直扩容是通过增加单个节点的处理能力来提高整个系统的性能&#xff0c;通常是通过增加服务器的硬件配置、升级CPU、内存、硬盘等来实…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】

微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来&#xff0c;Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...

PHP和Node.js哪个更爽?

先说结论&#xff0c;rust完胜。 php&#xff1a;laravel&#xff0c;swoole&#xff0c;webman&#xff0c;最开始在苏宁的时候写了几年php&#xff0c;当时觉得php真的是世界上最好的语言&#xff0c;因为当初活在舒适圈里&#xff0c;不愿意跳出来&#xff0c;就好比当初活在…...

中南大学无人机智能体的全面评估!BEDI:用于评估无人机上具身智能体的综合性基准测试

作者&#xff1a;Mingning Guo, Mengwei Wu, Jiarun He, Shaoxian Li, Haifeng Li, Chao Tao单位&#xff1a;中南大学地球科学与信息物理学院论文标题&#xff1a;BEDI: A Comprehensive Benchmark for Evaluating Embodied Agents on UAVs论文链接&#xff1a;https://arxiv.…...

前端导出带有合并单元格的列表

// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...

GitHub 趋势日报 (2025年06月08日)

&#x1f4ca; 由 TrendForge 系统生成 | &#x1f310; https://trendforge.devlive.org/ &#x1f310; 本日报中的项目描述已自动翻译为中文 &#x1f4c8; 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中&#xff0c;新增了一个本地验证码接口 /code&#xff0c;使用函数式路由&#xff08;RouterFunction&#xff09;和 Hutool 的 Circle…...

使用Spring AI和MCP协议构建图片搜索服务

目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式&#xff08;本地调用&#xff09; SSE模式&#xff08;远程调用&#xff09; 4. 注册工具提…...

【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案

目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后&#xff0c;迭代器会失效&#xff0c;因为顺序迭代器在内存中是连续存储的&#xff0c;元素删除后&#xff0c;后续元素会前移。 但一些场景中&#xff0c;我们又需要在执行删除操作…...

适应性Java用于现代 API:REST、GraphQL 和事件驱动

在快速发展的软件开发领域&#xff0c;REST、GraphQL 和事件驱动架构等新的 API 标准对于构建可扩展、高效的系统至关重要。Java 在现代 API 方面以其在企业应用中的稳定性而闻名&#xff0c;不断适应这些现代范式的需求。随着不断发展的生态系统&#xff0c;Java 在现代 API 方…...

WPF八大法则:告别模态窗口卡顿

⚙️ 核心问题&#xff1a;阻塞式模态窗口的缺陷 原始代码中ShowDialog()会阻塞UI线程&#xff0c;导致后续逻辑无法执行&#xff1a; var result modalWindow.ShowDialog(); // 线程阻塞 ProcessResult(result); // 必须等待窗口关闭根本问题&#xff1a…...