android surfaceflinger 实现自定义layer 拓展方法(2)
Android 14 SurfaceFlinger自定义Layer合成方案设计
本文系统梳理了在Android 14下,如何通过SurfaceFlinger实现自定义Layer的插入与合成,TaskView机制嵌入第三方应用窗口,输入事件的转发与管理,C++自研RenderEngine的渲染调度,以及车载多窗口场景下多组件统一动画调度架构。适用于车载、智能座舱等多窗口复杂场景,帮助开发者实现高效、可扩展的多层级UI合成与动画。
1. SurfaceFlinger中插入自定义 Layer 的机制
SurfaceFlinger 合成管线示意:多个来源的 BufferQueue(应用窗口、系统UI等)提交给 SurfaceFlinger,由其使用 GPU 或 HWC 合成输出。
在此架构下,我们可以通过 SurfaceControl 创建新的 Layer 接入合成流程。
在 Android 图形架构中,每个窗口都对应 SurfaceFlinger 的一个 Layer(包含 Surface 与 SurfaceControl 元数据)。WindowManager 通常通过 SurfaceControl 请求 SurfaceFlinger 创建 Layer 并将 Surface 交给应用绘制。要插入自定义 Layer,我们可以模拟这一过程:使用 SurfaceComposerClient/SurfaceControl 在 C++ 中创建一个新的 SurfaceFlinger Layer,并将其附加到默认显示的层级结构中。例如,可以获取内置显示的 token,创建一个 SurfaceControl (作为 Container 容器层或带 Buffer 的层),设置适当的尺寸、像素格式和 Z 顺序,使其参与 SurfaceFlinger 合成流程。
创建自定义 Layer 的步骤如下:
- 创建 SurfaceControl:通过 SurfaceComposerClient::createSurface 或 NDK 的 ASurfaceControl 接口创建 SurfaceFlinger Layer。这将向 SurfaceFlinger 请求一个新的 BufferQueue 和 SurfaceControl,用于我们的自定义内容。
- 设置父子关系:如果需要嵌入到现有层级,可以在创建时指定父 SurfaceControl(例如默认显示器的根层或某个窗口的 SurfaceControl)。也可以创建一个顶层窗口(需要系统权限)附加到屏幕。
- 显示 Layer:使用 SurfaceControl.Transaction 将新建 SurfaceControl 设置为可见、设置其尺寸和位置,并提交到 SurfaceFlinger。确保该 Layer 的 Z 顺序正确(比如在应用窗口之上或特定顺序),这样它会在合成时被计算。
通过上述机制,我们的自定义 Layer 将和普通窗口一样进入 SurfaceFlinger的图形列表,在每帧 VSYNC 合成时得到处理。
需要注意权限和时机:创建顶层Surface通常要求SYSTEM_UI权限或在系统进程中执行。此外,如果我们设计自定义 Layer 作为容器层,它本身可不含缓冲(通过 setContainerLayer),专门用于承载子Layer,实现分组合成和统一变换。
2. 在自定义 Layer 内嵌入第三方应用 Surface
要在上述自定义 Layer 中嵌入第三方应用窗口,可利用 Android 14 最新架构中的 TaskView 等机制。TaskView 提供了跨进程嵌入 UI 的能力,是 SurfaceView 的子类,实现了 ShellTaskOrganizer.TaskListener 接口。通过 TaskView,可将外部应用的 Activity 启动并显示在本进程的 Surface 中。其原理是在应用内创建一个 SurfaceView(即我们的自定义 Layer或容器),然后使用 ShellTaskOrganizer 控制Activity启动,并将目标应用的 Surface 重定向为该 SurfaceView 的子层。
具体实现流程:
- 启动嵌入的 Activity:利用 ActivityOptions#setLaunchBounds 或 PendingIntent 启动第三方应用的 Activity,使其以指定大小启动。TaskView 提供 startActivity(PendingIntent, ActivityOptions, Rect) 接口,将目标 Activity 启动并限制在我们容器的区域内。
- TaskOrganizer 回调:WindowManager 通知 ShellTaskOrganizer 有新任务(Task)出现,TaskView 作为 TaskListener 接收到 onTaskAppeared 回调,此时获得嵌入任务的 SurfaceControl(通常称为 Task 的 leash)。
- Surface 重定向:TaskView 在 Surface创建后,通过 SurfaceControl.Transaction.reparent 将嵌入应用的 SurfaceControl 设置为 TaskView Surface 的子层。这样,第三方应用窗口的 Surface 就被挂接到我们的自定义 Layer/SurfaceControl 下。AOSP 中 TaskView 的实现展示了这一过程:在 surfaceCreated 时调用 mTransaction.reparent(mTaskLeash, getSurfaceControl()).show(…),将任务表面作为子层并显示。结果是在 SurfaceFlinger 的层级中,我们自定义的 Layer 成为父层,第三方应用窗口作为其子 Layer 存在并一起合成。
这种通过 SurfaceControl 重父(reparent)的方式,使嵌入的应用窗口真正成为自定义 Layer 组成的一部分,从而实现统一渲染和同步动画。与早期通过 VirtualDisplay 嵌入(ActivityView)方式不同,无需独立的显示,直接在同一显示上组合渲染,效率更高且动画更顺畅。
为了管理嵌入的 Surface,可以封装一个管理类(类似 TaskView)在 C++ 层。利用 AIDL ITaskOrganizer 或 JNI 调用 ShellTaskOrganizer,也能在 native 层获得任务出现/消失事件和 SurfaceControl句柄。总之,通过上述机制,第三方应用的 Surface 会作为SurfaceControl 对象交由我们控制,我们可以对其做缩放、旋转、透明度等变换,与自定义内容统一布局。
3. 输入事件的转发与管理
嵌入第三方应用窗口后,需要保证该窗口能正常接收触摸、键盘等输入事件。Android 输入系统(InputDispatcher)会根据每个窗口的触摸区域和Z顺序将事件分发给相应窗口。为实现输入转发,我们需调整输入配置,使嵌入的应用窗口区域内的事件正确送达该应用,而不被父层拦截。
3.1 调整Touchable区域
可以参考 TaskView 对输入的处理方式。TaskView实现了 ViewTreeObserver.OnComputeInternalInsetsListener 接口,在 onComputeInternalInsets() 中修改触摸可点击区域。具体做法是:初始将整个根窗口区域设为可触摸,然后减去 TaskView 本身的矩形区域。这样,TaskView所在区域对宿主来说变为“空洞”,使得点击该区域不会传递给宿主Activity,而是落到下面的嵌入窗口。上述机制确保用户触摸TaskView区域时,由系统将事件分发给嵌入的第三方Activity的窗口。我们在自定义方案中,可采用类似策略:当自定义 Layer 显示嵌入应用时,调整宿主窗口或容器的触摸域,排除嵌入内容区域。
3.2 输入通道与焦点
嵌入的应用窗口本身在WMS中有独立的 InputChannel 和焦点管理。当用户触摸嵌入区域,InputDispatcher会根据坐标找到对应的目标窗口(第三方应用窗口)并通过其 InputChannel 发送事件。因此,需要确保第三方窗口在系统中可接收输入:例如,其 WindowState 应处于可交互状态(非不可触摸标志),并且SurfaceFlinger层级中的可见区域与实际嵌入区域匹配。由于我们通过SurfaceControl改变了窗口的位置和父子关系,WindowManager仍认为该窗口位于指定的Rect(launchBounds),InputDispatcher据此判断命中。TaskView 的实现证明通过减除父视图区域来实现点击透传。
另外需要处理焦点切换:当用户在嵌入窗口内交互时,可能需要将输入焦点从宿主应用切换到嵌入的应用Activity。TaskView内部已考虑这种情况,Shell 会处理嵌入任务的焦点。当嵌入窗口需要获取焦点时,WindowManager会通知相应Task获得焦点。开发中需确保嵌入Activity允许嵌入并可接收焦点(如 manifest 设置可嵌入,Activity resizeable 等)。Android Automotive 13/QPR 版甚至提供了向 TaskView 内部Activity注入输入事件的接口,可用于特殊情况下主动传递输入。
3.3 输入区域映射
若自研方案不使用高层TaskView,可直接在 InputDispatcher 配置输入区域。例如通过 InputMonitor 创建自定义输入区域监听,或者在WindowManager中为自定义Layer注册一个WindowToken,使其参与输入体系。另一种方案是仿照 ActivityView 的做法,通过 InputManager 的 createInputForwarder(displayId) 获取一个输入转发器,将宿主收到的事件直接 inject 到目标Display/窗口。不过在Android 14建议使用官方TaskView机制,其已经妥善处理了事件区域和焦点问题。
关键是在宿主与嵌入层之间建立正确的输入区域映射。推荐方式是使用 Android 已有的 Insets 机制(InternalInsetsInfo)或 WindowManager 提供的多窗口输入支持,使Touch能够落到第三方窗口上。通过调整触摸区域和使用 SurfaceControl 子层,用户输入会正常地由系统派发给第三方应用,实现与普通窗口无异的交互体验。
4. 自研 RenderEngine 的渲染调度与合成
我们的自定义渲染引擎使用 C++ 和 OpenGL,实现独立于 SurfaceFlinger 的绘制。为了将其绘制结果合成到屏幕,我们需要将 RenderEngine 输出的内容提供给 SurfaceFlinger。典型做法是让 RenderEngine 渲染到一个 Surface(即图形缓冲队列 BufferQueue)的Buffer中,然后由 SurfaceFlinger 捕获此Buffer并进行合成。
实现路径:
- EGL环境绑定 Surface:使用 ANativeWindow/SurfaceHolder 获得自定义 Layer 对应的 Surface(例如通过 SurfaceControl#allocateBuffers 或创建一个 ANativeWindow 引用)。然后在 RenderEngine 内部创建 EGLSurface 绑定到该 ANativeWindow。这允许 RenderEngine 直接通过 OpenGL 将内容绘制到Surface的 Buffer上。
- 渲染循环:RenderEngine 应按照显示刷新节奏产出新帧。可以利用Choreographer或DispSync获取VSYNC信号,确保每帧绘制在VSYNC间隙完成,防止撕裂。每当 RenderEngine 完成一帧渲染后,调用 eglSwapBuffers 提交给 SurfaceFlinger。SurfaceFlinger在下一个合成周期检测到我们的 Layer 有新buffer提交,即获取该 buffer 进行合成。
- 与SurfaceFlinger解耦:自研 RenderEngine 不直接嵌入SurfaceFlinger的渲染管线,而是作为独立内容提供者。SurfaceFlinger将其视作普通“应用”层:如果底层硬件支持,HWC可能直接处理该层的合成;否则SurfaceFlinger使用自己的GPU RenderEngine对各层(包括自定义Layer)进行二次合成。这种模式下,我们的渲染逻辑和SF完全分离,只通过缓冲交换衔接,保证模块化和稳定性。
- 性能优化:可以选择双缓冲或三缓冲策略,以平衡延迟和流畅度。Android 图形栈确保SurfaceFlinger只在显示刷新时批处理新Buffer;因此RenderEngine可以采用非阻塞方式提交绘制,并使用ASurfaceTransaction等接口获取呈现时机和帧统计信息以调优渲染节奏。
需要注意,当自定义 RenderEngine 绘制和第三方应用窗口合成在同一Layer下时,要合理规划Z次序和透明背景。例如,可将 RenderEngine 绘制内容作为容器Layer的底层背景,而应用Surface作为其子层叠加其上。通过SurfaceControl的setElevation或子母关系,我们可以控制哪个内容在上。如果需要让RenderEngine内容覆盖应用窗口(如绘制浮窗效果),也可以将其设置为更高Z顺序的子层。
总之,通过Surface接口,我们的C++ OpenGL引擎产出帧,SurfaceFlinger负责最终的合成显示。这样既利用了SurfaceFlinger的显示同步和硬件合成能力,又保证自研引擎逻辑独立、不改动系统SF源码,方便适配定制。
5. 多组件统一动画调度架构及车机多窗口适配
在车载多窗口场景下,往往需要同时管理多个嵌入的应用界面和自定义UI组件,并实现整体的统一动画和调度。为此,我们需要设计一套架构,使多个 Layer 和组件协同工作:
- 容器分层与分组:可采用容器 Layer 对不同组件分组管理。例如,为每个独立模块(导航、仪表、媒体等)创建各自容器SurfaceControl,将相关的应用Surface和UI元素作为其子层。再通过更高层级的父Container将这些模块容器组合,形成层次结构。这样可以对整个容器应用统一的变换和动画,使内部多个子窗口同步移动、缩放。例如,在切换模式或布局时,只需对容器Layer做动画,内部嵌入的应用窗口和绘制内容都会跟随改变,实现视觉同步。
- 统一动画调度:引入一个中央动画调度器(Animator/Scene Manager),基于系统VSYNC或时间线来驱动所有组件动画。Android 14 的 WindowManager Shell 引入了 Shell Transition 框架,将系统中的过渡动画集中处理。我们可以借鉴这一思想,在C++层维护一个调度器,掌控所有 SurfaceControl 的 Transaction提交。通过在同一帧内构建针对多层的 SurfaceControl.Transaction 并利用 Transaction.merge 或同步事务提交,保证多个 Layer 的属性更新同时生效。此外,可以标记事务为动画(内部为同步应用),让SurfaceFlinger意识到这些事务属于一组动画,在帧末尾一起提交,提高同步性。
- Frame Timeline同步:Android 13+ 引入 FrameTimeline 以在跨进程动画中同步时序。我们可利用 SurfaceControl.Transaction#setFrameTimelineVsync 将各层动画绑定到同一个 VSYNC 时间,从而在不同Renderer/应用之间对齐动画节奏。这样,车机场景下多个窗口的动画(如同时淡入多个界面)能步调一致,避免卡顿不同步。
- 输入与焦点管理:多窗口场景下需制定焦点策略。例如,引入焦点管理模块,根据用户操作或情景在各嵌入Task之间切换焦点。可以监听InputDispatcher的焦点变更回调,或使用WindowManager的分区焦点策略(Automotive 有专门的FocusManager)。确保在动画过程中焦点适时转移(比如窗口缩放退出时,焦点跳回主窗口)。
- 车机特殊考虑:汽车中可能有多个物理显示(中控屏、仪表屏)。方案应考虑多显示适配,比如使用多个自定义 Layer 分别附加到不同 Display 上,或者使用VirtualDisplay技术在后台渲染再投射到目标Layer上。如果需要在两个屏幕间同步动画,可通过共享同一时钟源和同步事务来实现。
综合来看,架构建议是将各组件解耦为独立的渲染单元(TaskView容器 + RenderEngine输出),并通过 SurfaceFlinger 的Layer体系将它们组装。在调度层面,引入统一的事务控制机制,在单一时间轴上更新所有相关 Layer 的属性,实现动画统一(类似于舞台总监统一指挥多个演员)。这样设计可保证多窗口界面在复杂场景下仍保持同步的动画和一致的输入体验。
参考实现:Android Shell的 TaskView 与 Transition 框架已经在多窗口(如分屏、画中画)中实现了类似思想。例如,Android 12+ 用 TaskView 取代旧分屏,所有应用窗口通过 ShellTaskOrganizer 统一管理。我们方案中的中枢调度器相当于 Shell,负责调用 SurfaceFlinger事务、协调RenderEngine绘制和任务窗口的显示/隐藏,从而达到车机多组件统一渲染和动画的效果。
最后,整个方案充分利用了 Android 14 图形架构的新特性(SurfaceControl、TaskView、多窗口Transitions 等),在 C++ 层面实现可落地的解决方案:自定义 Layer 作为载体,第三方应用Surface嵌入其中,输入事件正确转发,自研OpenGL引擎绘制丰富UI,并通过精心设计的层级和动画调度实现多个组件的统一渲染与联动。这套方案适用于车载等多窗口复杂场景,能够提供顺畅的统一动画效果和可靠的输入响应。各模块相对独立又通过SurfaceFlinger胶合,既保证了系统稳定又方便后续拓展。
参考资料
- Android Graphics Architecture – SurfaceFlinger & WindowManager
- AOSP TaskView 实现(SurfaceControl 重定向嵌入任务)
- AOSP TaskView 输入区域调整实现
- ActivityView (早期方案) 虚拟显示与输入转发
- Android Automotive 多窗口/TaskView 框架分析
本文持续更新,最后更新时间:2025年7月18日