plugins 插件系统
plugins 插件系统
plugins 是 JSXHook 提供的插件兼容层,用来在脚本里加载、切换、卸载插件。
从源码实现上看,它不是简单地“反射调用一个 apk”,而是做了下面几层事情:
- 解析要加载的是哪个插件包
- 尝试从“内嵌插件”或“已安装插件”里找到对应来源
- 创建插件上下文和类加载器
- 调用插件注册类的
loadDefault(...)或load(...) - 读取插件脚本资源目录里的
./index - 把插件导出的入口对象返回给脚本
如果你把它理解成“脚本侧的插件加载器”,这个理解方向是对的。
先记住这 8 条
plugins.load(packageName)的packageName不能为空;空字符串会直接抛异常。- 第二个参数
pluginName是可选的,空字符串会被当成“没传”。 - 同一个
packageName + pluginName组合重复加载时,会直接返回缓存过的导出对象。 - 同一个包如果之前已经有激活中的插件,再加载同包另一个插件前,源码会先卸载当前激活句柄。
plugins.load()需要有效的 Androidcontext;没有context时会抛异常,不是返回null。- 加载顺序是:优先尝试内嵌插件包,找不到再尝试已安装插件包。
plugins.unload(packageName)返回true的前提是当前确实存在这个包的激活插件句柄并成功移除;只删掉本地缓存但没有激活句柄时,最终仍可能是false。plugins.unloadAll()返回的是“本次卸载了多少个激活插件”,不是布尔值。
plugins.load(packageName, pluginName?)
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
packageName | string | 非空插件包名 | 无 | 会先 trim(),空字符串会报错 |
pluginName | string | 可选插件名 | 无 | 会先 trim(),空字符串会被当成未传 |
返回值
any返回值到底是什么
它返回的是插件入口导出的对象,准确地说是:
- 找到插件实例
- 读取插件声明的 assets 脚本根目录
- 加载其中的
./index - 如果入口模块本身还会被调用,拿它的最终导出结果
- 把这个最终结果返回给脚本
所以返回值类型不是固定的,可能是:
普通对象
函数
带方法的模块导出对象
其他插件自定义导出值
真实加载流程
源码里的流程大致是这样:
先清理
packageName/pluginName校验
packageName非空根据
packageName + pluginName生成缓存键如果这个缓存键已经加载过,直接返回缓存 exports
如果当前这个包已经有激活中的插件,先卸载它
解析宿主
context优先尝试加载内嵌插件
内嵌插件找不到时,再尝试已安装插件
通过插件注册类加载默认插件或命名插件
读取插件 assets 脚本目录并执行
./index返回最终导出对象
pluginName省略和不省略的区别
源码分支是明确的:
| 写法 | 实际调用 |
|---|---|
plugins.load("com.example.demo") | 调插件注册类的 loadDefault(...) |
plugins.load("com.example.demo", "customName") | 调插件注册类的 load("customName", ...) |
也就是说,第二个参数不是“显示名”,而是会真正影响插件注册类走哪个加载分支。
缓存规则
缓存键由 packageName 和 pluginName 组成:
| 组合 | 缓存键概念 |
|---|---|
只传 packageName | packageName default |
传了 pluginName | packageName pluginName |
这带来两个效果:
同一组合重复
load(),直接命中缓存。同包不同
pluginName虽然缓存键不同,但源码在真正加载前会先卸载当前这个包的激活实例,所以它不是“无限并存多个激活插件”。失败时会怎样
plugins.load() 不是“失败就返回 null”的风格。很多失败情况会直接抛异常,比如:
packageName为空- 当前没有有效 Android
context - 插件没有可用
classLoader - 插件注册类加载失败
loadDefault()/load()没返回可用实例- 插件返回了空的 assets 脚本目录
- 插件入口脚本
./index不存在或执行失败
所以调用它时,实际项目里很适合包一层 try/catch。
示例 1:加载默认插件
const plugin = plugins.load("com.example.plugin");
log(plugin);
示例 2:加载同包下的命名插件
const plugin = plugins.load("com.example.plugin", "customName");
log(plugin);
示例 3:给失败做兜底
function safeLoadPlugin(packageName, pluginName) {
try {
return plugins.load(packageName, pluginName);
} catch (error) {
log(`load plugin failed: ${error}`);
return null;
}
}
const plugin = safeLoadPlugin("com.example.plugin");
示例 4:按导出对象来使用
const plugin = plugins.load("com.example.plugin");
if (plugin && typeof plugin.run === "function") {
plugin.run();
}
一个很容易忽略的细节
这两句并不等价:
plugins.load("com.example.plugin", "");
plugins.load("com.example.plugin");
从最终效果上看,它们会走同一个“默认插件”分支,因为空字符串会先被 trim(),再当成“没传第二参数”处理。
plugins.unload(packageName)
参数
| 参数 | 类型 | 可填值 | 说明 |
|---|---|---|---|
packageName | string | 插件包名 | 会先 trim() |
返回值
boolean真实行为
源码会做两件事:
先把这个包对应的本地缓存项删掉
再尝试移除当前激活中的该包插件句柄,并执行清理逻辑
返回值规则
| 情况 | 返回值 |
|---|---|
packageName 为空或全空白 | false |
| 当前没有这个包的激活插件句柄 | false |
| 当前有激活句柄并成功移除 | true |
这里的“卸载”包含什么
如果插件之前真的激活过,清理过程会尽量做这些事:
- 清掉这个包对应的本地缓存 exports
- 调插件实例上的清理逻辑
- 解绑可能的插件服务
- 通知注册器执行 detached 逻辑
- 调用绑定时保存的 cleanup 回调
所以它不是单纯“把 JS 变量删掉”,而是带生命周期清理的卸载。
示例
plugins.unload("com.example.plugin");
更稳的写法
if (!plugins.unload("com.example.plugin")) {
log("plugin was not active or unload failed");
}
plugins.unloadAll()
参数
无
返回值
number返回值含义
返回的是“本次参与卸载的激活插件数量”。
例如:
没有任何激活插件时,返回
0有 2 个激活插件时,返回
2真实行为
枚举当前所有激活插件句柄
逐个移除并执行 cleanup
清空本地缓存
返回处理的数量
示例
const count = plugins.unloadAll();
log(`unloaded ${count} plugins`);
适合什么场景
脚本退出前统一清理
调试插件切换逻辑时做重置
你不确定当前激活了哪些插件,但想先清场再重载
实用建议
- 把
load()当成可能抛异常的 API 来写
- 把
let plugin = null;
try {
plugin = plugins.load("com.example.plugin");
} catch (error) {
log(error);
}
2. 同包多插件切换时,显式先卸载会更好读
虽然源码内部会在重载前处理激活句柄,但显式写出来更不容易让人误解:
plugins.unload("com.example.plugin");
const plugin = plugins.load("com.example.plugin", "toolbox");
3. 返回值没有统一结构,先判断再用
const plugin = plugins.load("com.example.plugin");
if (plugin && typeof plugin.init === "function") {
plugin.init();
}
4. 如果你在做独立模块导出,最好固定 `plugins.load("包名")` 的写法
手册里提到,独立模块导出时会扫描脚本里的 plugins.load("包名") 依赖。
所以包名最好写成稳定、明确、可静态识别的字符串,不要在这里塞太动态的拼接。
最后压成一句话
plugins.load()是一个“会缓存、会切换激活句柄、会抛异常”的正式加载器,不是随便拿个对象出来那么简单。plugins.unload()和plugins.unloadAll()也不是单纯删变量,而是带清理生命周期的卸载动作。- 真正在脚本里用时,最稳的习惯就是:
try/catch + 判断导出结构 + 结束时清理。
