plugin-framework 的插件开发示例与教程。
框架 v2.1 起只接受 .jar 插件包,且必须在 jar 内声明 META-INF/plugin.properties。
宿主加载插件时做的是 classLoader.loadClass(mainClass).getDeclaredConstructor().newInstance(),
因此插件类必须满足:
- 类名与
mainClass完全一致(含包名),且类是public - 有无参构造函数(默认构造函数即可;不要写只有带参构造的类)
- 实现
Plugin接口(常驻插件实现BackgroundPlugin,它继承Plugin) - 打包成
.jar,且 jar 内 必须有classes.dex(不是.class) - jar 内必须有
META-INF/plugin.properties,声明mainClass - 一个插件只能有一个 dex 文件(不要生成
classes2.dex),插件类应保持精简
execute(proxy, args) 的 args 语义:
| 调用来源 | args 内容 |
|---|---|
| 应用内「手动执行」按钮 | null |
子插件调度 dispatcher.call(id, extra) |
{"subPluginId": id} 加上 extra 的内容 |
也就是说:想让插件可配置,就走子插件调度,界面上的手动执行不传参。
插件源码需要引用宿主的两组类:
com.java.myapplication.Plugincom.java.myapplication.ShizukuProxycom.java.myapplication.BackgroundPlugin(仅后台常驻插件需要)com.java.myapplication.SubPluginDispatcher(仅子插件调度需要)
获取方式:编译宿主仓库得到 classes.jar(app/build/intermediates/.../classes),或把这几个接口源文件复制进插件工程。
package com.example;
import com.java.myapplication.Plugin;
import com.java.myapplication.ShizukuProxy;
import java.util.Map;
public class MyPlugin implements Plugin {
@Override
public String getName() { return "我的插件"; }
@Override
public String getDescription() { return "这是我的第一个插件"; }
@Override
public String getVersion() { return "1.0.0"; }
@Override
public boolean needsShizuku() { return true; }
@Override
public String execute(ShizukuProxy proxy, Map<String, ?> args) {
return proxy.execCommand("echo Hello World!");
}
}签名注意:execute 的参数必须写成 Map<String, ?>。宿主侧对应 Kotlin 的 Map<String, Any>?,编译后是 Map<String, ? extends Object>;写成 Map<String, Object> 会报 name clash ... same erasure 编译错误。
needsShizuku() 返回 false 表示插件不依赖提权,框架在 Shizuku 未授权时也允许执行。
新建 META-INF/plugin.properties:
mainClass=com.example.MyPlugin支持的键:
| 键 | 必填 | 说明 |
|---|---|---|
mainClass |
是 | 插件入口类全名 |
uid |
否 | 插件唯一标识 |
version |
否 | 版本号,缺省取 Plugin.getVersion() |
description |
否 | 描述,缺省取 Plugin.getDescription() |
subPlugins |
否 | 子插件 ID 列表,逗号分隔 |
关键点:jar 里必须是 classes.dex,不能是 .class。 框架用 DexClassLoader 加载,只有 JVM 字节码的 jar 会报 Failed to open dex files ... Entry not found,插件不会出现在列表里。
本仓库提供封装好的 build.sh:
./build.sh plugins/MyPlugin.java com.example.MyPlugin "/path/to/host-classes.jar:/path/to/android.jar"它依次完成 javac → d8 → 写入 META-INF/plugin.properties → 打包,产出 MyPlugin.jar。
手动执行等价于:
# 1) 编译
javac -cp host-classes.jar -d build/classes src/com/example/MyPlugin.java
# 2) 转成 dex
D8_JAR=$ANDROID_HOME/build-tools/<版本>/lib/d8.jar
java -cp "$D8_JAR" com.android.tools.r8.D8 --min-api 24 --output build/dex \
$(find build/classes -name '*.class')
# 3) 声明入口类
mkdir -p build/META-INF
echo "mainClass=com.example.MyPlugin" > build/META-INF/plugin.properties
# 4) 打包
cp build/dex/classes.dex build/classes.dex
( cd build && jar cf ../MyPlugin.jar classes.dex META-INF/plugin.properties )打开 Plugin Framework 应用,点右下角 +,选择 MyPlugin.jar。非后台插件会出现在「一次性任务」Tab。
| 文件 | 说明 | 需要 Shizuku |
|---|---|---|
| TestPlugin.java | 最小模板:回显参数与当前身份,演示免提权插件 | 否 |
| DeviceInfoPlugin.java | 只读示例:getProp + execCommand 组合并分段格式化 |
是 |
| BackgroundMonitor.kt | 常驻示例:BackgroundPlugin 循环 + subPlugins + dispatcher.call(id, args) |
是 |
BackgroundMonitor 带一份 同名 .properties 示例,演示 subPlugins 声明;build.sh 会自动采用它。
interface Plugin {
fun getName(): String
fun getDescription(): String
fun getVersion(): String
fun execute(proxy: ShizukuProxy, args: Map<String, Any>? = null): String
fun needsShizuku(): Boolean = true
}execute 的返回值会直接显示在结果弹窗中,注意排版。
| 方法 | 说明 |
|---|---|
execCommand(cmd) |
执行任意 shell 命令 |
getProp(prop) |
读取系统属性 |
installApk(path) |
静默安装 APK |
uninstallApp(packageName) |
卸载应用 |
launchApp(packageName, activityName?) |
启动应用 |
getSetting(namespace, key) |
读取系统设置 |
putSetting(namespace, key, value) |
修改系统设置 |
isShizukuAvailable() |
Shizuku 服务是否可用 |
checkPermission() |
是否已授权 |
namespace 仅接受 system / secure / global。所有参数都会先做格式校验,非法输入直接返回 Error: ...,命令不会执行。
后台插件实现 BackgroundPlugin(继承 Plugin):
interface BackgroundPlugin : Plugin {
suspend fun runInBackground(
proxy: ShizukuProxy,
scope: CoroutineScope,
dispatcher: SubPluginDispatcher
)
}后台常驻插件必须用 Kotlin 编写。 runInBackground 是 Kotlin 的 suspend fun,Java 编译后的签名会多出一个 Continuation<? super Unit> 参数,无法用普通 Java 方法实现。
完整可编译示例见 plugins/BackgroundMonitor.kt:
class BackgroundMonitor : BackgroundPlugin {
override fun getName() = "BackgroundMonitor"
override fun getDescription() = "每 10 秒采样一次"
override fun getVersion() = "1.0.0"
override fun execute(proxy: ShizukuProxy, args: Map<String, Any>?) =
"这是常驻插件,请在「长期任务」Tab 启动"
override suspend fun runInBackground(
proxy: ShizukuProxy,
scope: CoroutineScope,
dispatcher: SubPluginDispatcher
) {
while (scope.isActive) {
val battery = dispatcher.call("battery")
delay(10_000)
}
}
}scope 由宿主管理,点「停止」或宿主进程结束时取消,循环随之中断。
运行期间宿主会拉起前台服务保活,所以退出界面不会中断任务。
构建 Kotlin 插件需要额外两个变量(kotlin-compiler-embeddable 自身还依赖 kotlin-reflect 等):
KOTLINC_CP="/path/kotlin-compiler-embeddable.jar:/path/kotlin-stdlib.jar:/path/kotlin-reflect.jar:..."
KOTLIN_CP="/path/kotlin-stdlib.jar:/path/kotlinx-coroutines-core-jvm.jar"
KOTLINC_CP="$KOTLINC_CP:$KOTLIN_CP" \
KOTLIN_CP="$KOTLIN_CP" \
./build.sh plugins/BackgroundMonitor.kt com.plugin.monitor.BackgroundMonitor \
"/path/to/host-classes.jar:/path/to/android.jar"在 plugin.properties 中声明子插件 ID:
mainClass=com.example.AdKiller
uid=com.example.adkiller
version=2.0.0
subPlugins=monitor,kill,skip常驻插件通过 SubPluginDispatcher 调用:
val battery = dispatcher.call("battery")
val model = dispatcher.call("prop", mapOf("key" to "ro.product.model"))同一个子插件 ID 串行执行,不会并发触发。
调用参数会透传到被调度插件的 execute:上面第二行拿到的 args 是
{"subPluginId": "prop", "key": "ro.product.model"}。
注意子插件 ID 由声明它的那个插件自己响应——宿主遍历已加载插件,找到 plugin.properties
里 subPlugins 包含该 ID 的插件,然后把 subPluginId 交给它的 execute。
因此 BackgroundMonitor 里调用的 battery / prop 最终走回它自己的 execute 分支。
- 必须实现
Plugin接口,且 jar 内含META-INF/plugin.properties - 不要尝试绕过参数校验,宿主会在执行前拦截非法输入
- 插件以宿主进程身份运行,拥有宿主全部权限以及 Shizuku 的 ADB 权限,只加载可信来源的插件
- 插件无法使用宿主 Android 资源系统(
R.layout、R.string等) - 插件运行在应用进程内,默认没有存储权限,不要直接写
/sdcard
插件没出现在列表里:
- 确认是
.jar,扩展名正确 - 确认 jar 内有
classes.dex(用unzip -l XxxPlugin.jar查看;只有.class一定加载失败) - 确认 jar 内含
META-INF/plugin.properties - 确认
mainClass与真实类名一致 - 确认类实现了
Plugin接口 - 确认
execute用的是Map<String, ?>签名
日志里出现 Failed to open dex files from <path> because: Entry not found 就是第 2 条。
执行报 Error: 未获取Shizuku权限,请先授权:打开 Shizuku 应用重新授权本框架。
看日志:
logcat -s ShizukuProxy:V PluginLoader:V PluginManager:V BackgroundPluginHost:VApache License 2.0,与主仓库一致。