Skip to content

插件框架 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,详见异常。

查询当前插件 ​

自己的信息直接问就行,不需要把包名写死:

成员类型说明
currentPluginPackageNameString?当前插件包名。进程被系统重建后框架会从落盘记录恢复,不会丢
currentPluginClassNameString?当前插件的入口类名
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该插件是否"装了"
installedPluginsMap<String, PluginInfo?>已装插件表。value 可能是 null —— 目录在、但还没装载完成

isPluginInstalled 判的是插件目录是否存在,不查 PackageManager:插件不是系统安装的应用,"装了"就等于"磁盘上有它那个目录"。

宿主环境 ​

成员类型说明
getHostContext()Context宿主 Context;已释放会抛 RuntimeException
hostPackageNameString宿主包名
hostResourcesResources宿主资源
hostAssetAssetManager宿主 assets
hostThemeResources.Theme宿主主题
hostClassLoaderClassLoader宿主类加载器

别把两套资源搞混

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 是例外:它不做安装检查,不抛这个异常。