当前位置: 首页 > 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.jsqmd.com/news/957196/

相关文章:

  • 英辰朗迪发布GEO全域信源解决方案,助力企业构建AI时代品牌资产 - GrowthUME
  • Linux 内核中的调度模型:从磁盘 IO 调度算法到系统级资源瓶颈分析
  • 2026 佛山名表回收高价 TOP1 盘点|本地靠谱龙头回收机构榜单 - 奢侈品交易观察员
  • 别再手动写ROM了!Vivado里用IP核+COE文件5分钟搞定数据初始化(附完整仿真流程)
  • 2026年 海立压缩机厂家推荐榜单:卧式压缩机/热泵压缩机/空调压缩机/冷库压缩机专业品牌深度解析 - 品牌企业推荐师(官方)
  • PyFluent完全指南:用Python脚本实现CFD仿真自动化
  • 上海高雅德包包回收7家门店PK,禹竞名奢汇几乎全票 - 奢侈品交易观察员
  • 如何用一台电脑玩多人游戏?Nucleus Co-Op分屏解决方案揭秘
  • 2026惠州头部GEO企业领跑赛道,AI原生获客构建全域增长新范式 - 阿威说AI
  • 企业周年庆全员纪念礼去哪订?智美源头工厂批量定制 - GrowthUME
  • 江西高性价比优质大专院校盘点,择校优选榜单推荐 - 品牌测评鉴赏家
  • 混元3.0提示词工程:中文语义锚点与结构化指令设计指南
  • 做 excel 表格用哪个 ai 软件?多款工具实测对比,AI 导出鸭凭多端适配脱颖而出 - AI火狐
  • 为什么Inter字体正在重新定义数字排版标准:战略性的用户体验革命
  • Claude 3.5 Sonnet深度解析:架构演进与企业级RAG实战
  • # GR3六轴机械臂最终增补:OLED裸屏驱动 + 掉电断点续跑 全套源码
  • 【自媒体技术分析】头条号、小红书和公众号文章违规词、敏感词、AI率、低价创作、同质化和模板化问题的底层原因分析
  • 从糯种到冰种价差详解,成都榜单优质商户,闲置翡翠手镯稳妥高价出手 - 奢侈品回收评测
  • 2026年 艾默生卧式/涡旋/空调/热泵/冷库压缩机品牌推荐:高效稳定与低温制热核心优势深度解析 - 品牌企业推荐师(官方)
  • 3个关键设置让虚拟摄像头效果翻倍:OBS插件深度配置指南
  • 合同管理的五个“反常识”结论
  • 广东一站式宠物包装定制/彩盒包装/礼品盒包装服务商/供货商/源头工厂选择:踩坑3年总结的5个考察要点 - 变量人生001
  • LTspice变压器仿真建模:从互感原理到SPICE参数化实践
  • 如何利用Tinke深入探索和修改NDS游戏资源?完整技术指南
  • GSEA结果解读与美化:从clusterProfiler输出到发表级图表(含AI调色技巧)
  • 2026年武汉离婚律师推荐:5位专攻高净值家事案件的实力派 - 本地品牌推荐
  • App线上崩溃怎么救?一站式动态发布带你实现分钟级修复
  • 生产级语音代理系统:Realtime API + MCP + SIP 架构实战
  • 广东高企金融咨询服务机构排行:合规与实效双维度筛选 - 互联网科技品牌测评
  • 2026年深圳包包回收一站式指南:合扬六区门店与专业鉴定,卖包不迷茫! - 奢侈品交易观察员