当前位置: 首页 > news >正文

AOSP 12.0 SystemUI设计原理浅析之插件化

SystemUI 插件化系统详解

一、为什么引入插件化

核心痛点

SystemUI 是一个随 Android 平台一起刷机的系统应用,它的更新路径极其重:修改一行代码 → 整包编译 → OTA 推送 → 重启设备。这在原型验证阶段代价极高。AOSP 引入插件化(Plugin 系统)专门解决快速原型迭代这一场景,其设计目标有三:

  1. 解耦实验性代码与主干:原型代码不进 master 分支,只装在 dogfood 设备上;
  2. 热更新:修改原型只需重装一个小 APK,无需重刷系统;
  3. 隔离崩溃:插件崩溃不会让整个 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) 所有具体插件接口(ClockPluginOverlayPlugin 等)和 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() { }
}

注意 sysuiContextpluginContext 是两个不同的 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 的内部类(如 StatusBarNotificationStackScrollLayout);
  • 插件只能通过版本化的接口与 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 样式

开发步骤

  1. plugin/ExamplePlugin/ 为模板,在 Android 源码树中新建项目目录;
  2. Android.bp 中配置 libs: ["MtkSystemUIPluginLib"]certificate: "platform"
  3. 实现目标接口,在类上加 @Requires 注解;
  4. AndroidManifest.xml 中声明 PLUGIN 权限和对应 action 的 Service;
  5. 编译并安装:m YourPlugin && adb install -r $OUT/...,安装后即时生效,无需重启 SystemUI;
  6. 设置 → 系统 → 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/ 目录下的版本化接口。

pluginContextsysuiContext 的用途不同

  • inflate 插件自己的布局、访问插件自己的资源 → 用 pluginContext
  • 访问系统资源 ID(如 status_bar_height)→ 用 sysuiContext
  • PluginUtils.setId(sysuiContext, view, "some_id") 是标准做法。

版本不匹配时插件被静默跳过
不会有任何异常抛出。如果插件装上去没有生效,首先检查 logcat 中 PluginManager tag 的输出,确认版本号是否匹配。

不要把 MtkSystemUIPluginLib 放进 static_libs
这会将接口类打包进插件 APK,运行时 SysUI 加载插件后同一个接口类名会出现在两个 ClassLoader 中,导致 ClassCastException

http://www.rkmt.cn/news/1468806.html

相关文章:

  • 2026年 海立压缩机厂家推荐榜单:卧式压缩机/热泵压缩机/空调压缩机/冷库压缩机专业品牌深度解析 - 品牌企业推荐师(官方)
  • 2026惠州头部GEO企业领跑赛道,AI原生获客构建全域增长新范式 - 阿威说AI
  • 企业周年庆全员纪念礼去哪订?智美源头工厂批量定制 - GrowthUME
  • 混元3.0提示词工程:中文语义锚点与结构化指令设计指南
  • 为什么Inter字体正在重新定义数字排版标准:战略性的用户体验革命
  • 合同管理的五个“反常识”结论
  • 2026年 艾默生卧式/涡旋/空调/热泵/冷库压缩机品牌推荐:高效稳定与低温制热核心优势深度解析 - 品牌企业推荐师(官方)
  • 【自媒体技术分析】头条号、小红书和公众号文章违规词、敏感词、AI率、低价创作、同质化和模板化问题的底层原因分析
  • 如何利用Tinke深入探索和修改NDS游戏资源?完整技术指南
  • 3个关键设置让虚拟摄像头效果翻倍:OBS插件深度配置指南
  • LTspice变压器仿真建模:从互感原理到SPICE参数化实践
  • 广东一站式宠物包装定制/彩盒包装/礼品盒包装服务商/供货商/源头工厂选择:踩坑3年总结的5个考察要点 - 变量人生001
  • GSEA结果解读与美化:从clusterProfiler输出到发表级图表(含AI调色技巧)
  • 2026年深圳包包回收一站式指南:合扬六区门店与专业鉴定,卖包不迷茫! - 奢侈品交易观察员
  • 广东高企金融咨询服务机构排行:合规与实效双维度筛选 - 互联网科技品牌测评
  • 别再瞎找了!2026年闭眼可入的专业一键生成论文工具
  • 从2层板到10层板:手把手教你规划KiCad多层PCB的叠层结构与命名(附常用方案)
  • 从Retinex到Zero-Reference:低光照增强算法这十年,我用代码带你跑一遍
  • MATLAB扫地机器人仿真包:含A*路径规划、动态避障与U型转向功能
  • 3DS上的宝可梦存档管家:如何用PKSM一站式管理你的精灵收藏
  • 太原工商注册代办机构排行 企业服务选择客观参考 - 互联网科技品牌测评
  • 择优择校指南:江西师资雄厚民办高职盘点,优质院校实力一览 - 品牌测评鉴赏家
  • 5步终极指南:用Gaggiuino微控制器改造你的Gaggia Classic咖啡机
  • RimWorld性能优化终极方案:Performance-Fish深度解析与实战指南
  • 编写程序根据出差奔波时长,住宿环境,综合评估旅途疲劳值,推荐快速恢复方案。
  • MATLAB版带时间窗VRP遗传算法求解工具包,含完整函数与实测数据
  • 3大突破:从技术债到性能飞跃的架构重构之旅
  • 文心大模型5.0正式版:从技术参数到服务契约的范式跃迁
  • 2026年电采暖选购指南:河北贺达新能源如何定义采暖新标准 - 企业名录精选推荐
  • Windows 11优化神器:Win11Debloat让你的电脑速度提升51%的秘诀