☰
Surface 与 SurfaceControl:Android 显示链路中的生产与调度
2026/10/14 7:50:59 网站建设 项目流程

做图形框架源码分析这些年,我见过太多人栽在同一个坎上:看到代码里既有Surface又有SurfaceControl,以为它们是上下层调用关系,结果在定位问题上绕了一大圈。尤其是当你第一次接手窗口层级相关 bug 时,拿到WindowManagerService里的一堆句柄,Surface、SurfaceControl、SurfaceSession、SurfaceControl.Transaction全混在一起,光名字就能劝退一半人。这篇文章是“图像显示框架”系列的第九篇,我打算基于 Android 15 源码,把这两个最基础、也最容易被混淆的概念彻底拆开讲清楚。读完你会明白它们各自到底是什么、在显示链路里站在什么位置、代码上是怎么互通的,以及实际排查问题时怎么利用这两个对象快速定位方向。适合那些想深入理解 Android 图形显示栈、准备啃 SurfaceFlinger 源码,或者正在做系统窗口、投屏、多屏相关开发的读者。

1. 从显示链路图看这两个类的真实定位

1.1 一张链路图,看清 Surface 和 SurfaceControl 的上下游

先别急着看源码,把 Android 显示流程想象成一条完整的生产线。应用的ViewRootImpl通过performTraversals测量布局绘制之后,最终会把绘制指令交给RenderThread,由 GPU 在一张 buffer 上完成渲染。这一步产生的 buffer 不是直接推到屏幕上的,而是先送进一个缓冲队列,等系统侧的 SurfaceFlinger 来消费。SurfaceFlinger 拿到所有应用层送上来的 buffer 后,按 Z 轴顺序做合成(composite),最后才把合成结果交给显示控制器。

在这个流程里,Surface和SurfaceControl各自对应的位置完全不同:

  • Surface站在“生产端”。它本质上是一个连接到BufferQueue的入口,App 通过它取得 buffer、写入绘制内容、再提交回队列。可以说,Surface是 App 的绘制靶子,它解决的是“我画在哪里”的问题。
  • SurfaceControl站在“调度端”。它对应着 SurfaceFlinger 内部的一个 Layer 对象,是 App 或系统服务对某个图层的控制句柄。你可以通过它调整图层的大小、位置、透明度、可见性、Z 序,相当于给 SurfaceFlinger 发指令的遥控器。

所以两者根本不是同层概念,更不是“包含”与“被包含”的关系。一个SurfaceControl可以在后续某个时刻通过Surface(SurfaceControl)或SurfaceControl.createSurface关联出一个Surface,也就是说先有图层控制权,再有生产入口。理解这一层,后续读 AOSP 任何窗口管理代码都会顺很多。

1.2 为什么需要“生产与消费”分离

Android 从设计之初就把“绘制”和“合成”分开,原因是绘制发生在应用进程,而合成发生在独立的 SurfaceFlinger 系统进程。跨进程传递图像数据,如果每次都要把整块显存数据拷贝一遍,性能和内存都扛不住。于是 Android 引入了BufferQueue作为中转站,生产者(App)和消费者(SurfaceFlinger)各自持有队列的一端,buffer 本身通过共享内存分配,不跨进程拷贝。

这种生产者-消费者模型天然需要两个口子。App 要能拿到 buffer 去绘制,所以有了Surface这一层封装;而 SurfaceFlinger 要管理这些 Layer 的合成顺序和显示属性,必须要有一个进程间通信的引用,这就是SurfaceControl存在的意义。换句话说,Surface 解决“buffer 怎么流转”,SurfaceControl 解决“图层怎么排布、怎么显示”。这两件事在系统层面由 BufferQueue 和 Layer 分别承接,在应用层则暴露成两个不同的类。

打个比方可能更好理解。Surface像餐厅后厨的出餐台,厨师把菜做好放在出餐台;SurfaceControl像餐厅中央的排桌系统,经理决定哪桌先上菜、哪桌位置调换、哪桌暂时不对外接待。听起来都跟“上菜”有关,但一个管“生产交付”,一个管“调度显示”。

2. Surface:App 端的绘制入口与它的源码真相

2.1 Java 层 Surface:一个包装壳,藏着 native 句柄

先看Surface.java,路径在frameworks/base/core/java/android/view/Surface.java。这个 Java 类本身很薄,它的大多数逻辑都通过一个long类型的mNativeObject字段交给 native 层实现。也就是说,Java 层的Surface基本是个 native 对象的持有者,所有真实动作都发生在 C++ 世界里。

Java 层几个核心方法,源码注释和签名已经说明一切:

  • lockCanvas(Rect dirty):锁定一块 buffer,返回一个Canvas。dirty参数表示只更新脏区域,不进这个 rect 的内容被认为不需要重画。这个方法的 native 实现最终调用Surface::lockCanvas,内部会走dequeueBuffer取一块可写的 buffer。
  • unlockCanvasAndPost(Canvas canvas):把绘制好的 buffer 提交回队列。native 层调用queueBuffer,同时会带一个fence表示 GPU 绘制完成的时间点。
  • release():释放 native 资源并置空mNativeObject。如果忘记释放,会导致 buffer 句柄泄漏。

还有一个很容易被忽视的构造方式:Surface(SurfaceControl sc)。从SurfaceControl构造Surface时,Java 层会调用 native 的nativeCreateFromSurfaceControl,拿到同一个图层对应的生产端接口。这个关系非常关键,后面讲 SurfaceControl 时会再次遇到。

2.2 Native 层 Surface 与 BufferQueue 的“槽位”协作

真正干活的是frameworks/native/libs/gui/Surface.cpp。这个类实现了IGraphicBufferProducer的使用端封装,内部持有一个sp<IGraphicBufferProducer>,这个接口就是BufferQueue的生产者端。

理解Surface.cpp之前,必须先理解 BufferQueue 里“槽位(slot)”的概念。队列里维护一组BufferSlot,每个 slot 记录一块 buffer 的句柄、状态、fence 等。App 需要 buffer 时不是直接向系统要一块新内存,而是通过dequeueBuffer获取一个可用的 slot 索引,然后拿到对应的GraphicBuffer。绘制完成后queueBuffer把 slot 标记为QUEUED。如果整个过程只是为了测试或者清理,则用cancelBuffer把 slot 退回空闲状态。

这样做最大的好处是避免频繁分配和释放图形缓冲区。三缓冲场景下,同一批 buffer 会在 App 和 SurfaceFlinger 之间循环流转,每一轮dequeue和queue的操作成本都远低于重新分配内存。这也解释了为什么 App 的帧率上不去时,调高 buffer 数量并不能解决所有问题——你还要看消费者端 SurfaceFlinger 释放得够不够快。

Surface.cpp里还有个重要的成员mFrameTimestamps和mDataSpace,前者用于帧时间记录,后者用于色彩空间管理。Android 上图像显示不止是“画出来”,还涉及颜色如何还原,HDR、广色域这些能力都依赖于 buffer 携带的色彩数据描述。Android 15 源码里这类 metadata 的处理路径已经比早期版本完善得多。

2.3 lockCanvas 与 EGLSurface,两条路的异同

应用层画内容通常会碰两条路:

  • 老派的Surface.lockCanvas()+Canvas绘制。这条路走的是 CPU 软件绘制,canvas底层操作的是Skia的SkCanvas,把绘制指令变成像素填充到 buffer 中。早期系统 UI 和 2D 游戏常用这种方案,但现在多数 App 只把它用于获取Bitmap或截屏之类的辅助场景。
  • 硬件加速的EGLSurface。通过EglSurface关联Surface后,OpenGL ES / Vulkan 的绘制指令由 GPU 执行,最终输出到同一个 buffer 队列。应用侧要做的核心动作还是queueBuffer,只是绘制发起方从 CPU 变成了 GPU。

这两条路的最终落点都在Surface的 native 层。理解这一点对排查问题很有帮助:不管 App 是走 Skia 还是 GPU 渲染,只要它最终要上屏,数据一定是通过Surface进入BufferQueue。你看到“Surface 没画面”时,先区分 CPU 绘制和 GPU 绘制在哪个环节断了,再决定查 Canvas、EGL 还是 buffer 分配。

3. SurfaceControl:SF 图层 Layer 的“遥控器”

3.1 Java 层 SurfaceControl 与 System Server 中的创建路径

SurfaceControl.java位于frameworks/base/core/java/android/view/SurfaceControl.java,它的创建大多是靠SurfaceControl.Builder完成的。Builder 提供了一堆可配置项,比如名称、buffer 尺寸、格式、颜色空间、父图层等。但这些配置最终不会直接生效,它们只是用来告诉 SurfaceFlinger“我要创建一个什么样的 Layer”。

真正创建动作发生得较晚,通常在WindowManagerService里。WindowManagerService.addWindow流程中会通过WindowState调SurfaceControl.Builder创建窗口对应的 SurfaceControl,这个调用会跨 Binder 到 SurfaceFlinger,在 SF 侧创建一个Layer。创建完成后,应用进程拿到的只是一个轻量级句柄,真正的图层实体在 System Server 中由SurfaceFlinger管理。

看 WMS 源码时会发现每个WindowState持有自己的SurfaceControl,同时还有一个Surface对象。这两个对象的关系就是:通过SurfaceControl.Handle获得生产端接口,再构建Surface。窗口绘制的第一帧内容,就是经这个Surface进入 BufferQueue,流向 SF 中对应 Layer 的消费端。

3.2 Transaction 机制:一次 Binder 传递一组状态

SurfaceControl最核心的操作方式是SurfaceControl.Transaction。这个类你会在 SystemUI、Launcher、窗口动画等大量模块中见到。它的作用是批量设置多个图层属性,最后通过apply()一次性提交。

典型用法:

SurfaceControl.Transaction t = new SurfaceControl.Transaction(); t.setLayer(surfaceControl, 10); t.setPosition(surfaceControl, left, top); t.setSize(surfaceControl, width, height); t.setAlpha(surfaceControl, 1.0f); t.setVisibility(surfaceControl, true); t.apply();

apply()最终会通过 Binder 调用SurfaceFlinger.setTransactionState(),把这一组事务打包发过去。SF 收到后在合成线程统一解析和应用。把多次操作合并成一个 transaction,既是为了减少 Binder 调用次数,也是为了保证一组图层属性在 SF 侧的修改是原子的,避免出现“尺寸改了但位置还没改”的中间状态。

很多刚开始接触 SurfaceControl 的开发者容易忽略一个点:Transaction是有生命周期的,它不会自动重复使用,也不应该被长期持有不释放。每次构建新的 Transaction、提交、释放,是更安全的写法。如果反复使用同一个 Transaction 对象,要确保在每次apply()后重新设置完整状态,否则会出现状态残留。

3.3 SF 侧如何消费 Transaction 并影响合成

在 SurfaceFlinger 进程里,Transaction最终被解析成对具体Layer状态的修改。Layer 是一个树状结构,每个 Layer 可以有子 Layer,Z 序通过层级关系决定。SurfaceControl 的setLayer传入的是同层兄弟之间的相对顺序,而setParent决定它在图层树上的挂载位置。

Layer.cpp中直接与本次内容相关的属性包括:

  • mTransformationMatrix/mPosition:控制图层的位置和几何变换。
  • mAlpha:图层透明度。
  • mVisibleRegion:由 SF 根据遮挡关系计算出的可见区域。
  • mFrameRate:Android 10 之后引入的帧率控制字段,App 可以通过Surface.setFrameRate或Transaction.setFrameRate指示系统如何调度刷新率。

SF 合成时会对每个可见 Layer 调用 draw 或合成器指令。被Transaction.setVisibility(false)隐藏的 Layer 可直接跳过合成,这是性能优化的关键路径。相反,如果把透明度设成 0 而不是隐藏,合成器仍然会为它做无用功,只是结果看不见而已。后面我会专门讲这个坑。

4. Android 15 源码里的几个值得留意的点

4.1 版本差异与源码定位

分析源码前最重要的一件事是固定版本。Android 10 和 Android 15 之间,图形栈的差异非常明显,比如 BufferQueue 的分配器实现、Layer 类型、Transaction 的处理方式都有改动。我建议直接以android-15.0.0_r1这类 tag 为准,把以下目录单独拎出来:

  • frameworks/base/core/java/android/view/Surface.java
  • frameworks/base/core/java/android/view/SurfaceControl.java
  • frameworks/native/libs/gui/Surface.cpp
  • frameworks/native/libs/gui/BufferQueueProducer.cpp
  • frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
  • frameworks/native/services/surfaceflinger/Layer.cpp

如果你用的是公司定制系统,那还要结合自己的 vendor 分支,很多厂商会在SurfaceControl的 native 实现上加私有扩展。阅读时优先看libs/gui和services/surfaceflinger,Java 层只是皮,细节全在 native。

4.2 Android 15 对 BufferQueue / 图层参数的处理细节

Android 15 里,BufferQueue 的 slot 机制依然是核心,但比早期版本更强调 buffer 的“缓存复用”和“状态转移”。BufferItem中携带的时间戳、fence、数据空间等信息,在 SF 的合成决策中占据更重要的位置。比如自适应刷新率场景下,SF 需要根据 buffer 到达时间判断是维持 90Hz 还是降到 60Hz,这些判断依赖的正是 queueBuffer 时附带的时间戳与 fence 信息。

图层参数方面,Android 15 对Transaction.setFrameRate、setShadowRadius等接口的支持更稳定。如果你在系统侧做高刷新率适配,建议优先用 Transaction 里的setFrameRateSelectionOptions相关接口,尽量少直接操作底层的帧率控制逻辑。系统 API 的屏蔽性好,出问题时排查范围也小。

另外,Android 15 在窗口合成上更强调 DisplayArea 概念。DisplayArea是一棵节点树,窗口的 SurfaceControl 会被挂载到不同的 Area 下,比如状态栏、导航栏、应用区域各有自己的 Area。这种层级设计让跨窗口动画、后台窗口管理、多屏布局都变得更好控制,但代价是你在 dumpsys 输出里看到的不再是简单扁平的一排 Layer,而是一棵复杂的树。习惯用dumpsys SurfaceFlinger --list的老司机要花点时间适应。

4.3 帧时间线 FrameTimeline 与帧率控制

Android 10 开始引入 FrameTimeline,到 Android 15 它已经成了分析卡顿的核心工具。FrameTimeline 记录每一帧从 App 绘制开始到 SF 合成的完整时间线,包括 App 的AppFrame时间戳、SF 的SfFrame时间戳。定位卡顿问题时,dumpsys SurfaceFlinger --timeline能帮你分辨是 App 侧绘制慢了,还是 SF 合成慢了,又或者是 vsync 调度出了问题。

跟 SurfaceControl 直接相关的是帧率控制。App 可以在 Surface 上通过setFrameRate()明确要求某个刷新率,系统会据此调整屏幕刷新策略。做系统应用的读者注意,SF 侧会综合考虑所有可见 Layer 的帧率请求去决定最终刷新率,而不是只看一个窗口。所以当你发现屏幕刷新率不听话时,先查是不是有某个隐藏的 Layer 还在请求高刷新率。

5. 上手实操:从 SystemUI 到 App 层的利用方式

5.1 在 AOSP 里怎么快速定位相关源码

如果你还不习惯在 AOSP 源码里找东西,我建议从符号反查开始。比如想知道SurfaceControl.Transaction的setLayer是怎么传到 SF 的,就先在 Java 层搜nativeSetLayer,找到对应的 JNI 函数,然后看 C++ 层SurfaceControl.cpp的setLayer,最终跳到SurfaceFlinger的setTransactionState。这条链很清晰。

另一个高效路径是从 dumpsys 逆推。实机上执行dumpsys SurfaceFlinger --list,会打印出当前所有 Layer 的名称和 ID;挑一个可疑 Layer,再执行dumpsys SurfaceFlinger --layer <ID>,会看到它的详细参数,包括所属进程、buffer 尺寸、变换矩阵、透明度、帧率等。根据这些参数反搜代码,往往比硬啃源码更快。

5.2 一个可运行的最小例子

在系统应用或者有悬浮窗权限的应用里,你可以直接创建 SurfaceControl 并生成 Surface 来绘制,下面的代码演示了最小闭环:

// 创建 SurfaceControl(需要系统权限或悬浮窗权限) SurfaceControl sc = new SurfaceControl.Builder() .setName("demoLayer") .setBufferSize(720, 1280) .setFormat(PixelFormat.RGBA_8888) .build(); SurfaceControl.Transaction t = new SurfaceControl.Transaction(); t.setLayer(sc, Integer.MAX_VALUE); t.setPosition(sc, 100, 100); t.setVisibility(sc, true); t.apply(); Surface surface = new Surface(sc); Canvas canvas = surface.lockCanvas(null); canvas.drawColor(Color.RED); surface.unlockCanvasAndPost(canvas); // 不使用时释放 surface.release(); sc.release();

注意两个点:第一,setBufferSize要和后面 lockCanvas 时画布期待的分辨率一致,不然会出现内容被拉伸或裁剪;第二,Surface 使用完一定要release(),否则底层 buffer 不会回收到 BufferQueue,连续创建多次会直接 OOM。

实际应用场景里,这个方式多用于 SystemUI 的通知面板、Launcher 的窗口动画、以及 Android Auto 或多屏显示。普通应用极少直接创建 SurfaceControl,通常都是通过WindowManager间接操作。

5.3 dumpsys 与实机排查技巧

当我接手一个“窗口显示不出来”的 bug,常规排查顺序是:

  1. dumpsys window windows查看窗口状态,确认 WindowState 里是否有 SurfaceControl,Surface 是否为SHOWN状态。
  2. dumpsys SurfaceFlinger --list找到对应的 Layer,看是否存在。
  3. dumpsys SurfaceFlinger --layer <ID>查 Layer 的mCurrentState参数,重点看alpha、z、visibleRegion、buffer transform。
  4. 如果 Layer 存在但内容黑屏,多半是 buffer 没有正确送达。这时候查mBufferQueue的状态,看queueBuffer的次数有没有增长,fence 是否处于 pending 状态。

这套流程基本覆盖了 80% 的显示问题。顺带一提,dumpsys gfxinfo可以看 App 的绘制帧时间,但它主要面向应用层,查不到 SF 的合成细节。真正的合成耗时还是要在dumpsys SurfaceFlinger --latency里看。

6. 常见问题与避坑速查

问题现象可能原因排查方向
创建了 SurfaceControl 但页面不显示Transaction 没 apply,或没设置可见性检查 apply() 是否调用、setVisibility(true) 是否执行
Surface 能 lockCanvas,但画面是黑的buffer 尺寸不匹配,或 GPU fence 异常核对 setBufferSize 与 Canvas 区域;查 dumpsys SurfaceFlinger 的 fence 状态
图层遮挡关系错乱setLayer 值范围不对,或父子挂载错误确认 Layer 树结构,Z 序只在同级比较
画面撕裂但有内容 Auto缓冲队列 buffer 数量或 vsync 调度异常查 BufferQueue 的 dequeue/queue 次数,确认是否走了三缓冲
内存持续增长Surface 未 release检查代码路径,确保 Surface 生命周期与窗口一致
Transaction 频繁 Binder 调用导致卡顿每次改单个属性都单独 apply合并多次操作到同一个 Transaction

6.1 生命周期管理

Surface 和 SurfaceControl 的释放顺序是有讲究的:先释放 Surface,再释放 SurfaceControl。反过来做的话,Surface 可能挂在已经失效的图层句柄上,释放时容易踩 native 层的 use-after-free。虽然 Android 15 的 Binder 稳定性处理已经比以前好,但这种顺序问题仍然能通过 ASan 抓出来。

6.2 Transaction 生效时机

Transaction 的 apply 是异步的。你调用apply()之后,状态不是立刻在 SF 侧生效,而是进了 SF 的事务队列,等合成线程处理。所以如果你要在 apply 之后立即读取图层状态,会读到旧值。这种场景罕见,但涉及动画联动时需要留意,必要时可以监听 SF 的 transaction 完成回调。

6.3 Visibility 与 Alpha

这是我踩过最深的坑。想要“隐藏”一个图层,应该用setVisibility(false),不要用setAlpha(0f)。alpha 为 0 的图层在部分合成器实现里仍会参与合成计算,白白消耗 GPU 带宽;更重要的是,alpha 动画会触发连续合成,如果目标是“彻底隐藏”,不仅浪费,还可能影响其他图层的合成效率。setVisibility(false)则直接把 Layer 标记为跳过合成,性能收益明显。

6.4 Surface 泄漏排查

如果发现 App 的 Surface 数量只增不减,优先怀疑两个地方:一是原生代码里有人拿了 ANativeWindow 没释放,二是 Java 层 Surface 对象被持有时没有调用release()。实机上可以用dumpsys SurfaceFlinger --list看总 Layer 数,再对比adb shell dumpsys window里的活动窗口数。如果 Layer 数量远大于窗口数,那基本就是 Surface 泄漏了。

做图形栈代码久了,我越来越觉得 Surface 和 SurfaceControl 的区分,本质上就是“生产”和“调度”的区分。很多人纠结半天,其实只要把这条主线拎住,源码怎么读都不会迷路。下一步要深入的话,我建议顺着 BufferQueue 的生产消费状态机再挖一层,把那套 slot 状态转移吃透。等你把 BufferQueue 和 Layer 树这两块都摸熟,SurfaceFlinger 对你来说就不再是一团黑盒了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询