随着智能座舱功能越来越丰富,多媒体、车设、天气等业务都开始以卡片形式集成到 Launcher 中,Launcher 也逐渐变成了各类座舱业务的聚合入口。早期卡片数量较少时,还可以由 Launcher 团队直接开发,简单场景则通过 Widget 实现。随着卡片越来越多,我们不得不将卡片交给各个业务 Owner 开发,再以 AAR 的方式集成到 Launcher。
但 AAR 本质上仍然是编译期集成,业务与 Launcher 之间依然存在较强的版本和发布耦合。当卡片规模进一步扩大以后,Launcher 的调试、维护都变得非常困难,一个更自然的问题就出现了:
能不能让业务卡片不再作为 Launcher 的一部分参与编译,而是像"插件"一样,由独立 APK 提供实现,在运行时被 Launcher 动态发现、加载和使用?
这也就引出了 Android 中一个很有意思的架构设计------Plugin(插件)机制。通过 Plugin,我们可以尝试把 Launcher 从"所有业务代码的承载者",转变为"插件的宿主和调度者":Launcher 只定义接口、生命周期和展示容器,具体的卡片能力则由不同业务插件分别实现,从而进一步降低模块之间的耦合。

早期做互联网 Android 应用的朋友,对"插件化"这个概念应该并不陌生。不过这里要介绍的 Plugin,并不是常见的应用层插件化框架,而是 Android 原生系统中已经存在的一套插件机制 。例如博主早年介绍过的 车载 Android 应用开发与分析 - 初试 SystemUI Plugin,就是其中比较典型的应用。
本文将尝试移植 SystemUI Plugin Framework,并在自定义 Launcher 中实现对第三方 Widget 插件 APK 的动态加载。
这次移植主要有三个目标:
- 验证 宿主 APK 动态加载插件 APK 在车机环境中的可行性;
- 验证 Launcher 卡片由独立 APK 提供并动态加载 的架构方案;
- 将 SystemUI Plugin Framework 从 SystemUI 工程中剥离,封装为可独立维护和复用的 SDK。
一、整体架构与效果演示

工程一共包含四个 Gradle Module:

| Module | 类型 | 作用 |
|---|---|---|
| plugin-api | Library | Plugin 接口、Listener、生命周期接口和版本注解 |
| host | Application | 插件宿主,负责发现、加载和生命周期管理 |
| media-plugin | Application | 多媒体卡片插件 |
| weather-plugin | Application | 天气卡片插件 |
1.1 plugin-api:宿主和插件之间的 Sdk
plugin-api 不包含任何具体实现,只定义双方都需要遵守的接口。
主要包括:
| 接口 / 注解 | 作用 |
|---|---|
| Plugin | 所有插件的基础接口 |
| PluginListener | 插件生命周期回调 |
| PluginLifecycleManager | 控制单个插件实例的加载与卸载 |
| WidgetViewPlugin | Launcher 卡片插件接口 |
| LauncherOverlayPlugin | Launcher Overlay 插件接口 |
| @Requires | 声明依赖接口版本 |
| @ProvidesInterface | 声明 Plugin API 版本 |
对于 Launcher 来说,它并不需要知道"天气卡片"或者"音乐卡片"具体是怎么实现的,只需要认识:WidgetViewPlugin,然后调用类似:
scss
View createView(Context pluginContext);
即可获取插件提供的 View。这也是整个 Plugin 架构最核心的思想:
Host 依赖接口,而不是依赖具体业务实现。
二、插件是怎么被发现的
AOSP 的设计非常有意思。插件实现类会在 Manifest 中声明成一个 <service>:
ini
<service
android:name=".widget.MediaWidgetViewPlugin"
android:exported="false">
<intent-filter>
<action android:name="com.android.systemui.action.PLUGIN_WIDGET_VIEW" />
</intent-filter>
</service>
但这个 Service 不会真正被启动。它只是借用了 PackageManager 已有的 Intent 查询机制,把 Service 当成一个"插件注册表"。
AOSP 源码中的注释说得非常直接:
This isn't actually a service and shouldn't ever be started, but is a convenient PM based way to manage our plugins.
Host 只需要:
ini
Intent intent = new Intent(action);
List<ResolveInfo> result =
packageManager.queryIntentServices(intent, 0);
即可找到所有声明了对应 Action 的插件组件。
整个过程可以概括成:

这里需要注意:PackageManager 查询负责"发现候选插件",真正是否允许加载,则由 PluginActionManager 后续继续校验。
例如 AOSP 会进一步检查:
scss
mPm.checkPermission(PLUGIN_PERMISSION, packageName)
没有 Plugin 权限的 APK,即使被查询出来,也不会进入后续加载流程。
三、插件是怎么被加载的
SystemUI 并不会直接使用宿主 ClassLoader 去加载插件,而是为插件创建独立的:PathClassLoader
核心结构大致如下:

3.1 ClassLoader 隔离
核心代码来自 PluginInstance:
ini
List<String> zipPaths = new ArrayList<>();
List<String> libPaths = new ArrayList<>();
LoadedApk.makePaths(
null,
true,
appInfo,
zipPaths,
libPaths);
ClassLoader classLoader = new PathClassLoader(
TextUtils.join(File.pathSeparator, zipPaths),
TextUtils.join(File.pathSeparator, libPaths),
getParentClassLoader(baseClassLoader));
其中:
scss
LoadedApk.makePaths(...)
负责获得插件 APK、Native Library 等加载路径。然后创建:PathClassLoader 加载插件代码。
3.2 ClassLoaderFilter
插件的 Parent ClassLoader 并不是直接暴露整个 Host,而是包了一层:ClassLoaderFilter 。在我的移植版本中,只开放了几个必要的包:
c
androidx.constraintlayout.widget
com.android.systemui.common
com.android.systemui.log
com.android.systemui.plugin
其目的不是做真正意义上的"安全沙箱",而是限制插件能够直接访问的 Host 实现类。
更准确地说,它提供的是:
ClassLoader 层面的类可见性隔离。
插件仍然运行在 Host 进程,因此不能把它理解为进程级安全隔离。
3.3 反射创建插件
找到插件类以后,通过插件 ClassLoader 进行反射:
ini
ClassLoader loader = mClassLoaderFactory.get();
Class<T> instanceClass = (Class<T>) Class.forName(
mComponentName.getClassName(),
true,
loader);
T result = (T) mInstanceFactory.create(instanceClass);
至此:
markdown
Plugin APK
↓
PathClassLoader
↓
Class.forName()
↓
Plugin 实例
插件代码正式进入 Host 进程执行。
3.4 加载资源
仅仅加载 Java 类还不够,插件通常还需要访问自己的:
c
layout
drawable
string
color
style
因此 Host 还需要为插件创建独立的 Application Context:
ini
Context context =
mContext.createApplicationContext(appInfo, 0);
随后再包一层:
PluginContextWrapper
关键点是重写:
typescript
@Override
public ClassLoader getClassLoader() {
return mClassLoader;
}
同时对 LayoutInflater 做:
kotlin
cloneInContext(this)
这样插件在:
scss
LayoutInflater.inflate(...)
自己的 XML 时,遇到自定义 View 也能够通过插件自己的 ClassLoader 找到对应类。
最终:
PluginContext
├── Resources → Plugin APK
└── ClassLoader → Plugin PathClassLoader
插件的代码和资源才真正连在一起。
四、插件的生命周期
插件并不是简单地:
lua
load → 永久存在
而是拥有完整生命周期:
markdown
onPluginAttached
↓
onPluginLoaded
↓
onPluginUnloaded
↓
onPluginDetached
对应关系大致如下:

其中:
4.1 onPluginAttached
Host 已经发现 Plugin,但插件实例不一定马上加载。
Listener 可以返回:
arduino
false
实现延迟加载。
4.2 onPluginLoaded
插件已经完成:
sql
ClassLoader
→ Instance
→ PluginContext
→ Version Check
此时 Host 可以正式使用插件。
4.3 onPluginUnloaded
插件实例被释放。
这里应该:
- Remove View
- Cancel Animator
- Remove Handler Callback
- 释放 Listener
4.4 onPluginDetached
PluginInstance 生命周期彻底结束。
五、插件 APK 更新后发生什么
PluginManagerImpl 会监听:
PACKAGE_ADDED
PACKAGE_CHANGED
PACKAGE_REPLACED
PACKAGE_REMOVED
USER_UNLOCKED
例如插件执行:
adb install -r plugin.apk
系统发送:
PACKAGE_REPLACED
Host 收到以后会:
scss
PACKAGE_REPLACED
↓
clear ClassLoader
↓
reloadPackage()
↓
removePkg()
↓
queryPkg()
↓
创建新的 PluginInstance
↓
加载新插件
对应时序:

理论上,这条链路可以实现:
插件 APK 更新,而 Host APK 不重新安装。
如果框架实现完整,Host 本身也不应该依赖 force-stop 才能看到新代码。
如果实际调试中仍然必须:
arduino
adb shell am force-stop com.android.launcher3
才能更新,则通常说明还有:
- 旧 ClassLoader 没有清理;
- Plugin View 仍被持有;
- PluginInstance 生命周期没有完整退出;
- 静态对象持有旧插件;
- Context / Listener / Animator 存在引用;
等问题。
因此 force-stop 更适合作为调试兜底手段,而不应该成为正式 Plugin 热更新机制的一部分。
六、安全与稳定性
Plugin 最大的优势是灵活,但最大的风险也来自这里:
插件代码最终运行在 Host 进程里。
如果 Host 是:
android.uid.system
那么插件代码进入 Host 进程以后,实际上也是在这个进程的权限边界内执行。
因此这套机制不适合加载真正意义上的"不可信第三方 APK"。
它更适合:
OEM 内部业务插件
系统预装插件
平台签名插件
受控软件生态
AOSP 本身也做了多层保护。
6.1 Signature Permission
Host 定义:
ini
<permission
android:name="com.android.systemui.permission.PLUGIN"
android:protectionLevel="signature" />
Plugin 申请:
ini
<uses-permission
android:name="com.android.systemui.permission.PLUGIN" />
然后 Host 在加载插件之前再次检查:
scss
mPm.checkPermission(PLUGIN_PERMISSION,packageName)
因此流程其实是:
markdown
PackageManager 发现候选插件
↓
PLUGIN permission 校验
↓
Plugin Enabled 校验
↓
版本校验
↓
加载
在这个 PoC 中,Host 和 Plugin 使用同一套平台签名,从而限制能够接入 Plugin Framework 的 APK 范围。
6.2 Plugin API 版本校验
SystemUI Plugin 并不是简单判断:
versionCode
而是通过:
less
@ProvidesInterface
@Requires
描述 Plugin API 之间的依赖关系。
随后由 VersionChecker 进行匹配。这样 Host 可以判断:
- Plugin API 太老
- Plugin API 太新
- 依赖接口版本不匹配
而不是等到真正调用方法时才出现:NoSuchMethodError、LinkageError,对于长期演进的 Launcher Plugin API,这一点非常重要。
6.3 Crash 熔断
插件和 Host 运行在同一个进程,一个插件 Crash 就可能直接拖垮整个 Launcher。
因此 AOSP 设计了一套 Plugin Disable 机制。如果能够从堆栈中定位到某个 Plugin,则禁用对应 Plugin。如果无法判断是谁导致 Crash,则:
css
for (PluginActionManager<?> manager : mPluginMap.values()) {
manager.disableAll();
}
也就是:
找不到肇事插件时,宁可全部关闭。
对于 SystemUI 或 Launcher 这种核心系统进程,这种设计是合理的。
核心目标不是保证插件一定运行,而是:
优先保证宿主进程能够恢复。
6.4 Production Build 限制
AOSP Plugin Framework 本身对生产版本非常谨慎。
核心逻辑类似:
kotlin
if (!mIsDebuggable && !isPluginPrivileged(component)) {
return null;
}
即:
markdown
userdebug / eng
→ 可以用于普通 Plugin 调试
user
→ 只允许 privileged Plugin
因此这套框架本身就不是为"任意第三方 APK 动态执行代码"设计的。
它更接近:
受控环境下的系统模块动态扩展机制。
七、多个插件如何共存
这里还有一个比较值得注意的问题。PluginActionManager 默认:
ini
allowMultiple = false
当同一个 Action 查询到多个 Plugin 时,会认为出现冲突。
因此最初:
example-plugin
weather-plugin
都声明:PLUGIN_WIDGET_VIEW 时,并不能同时加载。本项目使用不同 Action 区分不同卡片类型:
PLUGIN_WIDGET_VIEW
PLUGIN_WIDGET_VIEW_WEATHER
Host 分别注册:
css
Listener A
Listener B
形成:

并通过:
javascript
Map<String, View> mPluginViews;
维护每个 Plugin 对应的 View 生命周期。
实际日志:
makefile
PluginInstance: Created plugin:
com.example.plugin.widget.ExampleWidgetViewPlugin
ExampleWidgetViewPlugin: onCreate
ExampleWidgetViewPlugin: createView
PluginInstance: Created plugin:
com.example.plugin.weather.WeatherWidgetViewPlugin
证明两个 Plugin 已经同时进入同一个 Host 进程。
八、总结
把 SystemUI Plugin 从 AOSP 中真正拆出来以后,会发现它的核心其实并不复杂。整套机制可以浓缩成几步:
markdown
Plugin API
↓
Manifest Service + Action
↓
PackageManager 发现
↓
Permission / Version 校验
↓
PathClassLoader
↓
PluginContext
↓
Reflection
↓
Plugin Lifecycle
但真正有价值的并不是"动态加载 APK"本身,而是它解决了一个架构问题:
markdown
传统 Launcher
Launcher APK
├── Media Card
├── Weather Card
├── Vehicle Card
├── Navigation Card
└── ...
↓
Plugin 化 Launcher
Launcher Host
├── Plugin API
├── Plugin Manager
└── Card Container
Media Plugin APK
Weather Plugin APK
Vehicle Plugin APK
Navigation Plugin APK
Launcher 从原来的:
所有业务代码的承载者
逐渐转变为:
Plugin 的宿主、调度者和展示容器。
业务卡片则可以拥有独立的代码、资源、版本和生命周期。对于传统互联网 App,这种依赖隐藏 API、运行在宿主进程内的 Plugin 方案显然不合适。
但对于能够掌控 Framework、系统签名以及整体软件生态的 智能座舱平台 来说,这套 Plugin 机制反而非常适合作为模块解耦的一种手段。理解并掌握其实现原理后,它的应用场景也不必局限于 Launcher 和 SystemUI,同样可以扩展到其他需要动态扩展和独立交付的系统模块中。
本文由 ChatGPT-5.6 Sol 辅助编写,代码由 Deepseek V4-Flash 生成,KiMi-K3 二次 Review。