SystemUI 插件化系统详解
一、为什么引入插件化
核心痛点
SystemUI 是一个随 Android 平台一起刷机的系统应用,它的更新路径极其重:修改一行代码 → 整包编译 → OTA 推送 → 重启设备。这在原型验证阶段代价极高。AOSP 引入插件化(Plugin 系统)专门解决快速原型迭代这一场景,其设计目标有三:
- 解耦实验性代码与主干:原型代码不进
master分支,只装在 dogfood 设备上; - 热更新:修改原型只需重装一个小 APK,无需重刷系统;
- 隔离崩溃:插件崩溃不会让整个 SystemUI 不可用。
不能用的场景
插件化有一个硬性约束:只在 Build.IS_DEBUGGABLE == true(userdebug/eng)构建上生效。这是刻意的——它是一个开发工具,不是生产交付手段。量产设备(user build)上插件扫描逻辑完全不会执行。
二、插件化设计原理
总体分层
┌─────────────────────────────────────────────────────────┐
│ MtkSystemUI 进程 │
│ │
│ ┌────────────────┐ PluginManager ┌─────────────┐ │
│ │ 宿主 Hook 点 │◄──────────────────►│ PluginLib │ │
│ │ (addListener) │ │ 接口定义 │ │
│ └────────────────┘ └──────┬──────┘ │
│ │ 接口实现由│
│ ┌─────────────────────────────┐ │ SysUI提供 │
│ │ PathClassLoader (插件APK) │ │ │
│ │ 父ClassLoader 仅暴露 │◄───────┘ │
│ │ com.android.systemui.plugin.* │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘▲│ adb install│
┌─────────┴──────────┐
│ 插件 APK │
│ (平台签名) │
│ 声明 PLUGIN 权限 │
│ Service + action │
└────────────────────┘
模块职责划分
| 模块 | 职责 |
|---|---|
plugin_core/ (MtkPluginCoreLib) |
Plugin 接口、PluginListener 接口、所有注解 |
plugin/ (MtkSystemUIPluginLib) |
所有具体插件接口(ClockPlugin、OverlayPlugin 等)和 PluginDependency |
src/.../plugins/PluginInitializerImpl.java |
桥接 SysUI 的 Dependency DI 到 PluginManager |
src/.../plugins/PluginEnablerImpl.java |
用 PackageManager 组件启用/禁用状态管理插件开关 |
src/.../plugins/PluginDependencyProvider.java |
向插件安全暴露 SysUI 内部依赖 |
1. 接口定义层(plugin_core/ + plugin/)
所有插件接口都有三个要素:
// plugin/src/com/android/systemui/plugins/OverlayPlugin.java
@ProvidesInterface(action = OverlayPlugin.ACTION, version = OverlayPlugin.VERSION)
public interface OverlayPlugin extends Plugin {String ACTION = "com.android.systemui.action.PLUGIN_OVERLAY";int VERSION = 4; // 接口不兼容变更时必须递增void setup(View statusBar, View navBar);// default 方法:可以在不递增 VERSION 的情况下新增功能default void setup(View statusBar, View navBar, Callback callback,DozeParameters dozeParameters) {setup(statusBar, navBar); // 兼容旧插件}
}
@ProvidesInterface 注解使用 RetentionPolicy.RUNTIME,由 PluginManager 在运行时通过反射读取,用于版本比对。
Plugin 基础接口只有两个生命周期回调:
// plugin_core/src/com/android/systemui/plugins/Plugin.java
public interface Plugin {default void onCreate(Context sysuiContext, Context pluginContext) { }default void onDestroy() { }
}
注意 sysuiContext 和 pluginContext 是两个不同的 Context:插件用 pluginContext 访问自己 APK 内的资源,用 sysuiContext 访问 SysUI 的资源。
2. 插件 APK 的结构
插件 APK 必须满足以下三项声明:
AndroidManifest.xml
<!-- 1. 声明持有权限 -->
<uses-permission android:name="com.android.systemui.permission.PLUGIN" /><!-- 2. 用 Service 声明插件入口点(非真实 Service,仅利用 PackageManager 查询机制) -->
<service android:name=".SampleOverlayPlugin"><intent-filter><action android:name="com.android.systemui.action.PLUGIN_OVERLAY" /></intent-filter>
</service><!-- 3. 可选:声明插件设置 Activity -->
<activity android:name=".PluginSettings"><intent-filter><action android:name="com.android.systemui.action.PLUGIN_SETTINGS" /></intent-filter>
</activity>
Android.bp — 关键:PluginLib 必须是 libs(运行时依赖),不能是 static_libs,否则会把接口打包进插件 APK,与 SysUI 进程中已有的实现产生类冲突:
android_app {name: "MtkExamplePlugin",libs: ["MtkSystemUIPluginLib"], // 运行时依赖,不打包进 APKcertificate: "platform", // 必须平台签名srcs: ["src/**/*.java"],
}
实现类 — 用 @Requires 声明依赖,以便版本检查器验证:
@Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION)
public class SampleOverlayPlugin implements OverlayPlugin {@Overridepublic void onCreate(Context sysuiContext, Context pluginContext) {mPluginContext = pluginContext; // 保存插件自己的 Context}@Overridepublic void setup(View statusBar, View navBar) {// 用 pluginContext 加载自己的布局mOverlayView = LayoutInflater.from(mPluginContext).inflate(R.layout.colored_overlay, (ViewGroup) statusBar, false);((ViewGroup) statusBar).addView(mOverlayView);}@Overridepublic void onDestroy() {// 必须清理所有引用,防止内存泄漏进 SysUI 进程if (mOverlayView != null) {mOverlayView.post(() ->((ViewGroup) mOverlayView.getParent()).removeView(mOverlayView));}}
}
3. SysUI 宿主:注册监听
在 SysUI 内部使用插件的标准模式:
@SysUISingleton
public class MyController {private OverlayPlugin mActivePlugin;@Injectpublic MyController(PluginManager pluginManager) {pluginManager.addPluginListener(OverlayPlugin.ACTION,new PluginListener<OverlayPlugin>() {@Overridepublic void onPluginConnected(OverlayPlugin plugin, Context pluginContext) {// 插件就绪,调用其方法plugin.setup(mStatusBarWindow, mNavBarView);mActivePlugin = plugin;}@Overridepublic void onPluginDisconnected(OverlayPlugin plugin) {// 插件被卸载/禁用,停止使用并释放引用mActivePlugin = null;}},OverlayPlugin.VERSION,true /* allowMultiple */);}
}
4. PluginManager 的扫描与加载流程
PluginManager(实现在 shared 库的 PluginManagerImpl)的完整工作流程:
addPluginListener(action, listener, version)│▼
PackageManager.queryIntentServices(Intent(action))│ 扫描所有声明该 action 的 Service▼
对每个找到的 ComponentInfo:│├─ 检查 1: Build.IS_DEBUGGABLE == true? 否 → 忽略│├─ 检查 2: 持有 PLUGIN signature 权限? 否 → 记录违规,忽略│├─ 检查 3: 组件是否处于 enabled 状态? 否 → 忽略│▼
创建 PathClassLoader:parent = 经过过滤的 SysUI ClassLoader(只暴露 com.android.systemui.plugin.* 包)path = 插件 APK 路径│▼
反射实例化插件类│▼
版本检查:读取实现类的 @Requires(target=X.class, version=N)与宿主接口 @ProvidesInterface(version=M) 比对N != M → 忽略插件(版本不匹配)│▼
plugin.onCreate(sysuiContext, pluginContext)
pluginListener.onPluginConnected(plugin, pluginContext)
ClassLoader 过滤是安全隔离的核心。插件 APK 的父 ClassLoader 只暴露 com.android.systemui.plugin.* 包,这意味着:
- 插件无法访问 SysUI 的内部类(如
StatusBar、NotificationStackScrollLayout); - 插件只能通过版本化的接口与 SysUI 交互;
- 插件可以随意引入自己的第三方库,不会与 SysUI 发生类名冲突。
5. 崩溃隔离机制
PluginManagerImpl 注册了一个全局的 UncaughtExceptionHandler。当 SysUI 崩溃时:
捕获到 Throwable│▼
遍历所有已加载插件,检查 stackTrace 中的类名
是否属于某个插件的包名?│├─ 是 → PluginEnablerImpl.setDisabled(component, DISABLED_FROM_CRASH)│ 将 reason 写入 SharedPreferences("auto_disabled_plugins_prefs")│ PackageManager.setComponentEnabledSetting(COMPONENT_ENABLED_STATE_DISABLED)│└─ 否 → 禁用所有插件(防止是多插件交互导致的崩溃)
PluginEnablerImpl 同时维护 auto_disabled_plugins_prefs SharedPreferences,记录是因崩溃被禁还是手动禁用,SystemUI Tuner 界面据此显示不同的状态标记。
6. 插件向 SysUI 请求依赖(PluginDependency)
插件不能直接访问 SysUI 内部对象,但可以通过白名单机制获得部分 SysUI 依赖:
宿主侧,在 PluginInitializerImpl.onPluginManagerInit() 中注册允许暴露的依赖:
Dependency.get(PluginDependencyProvider.class).allowPluginDependency(ActivityStarter.class);
插件侧,先在 @Requires 中声明依赖,再通过 PluginDependency.get() 获取:
@Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION)
@Requires(target = ActivityStarter.class, version = ActivityStarter.VERSION)
public class MyPlugin implements OverlayPlugin {public void launchSomething() {ActivityStarter starter = PluginDependency.get(this, ActivityStarter.class);starter.startActivity(intent, true);}
}
PluginDependencyProvider.get() 会先验证插件是否通过 @Requires 声明了该依赖,未声明则抛出 IllegalArgumentException,防止插件越权获取未声明的 SysUI 对象。
三、日常开发中如何使用
场景一:使用已有 Hook 快速验证 UI 方案
目前可直接使用的插件 Hook(完整列表见 docs/plugin_hooks.md):
| Action | 接口 | 用途 |
|---|---|---|
PLUGIN_OVERLAY |
OverlayPlugin |
在状态栏/导航栏 Window 上叠加自定义 View |
PLUGIN_CLOCK |
ClockPlugin |
替换锁屏时钟 |
PLUGIN_QS |
QS |
完整替换快捷设置面板 |
PLUGIN_QS_FACTORY |
QSFactory |
替换/新增 QS Tile 及其布局 |
PLUGIN_VOLUME |
VolumeDialog |
替换音量对话框 |
PLUGIN_GLOBAL_ACTIONS |
GlobalActions |
替换长按电源键菜单 |
PLUGIN_TOAST |
ToastPlugin |
替换 Toast 样式 |
开发步骤:
- 以
plugin/ExamplePlugin/为模板,在 Android 源码树中新建项目目录; Android.bp中配置libs: ["MtkSystemUIPluginLib"],certificate: "platform";- 实现目标接口,在类上加
@Requires注解; AndroidManifest.xml中声明PLUGIN权限和对应action的 Service;- 编译并安装:
m YourPlugin && adb install -r $OUT/...,安装后即时生效,无需重启 SystemUI; - 在 设置 → 系统 → SystemUI Tuner 中可看到插件开关,可手动启用/禁用。
场景二:在 SysUI 主代码中新增一个插件 Hook 点
适合需要从外部替换某块行为,但又不想让实验性代码污染主干的情况。
第一步:在 plugin/src/com/android/systemui/plugins/ 中定义接口:
@ProvidesInterface(action = MyFeaturePlugin.ACTION, version = MyFeaturePlugin.VERSION)
public interface MyFeaturePlugin extends Plugin {String ACTION = "com.android.systemui.action.PLUGIN_MY_FEATURE";int VERSION = 1;void onSomeEvent(SomeData data);// 用 default 方法保持向后兼容,避免频繁递增 VERSIONdefault void onAnotherEvent(OtherData data) { }
}
第二步:在宿主 Controller 中注入 PluginManager 并注册监听:
@SysUISingleton
public class MyFeatureController {private MyFeaturePlugin mPlugin;@Injectpublic MyFeatureController(PluginManager pluginManager) {pluginManager.addPluginListener(MyFeaturePlugin.ACTION,new PluginListener<MyFeaturePlugin>() {@Overridepublic void onPluginConnected(MyFeaturePlugin plugin, Context ctx) {mPlugin = plugin;}@Overridepublic void onPluginDisconnected(MyFeaturePlugin plugin) {mPlugin = null;}},MyFeaturePlugin.VERSION,false /* 单插件模式 */);}public void handleEvent(SomeData data) {if (mPlugin != null) {mPlugin.onSomeEvent(data); // 有插件时走插件路径} else {// 默认实现}}
}
第三步:在 docs/plugin_hooks.md 中登记新 Hook,方便后续开发者发现。
场景三:版本兼容性管理
接口修改时的判断原则:
| 变更类型 | 是否递增 VERSION |
|---|---|
新增方法并提供 default 实现 |
不需要,旧插件仍可用 |
新增方法无 default 实现 |
必须递增,旧插件加载时版本不匹配会被静默忽略 |
| 修改现有方法签名 | 必须递增 |
| 删除方法 | 必须递增 |
四、关键约束与常见陷阱
onDestroy() 必须清理所有强引用
插件代码被加载进 SysUI 进程,onDestroy() 之后如果还持有 View、Context 等引用,这些对象会永久驻留在 SysUI 的堆中,且无法被 GC 回收。
不要在插件中直接引用 SysUI 内部类
编译时可能通过(因为编译依赖了 MtkSystemUI-core),但运行时 ClassLoader 过滤会导致 ClassNotFoundException。插件与 SysUI 的交互边界只能是 plugin/ 目录下的版本化接口。
pluginContext 与 sysuiContext 的用途不同
- inflate 插件自己的布局、访问插件自己的资源 → 用
pluginContext; - 访问系统资源 ID(如
status_bar_height)→ 用sysuiContext; PluginUtils.setId(sysuiContext, view, "some_id")是标准做法。
版本不匹配时插件被静默跳过
不会有任何异常抛出。如果插件装上去没有生效,首先检查 logcat 中 PluginManager tag 的输出,确认版本号是否匹配。
不要把 MtkSystemUIPluginLib 放进 static_libs
这会将接口类打包进插件 APK,运行时 SysUI 加载插件后同一个接口类名会出现在两个 ClassLoader 中,导致 ClassCastException。
