外观
插件框架 com.hualuo.plugin.framework
框架由宿主在运行时提供,插件通过 compileOnly(libs.hualuo.plugin.framework) 只在编译期引用,不打进插件 APK。
⚠️ 这一节只讲插件侧能用的 API。 框架里还有一批是宿主专用的(
PluginManager.init/install/uninstall、ProxyActivity、ProxyService、ReflectUtils、NativeLibExtractor等),插件调用它们没有意义,也不保证稳定 —— 这里不列。
插件用得到的四组
| 类 | 用途 |
|---|---|
BasePluginActivity | 插件入口基类(每个插件必用) |
IPluginActivity | 上面的接口,定义插件的生命周期与转发能力 |
PluginManager | 启动其它插件 / 启动插件前台服务 |
BasePluginService | 插件服务基类(需要后台常驻时才用) |
BasePluginActivity — 插件入口基类
插件的 MainActivity 继承它。它把宿主的 ProxyActivity 包了一层,让插件代码看起来像普通 Activity。
kotlin
class MainActivity : BasePluginActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { PluginTheme { App(onFinish = { finish() }) } }
}
}属性
| 成员 | 说明 |
|---|---|
activity | 宿主真 Activity(androidx.activity.ComponentActivity)。跳转、申请权限这类需要真 Activity 的场景用它 |
生命周期(全部可覆写)
宿主 ProxyActivity 会把生命周期分发进来:
onCreate → onStart → onResume → onPause → onStop → onDestroy
onSaveInstanceState / onRestoreInstanceState转发给宿主的方法
插件里可以直接调,行为等同在真 Activity 上调用:
| 方法 | 说明 |
|---|---|
setContentView(int) / setContentView(View) | 设置内容视图 |
findViewById<T>(int) | 查视图 |
assets / resources | 资源访问(注意是插件自己的资源) |
layoutInflater | 布局加载器 |
startActivity(Intent) | 跳转(同插件内的 Activity) |
finish() | 关闭当前页面 |
setTitle(CharSequence) / setTitle(int) | 设置标题 |
onKeyDown(int, KeyEvent) | 按键回调 |
enableEdgeToEdge() | 开全屏沉浸(转发给宿主) |
UI 需要退出/跳转时,从入口把
activity的能力以回调传进去(如App(onFinish = { finish() })),不要在 UI 里写findActivity()逐层解包 —— 那条路在多 Activity 插件里会拿错实例。
PluginManager — 跨插件能力
全局单例,直接 PluginManager.xxx 调。下面列出的成员插件都能用;init / install / uninstall / installCurrentPlugin / tryInstallCurrentPlugin / deletePluginDir 是宿主和框架自己用的,插件调它们没有意义。
插件前台服务
kotlin
private const val PLUGIN_PACKAGE = "com.zhinian.plugin.music"
PluginManager.startPluginForegroundService(PLUGIN_PACKAGE, intent)
PluginManager.stopPluginService(PLUGIN_PACKAGE, intent)插件要后台常驻(放音乐、悬浮窗、长任务)时用这两个。包名要传插件自己的(写死一个常量即可,参考 obsession-plugin-music 的写法)。
用之前得先在插件 Manifest 里声明 <service>,类名写插件自己的实现类:
xml
<service
android:name=".player.PlayerService"
android:foregroundServiceType="mediaPlayback" />宿主 Manifest 里注册的是框架的 ProxyService,两边靠这套代理对上;foregroundServiceType 按用途选(mediaPlayback / specialUse …),Android 14+ 必须声明对应类型,否则启动前台服务会抛异常。
启动其它插件的 Activity
最省事的是 launchPlugin —— 它自己读目标插件 Manifest 里声明的入口类,不用你拼 Intent:
kotlin
PluginManager.launchPlugin("com.zhinian.plugin.music")要指定具体 Activity、或带参数,用 startPluginActivity:
kotlin
PluginManager.startPluginActivity(packageName, intent)| 参数 | 说明 |
|---|---|
packageName | 目标插件包名(如 com.zhinian.plugin.music) |
intent | 要传的 Intent(component 由框架换成宿主的代理 Activity) |
两个都会在目标插件没装时抛 PluginNotInstallException,详见异常。
查询当前插件
自己的信息直接问就行,不需要把包名写死:
| 成员 | 类型 | 说明 |
|---|---|---|
currentPluginPackageName | String? | 当前插件包名。进程被系统重建后框架会从落盘记录恢复,不会丢 |
currentPluginClassName | String? | 当前插件的入口类名 |
getCurrentPluginInfo() | PluginInfo | 当前插件信息;拿不到抛 RuntimeException |
getCurrentPluginInfoOrNull() | PluginInfo? | 上面那个的安全版,拿不到返回 null |
getCurrentPluginApkPath() | String | 当前插件 APK 的绝对路径;拿不到抛 RuntimeException |
getCurrentPluginApkPathOrNull() | String? | 上面那个的安全版 |
这四个都是函数,get 前缀不能省
currentPluginInfoOrNull / currentPluginApkPathOrNull 这种写法不存在 —— 只有 currentPluginPackageName / currentPluginClassName 是属性,其余四个都是函数。
查询其它插件
| 成员 | 类型 | 说明 |
|---|---|---|
getPluginInfo(pkg) | PluginInfo | 指定插件的信息;没装或没加载都会抛 RuntimeException |
getPluginInfoOrNull(pkg) | PluginInfo? | 上面那个的安全版,拿不到返回 null |
isPluginInstalled(pkg) | Boolean | 该插件是否"装了" |
installedPlugins | Map<String, PluginInfo?> | 已装插件表。value 可能是 null —— 目录在、但还没装载完成 |
isPluginInstalled 判的是插件目录是否存在,不查 PackageManager:插件不是系统安装的应用,"装了"就等于"磁盘上有它那个目录"。
宿主环境
| 成员 | 类型 | 说明 |
|---|---|---|
getHostContext() | Context | 宿主 Context;已释放会抛 RuntimeException |
hostPackageName | String | 宿主包名 |
hostResources | Resources | 宿主资源 |
hostAsset | AssetManager | 宿主 assets |
hostTheme | Resources.Theme | 宿主主题 |
hostClassLoader | ClassLoader | 宿主类加载器 |
别把两套资源搞混
hostResources / hostAsset 是宿主的。插件自己的资源在 BasePluginActivity 的 resources / assets / layoutInflater 上(见上文)。
读错了不会报错,只会安静地拿到宿主的资源 —— 表现是"图片、字符串莫名其妙不对",很难查。
路径与目录
插件在磁盘上的布局固定是 <插件目录>/<包名>/plugin.apk:
| 成员 | 返回 |
|---|---|
getPluginDir(pkg) | <pluginsDir>/<包名> 的绝对路径 |
getPluginApkPath(pkg) | <pluginsDir>/<包名>/plugin.apk 的绝对路径 |
getPluginApkPathByPluginDir(pluginDir) | 给定目录下的 plugin.apk 绝对路径 |
类加载与资源读取框架都处理好了,插件平时不用碰这些路径 —— 只有要把文件路径交给第三方库(自建播放器、解包工具之类)时才用得上。
PluginInfo
上面几个方法返回的数据类:
| 字段 | 说明 |
|---|---|
pluginName | 插件名 |
packageName | 包名 |
versionName | 版本名 |
launchClassName | 入口类名(Manifest 里那个 meta-data 声明的) |
pluginDir | 插件目录 |
pluginApkPath | 插件 APK 路径 |
pluginContext | 该插件的 PluginContext |
BasePluginService — 插件服务基类
需要后台常驻时继承它。插件服务不是真 Service —— 宿主 Manifest 里注册的是 ProxyService,框架负责把回调转发进来。
kotlin
class PlayerService : BasePluginService() {
override fun onCreate() { /* 初始化 */ }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { … }
override fun onDestroy() { /* 收尾 */ }
}可覆写的方法
| 方法 | 说明 |
|---|---|
onCreate() | 创建 |
onStartCommand(intent, flags, startId): Int | 启动命令(返回值语义同系统 Service) |
onBind(intent): IBinder? | 绑定 |
onUnbind(intent): Boolean / onRebind(intent) | 解绑 / 重绑 |
onDestroy() | 销毁 |
onLowMemory() / onTrimMemory(level) | 内存紧张 |
onConfigurationChanged(newConfig) | 配置变化 |
onTaskRemoved(rootIntent) | 任务被移除 |
onTimeout(startId) / onTimeout(startId, fgsType) | 超时(Android 14+ 前台服务类型超时) |
受保护的成员
| 成员 | 说明 |
|---|---|
proxyService | 宿主那个真 Service |
context | 可直接用的 Context(已折算成插件上下文) |
IPluginActivity — 接口
BasePluginActivity 实现的接口。一般不直接实现它 —— 继承 BasePluginActivity 就自动满足。
列在这里是为了让你知道基类到底提供了什么、漏了什么:
attach(ComponentActivity) onCreate(Bundle) onStart()
onResume() onPause() onStop()
onDestroy() onSaveInstanceState(Bundle)
onRestoreInstanceState(Bundle) setContentView(Int | View) findViewById(Int)
assets resources layoutInflater startActivity(Intent) finish()
setTitle(CharSequence | Int) onKeyDown(Int, KeyEvent) enableEdgeToEdge()异常
PluginNotInstallException
com.hualuo.plugin.framework.exception.PluginNotInstallException,继承 RuntimeException。
跨插件那五个方法都会抛,不只是带 @Throws 标注的三个:
| 方法 | @Throws 标注 | 实际会抛 |
|---|---|---|
startPluginActivity | ✅ | ✅ |
startPluginForegroundService | ✅ | ✅ |
bindPluginService | ✅ | ✅ |
stopPluginService | ❌ | ✅ |
launchPlugin | ❌ | ✅ |
后两个没有 @Throws,但内部一样走 checkPluginInstalled —— Kotlin 不会因此放过你。@Throws 只是给 Java 调用方看的注解,Kotlin 侧从来不靠它做检查,别拿它当"这个方法安全"的依据。
两种抛法,message 不同:
| message | 什么时候 |
|---|---|
插件未安装: <包名> | isPluginInstalled(pkg) 为 false |
插件已安装但未加载: <包名> | 装是装了,但 getPluginInfoOrNull(pkg) 返回 null —— 这份插件信息还没加载出来 |
所以调用点一律兜住,而且别把非预期异常一起吞了:
kotlin
runCatching {
PluginManager.startPluginActivity("com.zhinian.plugin.music", intent)
}.onFailure { e ->
if (e is PluginNotInstallException) {
PopupPrompt.error("对方插件还没装")
} else {
throw e // 真出别的错就别装没事,否则现场全没了
}
}不想用异常做流程控制,就先判断再调用 —— 这两个方法不抛:
kotlin
if (PluginManager.isPluginInstalled(pkg)) { … }
PluginManager.getPluginInfoOrNull(pkg)?.let { … }unbindPluginService 是例外:它不做安装检查,不抛这个异常。