Android 应用间 UI 刷新与合成

197 阅读12分钟

一块屏幕上可以同时出现应用主界面、状态栏、输入法、相机预览和视频画面。这些画面来自不同内容流,各自通过生产入口和 Buffer 周转通道进入 SurfaceFlinger 的合成场景。

以下以 Android 15 为参考版本,建立从内容生产、Buffer 交接、Layer 状态到最终合成的系统模型,并说明常见窗口路径中的 BLAST 适配层。

系统不会在队列层把所有内容合并成一条“大队列”。每条内容流先独立完成 Buffer 的生产和交接,BLAST(若处在该窗口路径)再把一次提交放进 SurfaceControl Transaction 的语境,SurfaceFlinger 到刷新时才从多个 Layer 的可用状态中选择合成输入。因此,queueBuffer() 成功只说明一次提交被接受,不能直接推出 acquire、latch、合成或扫描已经完成。

一块屏幕不是一条队列

设想一个视频应用正在播放全屏视频,顶部仍显示系统状态栏,底部弹出输入法,应用又叠加了一层播放控件。当前显示结果至少涉及以下内容来源:

内容典型生产者典型 Buffer 流合成单元
应用主 UI 与播放控件HWUI / RenderThread / GPU应用主 Surface 对应的周转通道应用主 Layer
视频画面MediaCodec输出 Surface 对应的周转通道SurfaceView 等独立 Layer,或被应用采样进主 UI
状态栏SystemUISystemUI 自己的 Surface/BufferQueue状态栏 Layer
输入法IME 进程输入法窗口自己的 Surface/BufferQueue输入法 Layer

这些内容的生产速度、尺寸、格式和生命周期都不同。把它们塞进同一个 Producer/Consumer 契约,会让一条流的背压、重建和缓存变化干扰其他流。Android 更常见的组织方式是让它们分别周转,在合成阶段再统一决定“屏幕上出现什么”。

flowchart LR
    UI[App UI / HWUI] --> S1[Surface A]
    V[MediaCodec] --> S2[Surface B]
    SYS[SystemUI] --> S3[Surface C]
    IME[输入法] --> S4[Surface D]

    S1 --> Q1[BufferQueue / BLAST A]
    S2 --> Q2[BufferQueue B]
    S3 --> Q3[BufferQueue / BLAST C]
    S4 --> Q4[BufferQueue / BLAST D]

    Q1 --> L1[App Layer]
    Q2 --> L2[Video Layer]
    Q3 --> L3[Status Bar Layer]
    Q4 --> L4[IME Layer]

    L1 --> SF[SurfaceFlinger Layer 场景]
    L2 --> SF
    L3 --> SF
    L4 --> SF
    SF --> COMP[HWC / RenderEngine]
    COMP --> DISP[Display]

四条 Buffer 流分别周转;SurfaceFlinger 汇合的是各个 Layer 当前可用的 Buffer 和属性。

参与者分别负责什么

这条链路里最容易混淆的是 Window、Surface、BufferQueue 和 Layer。它们处在不同问题域:

对象或角色回答的问题不负责什么
Window窗口如何被应用与 WindowManager 管理不直接定义 Buffer 池内部状态
Surface / ANativeWindowProducer 从哪里请求可写 Buffer、向哪里提交不决定 Z 顺序和最终合成策略
BufferQueueBuffer 如何分配、复用、排队和交接不决定窗口位置、透明度和遮挡
SurfaceControl / TransactionLayer 的 Buffer 与几何、裁剪、层级等状态如何提交不替代 Producer 对 Buffer 的写入
LayerSurfaceFlinger 场景中的合成内容单元不等于一个 Java Window,也不保证总有 Buffer
SurfaceFlinger收集 Layer 状态、组织刷新与合成不替代应用、相机或解码器生产内容
HWC / RenderEngine按当前能力完成设备合成或 GPU 客户端合成不改变上游 BufferQueue 的容量策略

Surface 是相对于某条内容流的 Producer 入口。Java 层开发者可能通过 Canvas、MediaCodec 或 Camera 间接使用它;native 层通常表现为 Surface / ANativeWindow,内部连接 IGraphicBufferProducer。真正写像素的执行者可以是 CPU、HWUI/RenderThread、EGL/Vulkan、Camera HAL 或 Codec。

Consumer 也不是固定等于 SurfaceFlinger。ImageReader、SurfaceTexture、Codec、BLASTBufferQueue 都可以处在 Consumer 一侧。某个组件还可能一边消费上游 Buffer,一边把处理结果通过另一条 Surface 流继续生产。因此“Producer/Consumer”是相对于一条 BufferQueue 的角色,不是进程或类的永久身份。

多条 BufferQueue 的来源

每调用一次 BufferQueue::createBufferQueue(),都会得到一组新的 Core、Producer 端实现和 Consumer 端实现。系统没有“所有应用共用 N 条 BufferQueue”的固定总数;数量由当前活跃的数据流产生,并随对象生命周期变化。

可能创建独立 Buffer 周转通道的场景包括:

  • 应用顶层窗口的主绘制 Surface;
  • SurfaceView 的独立显示 Surface;
  • SurfaceTexture、TextureView 使用的输入流;
  • Camera、MediaCodec、ImageReader、ImageWriter 的输入或输出;
  • 录屏、虚拟显示和屏幕采集;
  • SystemUI、壁纸、启动画面、输入法等系统界面;
  • Android 15 窗口路径中的 BLASTBufferQueue。

单条 BufferQueue 的直接连接语义仍是一名逻辑 Producer 对一名逻辑 Consumer。系统的“多生产者、多消费者”效果主要通过多条队列、转发组件和合成图组织,不是让多个独立窗口同时连接一个 Core。

Window、Layer 与 BufferQueue 不是严格一一对应

“一个普通顶层窗口通常有一条主绘制流”可以作为入门模型,但不能写成全局公式。

一个 Activity Window 可能同时包含:

Activity Window
├── 主 UI Surface / Buffer 流
├── SurfaceView 视频 Surface / Buffer 流
├── Camera → ImageReader 的处理流
└── SurfaceTexture 接收的纹理流

其中主 UI 和 SurfaceView 可能分别形成可见 Layer;ImageReader 的流可能只在应用内部处理;TextureView 的 SurfaceTexture 有自己的 BufferQueue,但内容通常再被采样进应用主 UI Layer。反过来,SurfaceFlinger 中用于容器、颜色或效果的 Layer 也可能没有持续提交像素的 Producer。

因此:

有 BufferQueue
≠ 必然有独立 Window
≠ 必然有独立可见 Layer

有 Layer
≠ 必然有自己的 BufferQueue

窗口重建、Surface 销毁重建、SurfaceView attach/detach、Camera 会话或 Codec 配置变化,还会让这些关系随时间改变。

一帧如何从应用进入 SurfaceFlinger

经典模型常写成“应用向 BufferQueue queue,SurfaceFlinger直接消费”。它适合说明生产—消费关系,但 Android 15 的常见窗口路径还可能经过客户端进程中的 BLASTBufferQueue。

BLASTBufferQueue.cpp 展示了这层适配:BLAST 内部仍创建普通 BufferQueue;本地 Consumer acquire 到 BufferItem 后,再用 SurfaceControl::Transaction::setBuffer() 把 Buffer、Fence、frame number 等状态送入 SurfaceFlinger。

sequenceDiagram
    participant App as App / HWUI
    participant Surface as Surface
    participant BQ as BLAST 内部 BufferQueue
    participant BLAST as BLAST Consumer
    participant Tx as SurfaceControl Transaction
    participant SF as SurfaceFlinger
    participant Comp as HWC / RenderEngine

    App->>Surface: 请求可写 Buffer
    Surface->>BQ: dequeueBuffer()
    BQ-->>Surface: slot + GraphicBuffer + release Fence
    App->>App: CPU/GPU 写入
    Surface->>BQ: queueBuffer(acquire Fence, frame data)
    BQ-->>BLAST: onFrameAvailable
    BLAST->>BQ: acquireBuffer()
    BQ-->>BLAST: BufferItem + GraphicBuffer + Fence
    BLAST->>Tx: setBuffer() + 帧属性
    Tx->>SF: 提交 Transaction
    SF->>SF: 应用 Layer 状态并选择可用 Buffer
    SF->>Comp: 组织本次刷新合成输入
    Comp-->>SF: present / completion fences

图中只保留从提交到合成的主路径。

Surface 先提供可写目标

应用层看到的是 Surface。HWUI、EGL、Vulkan、CPU Canvas、Camera 或 MediaCodec 通过它取得可写 Buffer。Buffer 的规格由当次请求和 Consumer 约束共同决定,稳定规格下可复用已经分配的图形资源。

这一段只负责生成像素。WindowManager 不写像素,SurfaceFlinger 也不替 Camera 或解码器生产帧。

queueBuffer 只完成一次提交

Producer 写入后调用 queueBuffer(),提交 Slot、裁剪、变换、时间戳、damage 和 acquire Fence 等帧级信息。成功返回能确认 BufferQueue 接受了本次提交,却不能推出:

  • GPU 已经写完全部像素;
  • Consumer 已经 acquire;
  • BLAST 已经把它放入 Transaction;
  • SurfaceFlinger 已经选中这帧;
  • 这一 Layer 当前可见;
  • HWC 或 RenderEngine 已经完成合成;
  • 显示设备已经扫描到这帧。

GPU 可以在 queueBuffer() 返回后继续执行,Consumer 通过 acquire Fence 等待真实写入完成。队列状态和硬件完成点是两条相关但不同的时间线。

BLAST 把 Buffer 与 Layer 状态放到同一事务语境

窗口 resize、旋转或转场时,一帧新 Buffer 往往需要配合新的 crop、transform、position 或其他 Layer 属性。若像素提交和几何状态在不同时间生效,可能短暂出现旧帧拉伸、尺寸不匹配或黑边。

BLAST 的职责不是替代 BufferQueue,而是把本地队列产出的 BufferItem 适配为 SurfaceControl Transaction 中的 buffer state。这样,本帧 Buffer、acquire Fence、frame number 和相关帧属性可以更容易与窗口事务协调。

两者职责不同:

BufferQueue:管理 Buffer 池、Slot 状态、提交队列和 Producer/Consumer 交接
BLAST:      acquire 一次提交,再把 Buffer 与帧属性装入 SurfaceControl Transaction

SurfaceFlinger 面对的是 Layer 场景

SurfaceFlinger 收到 Transaction 后,把 Buffer 状态与 Layer 的位置、裁剪、透明度、Z 顺序、可见区域等属性放进同一个合成场景。它会在刷新调度中选择适合当前时点的 Layer 状态和 Buffer,而不是每收到一次 queueBuffer() 就立即刷新一次屏幕。

一帧能否进入本次合成,至少受以下条件影响:

条件未满足时的结果
Buffer 已到达 SurfaceFlinger 可使用的状态仍停留在上游队列或事务处理中
acquire Fence 已满足,或能作为下游依赖继续传递不能安全读取 Producer 尚未完成的内容
Transaction 已在合适的时点应用Buffer 与几何状态可能尚未生效
Layer 可见且处于显示区域即使有新 Buffer 也可能不参与合成
刷新调度选择了该状态可能等待下一次刷新
合成和 present 完成屏幕扫描输出仍在更后面

这也是排查“queue 成功但没上屏”时需要逐段定位的原因:先确认生产提交,再确认 Consumer/BLAST,接着看 Transaction 和 Layer 可见性,最后看合成与显示完成点。

一个“提交成功但画面没变”的最小推演

把“视频正在播放、输入法刚弹出”当作一个观察场景。视频和输入法各自提交了新内容,但这一刷新周期里屏幕仍可能只显示旧画面。沿同一条证据链看,问题不在“有没有一个全局队列”,而在新状态停在哪一段:

观察点发生的状态变化这一步能说明什么还不能说明什么
视频 Producer 调用 queueBuffer() 返回成功视频流的一个 Slot 进入该队列的提交路径Producer 已交出一次帧级提交Consumer 尚未 acquire,GPU 也可能仍在执行
BLAST Consumer 收到并 acquire BufferItem本地队列的提交被取出,带上 Buffer、Fence 和帧属性这条流已经越过队列边界Transaction 尚未必已在 SurfaceFlinger 生效
SurfaceFlinger 应用 Transaction对应 Layer 的 Buffer/几何状态进入场景新状态具备参与刷新选择的条件Layer 仍可能不可见或未被本次刷新选中
HWC/RenderEngine 完成本次合成多个 Layer 被组织为本次显示输入这次合成已经完成不能把它倒推为所有上游流都同步完成

遇到“提交成功但没有新画面”,按 Producer、队列、BLAST/Transaction、Layer 场景和合成输出逐段定位。以上是 Android 15 硬件加速窗口的常见路径;SurfaceView、TextureView、无 Buffer 的 Layer、软件后端和异常调度需要另行核对。

多路内容在哪里汇合

多条 BufferQueue 的队列、Slot 和背压彼此独立。SurfaceFlinger取得的是各个 Layer 在当前刷新时可用的 Buffer 与状态,再把它们组织成一组合成输入。

flowchart TB
    subgraph Streams[独立内容流]
        A[App Layer<br/>Buffer A + 属性 A]
        B[Video Layer<br/>Buffer B + 属性 B]
        C[Status Bar Layer<br/>Buffer C + 属性 C]
    end

    A --> Scene[SurfaceFlinger<br/>可见 Layer 场景]
    B --> Scene
    C --> Scene

    Scene --> Decide{设备能力与当前 Layer 状态}
    Decide -->|适合设备合成| HWC[HWC / Display Composition]
    Decide -->|需要客户端合成| RE[RenderEngine / GPU]
    RE --> Client[Client Target]
    Client --> HWC
    HWC --> Display[Display]

HWC 可以让显示硬件直接合成满足条件的 Layer。需要 GPU 客户端合成时,RenderEngine 把相关 Layer 画进 Client Target,再交给 HWC 与其他设备合成 Layer 一起 present。具体选择受设备能力、格式、变换、效果和当帧状态影响,不能从“有几个 BufferQueue”直接推出。

合成阶段可能读取多个输入 Buffer,也可能写入新的合成目标 Buffer。BufferQueue 交接不把整幅像素放进 Binder Parcel,并不代表整条图形管线都不产生额外读写。

Camera、Codec 和应用 UI 如何放回同一张图

相机预览和普通控件通常有两条组织路径:

SurfaceView 路径

Camera 把应用提供的 Surface 当成 Producer 入口,向独立 Buffer 流提交预览帧。SurfaceView 对应的 Layer 与应用主 UI Layer 分开,SurfaceFlinger 在合成阶段组合两者。

Camera/HAL → 预览 Surface → 独立 Buffer 流 → Preview Layer ─┐
App/HWUI   → 主 Surface   → 主 UI Buffer 流 → App Layer ─────┼→ SurfaceFlinger
SystemUI   → 自己的 Surface/流             → System Layer ──┘

Camera 的执行上下文不会变成应用主 UI 的 Producer;应用只是把一条可写 Surface 交给它。

TextureView 路径

Camera 或 Codec 把内容写入 SurfaceTexture 的 BufferQueue。应用渲染时再把该纹理采样进主 UI Buffer,因此可能存在上游媒体流与下游窗口主流两条 BufferQueue,但 SurfaceFlinger 最终只看到合成后的应用主 Layer。

Camera/Codec → SurfaceTexture BufferQueue
                      ↓ 应用采样纹理
              App 主 UI 绘制
                      ↓
              主 Surface / Buffer 流
                      ↓
                 App Layer

两种路径都能显示相机或视频,但 Layer 数量、Buffer 流位置和合成边界不同。只数 Window 或只数可见 Layer,都不能准确推导系统中 BufferQueue 的总数。

SurfaceFlinger 的部署边界

标准 Android 系统实例中,SurfaceFlinger 是由 init 启动的独立原生系统服务。固定版本 surfaceflinger.rc 定义主服务,main_surfaceflinger.cpp 创建实例、注册服务并进入运行循环。

通常是一套 Android userspace/Binder 服务域中有一个当前有效的主 SurfaceFlinger 服务,而不是每个 App、Window、Layer 或物理显示各起一个 SurfaceFlinger 进程。一个 SurfaceFlinger 可以管理大量 Layer 和多个显示设备;它内部也不只有一个线程。

因此下面两句话同时成立:

系统中通常只有一个主 SurfaceFlinger 服务角色
系统中同时可以存在很多 BufferQueue 和 Layer

前者描述中心合成服务的部署,后者描述独立内容流和场景节点的数量。

按阶段定位“queue 成功但未上屏”

把一帧从应用跟到屏幕时,可以依次问:

内容由谁生产
→ 写入哪条 Surface / Buffer 流
→ 谁消费这条队列,是否经过 BLAST
→ Buffer 如何进入哪个 Layer
→ 对应 Transaction 是否生效
→ Layer 是否可见、是否被选入本次刷新
→ HWC / RenderEngine 如何完成合成
→ present 和显示完成点是否到达

排查时,将现象对应到一个阶段:没有提交,查 Producer;有提交但没有 acquire,查队列和 Fence;已经进入 Transaction 但没有画面,查 Layer 可见性与刷新选择;已经进入合成仍未显示,查 HWC/RenderEngine 与 present。队列数量本身不能推出帧率、延迟或设备性能。

参考资料