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替换为我们自己的ZeusClassLoader。ZeusClassLoader的构造函数中将会传递进宿主的ClassLoader,除此之外,其内部有一个mClassLoaderList变量,保存着所有插件ClassLoader的集合。于是ZeusClassLoader的loadClass方法,会先尝试使用宿主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,发射调用这个newAssetManager的addAssetPath方法把插件Apk的路径加载进来,然后根据这个newAssetManager生成一个新的Resource对象newResource,然后在宿主Activity中重写getResources和getAssets返回newAssetManager和newResource,这样宿主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中的四大组件,需要以下几个步骤: 1. 预先在宿主的AndroidManifest文件中声明插件中的四大组件 <?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android"http://schemas.android.co…...
00后卷王自述,我真的很卷吗?
前段时间我去面试了一个软件测试公司,成功拿到了offer,薪资也从10k涨到了18k,对于工作都还没两年的我来说,还是比较满意的,毕竟有些工作了3到4年的可能还没有我的高。 在公司一段时间后大家都说我是卷王,其…...
真题详解(树的结点)-软件设计(八十四)
真题详解(汇总)-软件设计(八十三)https://blog.csdn.net/ke1ying/article/details/130856130?spm1001.2014.3001.5501 COCOMOII估算不包括_____。 对象点 B.功能点 C.用例数 D.源代码行 答案:C 语法翻译是一种ÿ…...
LDA算法实现鸢尾花数据集降维
目录 1. 作者介绍2. LDA降维算法2.1 基本概念2.2 算法流程 3. LDA算法实现3.1 数据集介绍3.2 代码实现3.3 结果展示 1. 作者介绍 唐杰,男,西安工程大学电子信息学院,2022级研究生 研究方向:机器视觉与人工智能 电子邮件ÿ…...
深入理解Linux虚拟内存管理
系列文章目录 Linux 内核设计与实现 深入理解 Linux 内核(一) 深入理解 Linux 内核(二) Linux 设备驱动程序(一) Linux 设备驱动程序(二) Linux 设备驱动程序(三…...
自动化测试框架、Python面向对象以及POM设计模型简介
目录 1 自动化测试框架概述 2 自动化测试框架需要的环境 3 自动化测试框架设计思想:Python面向对象 4 自动化测试框架设计思想:POM(Page Object Model)页面对象模型 1 自动化测试框架概述 所谓的框架其实就是一个解决问题…...
【CSSpart4--盒子模型】
CSSpart4--盒子模型 网页布局的三大核心:盒子模型,浮动,定位网页布局的过程(本质):盒子模型的组成四部分:边框,内容,内边距,外边距 一 、盒子边框border:1.1 …...
Linux - Java 8 入门安装与重装教程集锦
一、入门初始安装 1. 具体安装教程 1. linux 系统中如何安装java环境(通过tar.gz文件) 安装包下载链接 Java 的 tar.gz 安装包下载链接传送门 Linux 系统的 Java 环境变量配置教程 1. linux查看java版本,以及配置java home 2. Linux环…...
2023年最新企业网盘排行榜出炉
随着云计算技术的不断发展,企业日常工作中大量的资料、文档等信息需要实现集中管理,此时企业网盘工具就应运而生。企业网盘是一种可用于企业内部管理、团队协作及文件共享的云存储平台,能够极大提高企业办公效率和安全性。 一、企业网盘的帮助…...
C++内存分类
内存分配方式(内存布局): 内存5分类 堆、栈、自由存储区、全局/静态存储区、常量存储区 (1)栈:内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数,函数调用后返回的地址。(为运行函数而…...
不是说00后已经躺平了吗,怎么还是这么卷.....
都说00后已经躺平了,但是有一说一,该卷的还是卷。 前段时间我们部门就来了个00后,工作都还没两年,跳到我们公司起薪20K,都快接近我了。 后来才知道人家是个卷王,从早干到晚就差搬张床到工位睡觉了。最近和…...
国内免费版ChatGPT
目录 前言:网站大全 1. ChatGPT是什么 2. ChatGPT的发展历程 3. ChatGPT对程序员的影响 4. ChatGPT对普通人的影响 5. ChatGPT的不足之处 前言:网站大全 AI文本工具站 (laicj.cn) ——gpt-3.5 功能强大(推荐) Chatgpt在线网页版-…...
常用本地事务和分布式事务解决方案模型
目录 1 DTP模型2 2PC2.1 方案简介2.2 处理流程2.2.1 阶段1:准备阶段2.2.2 阶段2:提交阶段 2.3 方案总结 3 3PC3.1 方案简介3.2 处理流程3.2.1 阶段1:canCommit3.2.2 阶段2:preCommit3.3.3 阶段3:do Commit 3.3 方案总结…...
无代码玩转GIS应用,我也在行【文末送书】
您好,我是码农飞哥(wei158556),感谢您阅读本文,欢迎一键三连哦。💪🏻 1. Python基础专栏,基础知识一网打尽,9.9元买不了吃亏,买不了上当。 Python从入门到精通…...
xlsx是什么格式
xlsx是什么格式? xlsx是Excel文档的扩展名,其基于Office Open XML标准的压缩文件格式,取代了其以前专有的默认文件格式,在传统的文件名扩展名后面添加了字母x,即.xlsx取代.xls。 xlsx文件是什么格式? xlsx是Excel表格的文件格…...
将 Maven 配置为使用阿里云镜像
将 Maven 配置为使用阿里云镜像的步骤如下: 打开 Maven 的 settings.xml 文件:在 Maven 安装目录下的 conf 文件夹中,找到 settings.xml 文件,并打开它。 添加镜像配置:在 settings.xml 文件中,找到 <m…...
行业报告 | 2022文化科技十大前沿应用趋势(下)
原创 | 文 BFT机器人 04 商业创新 趋势7:区块链技术连接传统文化,数字藏品市场在探索中发展 核心内容: 2022年,数字藏品在区块链技术的助力下应运而生。狭义的数字藏品是指使用区块链技术、基于特定的文化资源所生成唯一的数字凭…...
ASEMI代理韩景元可控硅C106M参数,C106M封装,C106M尺寸
编辑-Z 韩景元可控硅C106M参数: 型号:C106M 断态重复峰值电压VDRM:600V 通态电流IT(RMS):4A 通态浪涌电流ITSM:30A 平均栅极功耗PG(AV):0.2W 峰值门功率耗散PGM:1W 工作接点温度Tj&…...
ChatGPT资料汇总学习
🧠 Awesome-ChatGPT ChatGPT资料汇总学习,持续更新… ChatGPT再一次掀起了AI的热潮,是否还会像BERT一样成为AI进程上的里程碑事件,还是噱头炒作,持续关注,让时间流淌~ ChatGPT免费体验入口网址 http://c…...
什么是垂直扩容和水平扩容
垂直扩容和水平扩容是架构设计中常用的两种扩容方式,它们各有优势,应根据具体场景选择合适的扩容方式。 1.垂直扩容 垂直扩容是通过增加单个节点的处理能力来提高整个系统的性能,通常是通过增加服务器的硬件配置、升级CPU、内存、硬盘等来实…...
(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
题目:3442. 奇偶频次间的最大差值 I 思路 :哈希,时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况,哈希表这里用数组即可实现。 C版本: class Solution { public:int maxDifference(string s) {int a[26]…...
19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...
跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
数据库分批入库
今天在工作中,遇到一个问题,就是分批查询的时候,由于批次过大导致出现了一些问题,一下是问题描述和解决方案: 示例: // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...
保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek
文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama(有网络的电脑)2.2.3 安装Ollama(无网络的电脑)2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…...
Go 并发编程基础:通道(Channel)的使用
在 Go 中,Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式,用于在多个 Goroutine 之间传递数据,从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...
Git常用命令完全指南:从入门到精通
Git常用命令完全指南:从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...
怎么让Comfyui导出的图像不包含工作流信息,
为了数据安全,让Comfyui导出的图像不包含工作流信息,导出的图像就不会拖到comfyui中加载出来工作流。 ComfyUI的目录下node.py 直接移除 pnginfo(推荐) 在 save_images 方法中,删除或注释掉所有与 metadata …...
嵌入式常见 CPU 架构
架构类型架构厂商芯片厂商典型芯片特点与应用场景PICRISC (8/16 位)MicrochipMicrochipPIC16F877A、PIC18F4550简化指令集,单周期执行;低功耗、CIP 独立外设;用于家电、小电机控制、安防面板等嵌入式场景8051CISC (8 位)Intel(原始…...
