当前位置: 首页 > 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、内存、硬盘等来实…...

conda相比python好处

Conda 作为 Python 的环境和包管理工具&#xff0c;相比原生 Python 生态&#xff08;如 pip 虚拟环境&#xff09;有许多独特优势&#xff0c;尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处&#xff1a; 一、一站式环境管理&#xff1a…...

【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器

——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的​​一体化测试平台​​&#xff0c;覆盖应用全生命周期测试需求&#xff0c;主要提供五大核心能力&#xff1a; ​​测试类型​​​​检测目标​​​​关键指标​​功能体验基…...

数据链路层的主要功能是什么

数据链路层&#xff08;OSI模型第2层&#xff09;的核心功能是在相邻网络节点&#xff08;如交换机、主机&#xff09;间提供可靠的数据帧传输服务&#xff0c;主要职责包括&#xff1a; &#x1f511; 核心功能详解&#xff1a; 帧封装与解封装 封装&#xff1a; 将网络层下发…...

【Java_EE】Spring MVC

目录 Spring Web MVC ​编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 ​编辑参数重命名 RequestParam ​编辑​编辑传递集合 RequestParam 传递JSON数据 ​编辑RequestBody ​…...

汇编常见指令

汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX&#xff08;不访问内存&#xff09;XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...

select、poll、epoll 与 Reactor 模式

在高并发网络编程领域&#xff0c;高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表&#xff0c;以及基于它们实现的 Reactor 模式&#xff0c;为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。​ 一、I…...

Angular微前端架构:Module Federation + ngx-build-plus (Webpack)

以下是一个完整的 Angular 微前端示例&#xff0c;其中使用的是 Module Federation 和 npx-build-plus 实现了主应用&#xff08;Shell&#xff09;与子应用&#xff08;Remote&#xff09;的集成。 &#x1f6e0;️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...

【Linux手册】探秘系统世界:从用户交互到硬件底层的全链路工作之旅

目录 前言 操作系统与驱动程序 是什么&#xff0c;为什么 怎么做 system call 用户操作接口 总结 前言 日常生活中&#xff0c;我们在使用电子设备时&#xff0c;我们所输入执行的每一条指令最终大多都会作用到硬件上&#xff0c;比如下载一款软件最终会下载到硬盘上&am…...

Python竞赛环境搭建全攻略

Python环境搭建竞赛技术文章大纲 竞赛背景与意义 竞赛的目的与价值Python在竞赛中的应用场景环境搭建对竞赛效率的影响 竞赛环境需求分析 常见竞赛类型&#xff08;算法、数据分析、机器学习等&#xff09;不同竞赛对Python版本及库的要求硬件与操作系统的兼容性问题 Pyth…...

大模型——基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程

基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程 下载安装Docker Docker官网:https://www.docker.com/ 自定义Docker安装路径 Docker默认安装在C盘,大小大概2.9G,做这行最忌讳的就是安装软件全装C盘,所以我调整了下安装路径。 新建安装目录:E:\MyS…...