Android 技术目前已经非常成熟了,无论是硬件还是软件,当前大环境下,为数不多的创新主要还集中在AI应用层面,当前比较尴尬的是AI应用也非常依赖后端算力,On-Device AI总体来说没有气候。主要瓶颈:模型算力、内存太小、接口不统一等。
不过,随着未来人形机器人以及手机硬件的发展,端侧必然也是趋势,目前端侧最成功的是自动驾驶了,其中最值得期待的是FSD的发展。作为Android开发者,在AI浪潮中你没有选择,只能跟随AI发展的趋势。相信不远的未来,无论是AI-Agent还是On-Device AI,都是有机会的。
回到本篇文章的主题,我们来讨论下关于SurfaceFlinger优化的问题。不过,说到底,很多时候说优化其实还有歧义,对于任何一款硬件,本身具备上限,你不可能突破上限,突破上限那才叫优化。现实中,很多优化只是为了接近上限。
关于HWC
HWC是Hardwrare Composer 的简称,其作用是按照屏幕单位,合成来自不同Surface Buffer画面,其工作在SurfaceFlinger中,也就是系统进程中运行。
SurfaceFlinger与屏幕关系
单一屏幕(Display)都有一个与之对应的LayerStack(可以当作是屏幕ID),而SurfaceFlinger会维护不同Display的LayerStack。其中,一个Display可以有多个Surface,如Activity Window、状态栏、导航栏、Dialog Window等,SurfaceFlinger除了负责数据交互和Vsync通信之外,还会调度HWC去合成,最后调用GPU去渲染到Display。
你可能会疑惑,View本身就在Display中,非要绕一圈么,其实这里的误解是Display是虚拟设备,他和物理屏幕存在映射关系,无论是HDMI Display、Wifi Display、Overlay Display,他们都是物理或者逻辑屏幕的虚拟映射。
Android系统中,本身就是支持多屏的,一般Default Display就是主屏幕了。
HWC退化是什么
在SurfaceFlinger中,很多人一提到合成就认为必然通过HWC进行,然而,很多时候往往都会事与愿违,特别是在低端设备、透明叠层的情况中会退化为GPU Client合成。
GPU Client合成并不是说在app中合成,依然是在SurfaceFlinger中调度GPU实现合成,合成之后渲染输出。
什么情况下会退化为GPU Client呢?
SurfaceFlinger本身属于操作系统程序的一环,肯定会优先读取芯片方案中配置来决定,如果配置依然是HWC优先的话,优先会提交给HWC模块。HWC优点是省电、高性能,但也不是很强大,其直接调用Overlay(Device Composition)叠加渲染,也就是调用屏幕芯片去渲染,而在HWC发现Surface的Buffer有特殊的渲染效果如特殊旋转、rgba101010格式、多Surface叠加、Surface数量超过了单一屏幕的负载时就会退化为GPU Client。
HWC与GPU Client共同工作流程
为什么要治理
-
释放 GPU 算力(Client 转向 Device 合成) 如果没有 HWC,SurfaceFlinger 必须通过 OpenGL ES 或 Vulkan 将所有 Layer 渲染到一个离屏缓冲区(Client Composition)。HWC 利用硬件的 Overlay 通道(Device Composition)直接扫描显示各个 Layer,从而将 GPU 算力完全释放给 App 层,用于处理更复杂的游戏渲染、UI 动效或自定义 Shader 运算。
-
显著降低系统功耗 GPU 是高功耗组件。唤醒 GPU 进行简单的图层叠加(例如将状态栏、导航栏与主 UI 结合)会造成不必要的电量浪费。专门的硬件显示控制器(如高通的 MDP/SDE)针对图层叠加做了极度优化,能耗比远超 GPU,这在播放视频或长时间浏览 UI 时对延长设备续航至关重要。
-
大幅节省内存带宽(零拷贝显示) 使用 GPU 合成图层时,系统需要先将各个 Layer 的 GraphicBuffer 读取为纹理,再将合成结果写入到一个独立的 Framebuffer 中,带来巨大的读写带宽消耗。HWC 的硬件 Overlay 机制允许显示控制器直接从各个独立 Layer 的物理内存中读取数据并扫描输出到屏幕,实现了零拷贝(Zero-copy),极大缓解了系统的内存带宽压力。
-
降低显示延迟并减少掉帧 由于绕过了 GPU 的图形渲染管线(无需等待 GPU 任务队列调度、纹理上传及光栅化),图层可以直接在 VSync 周期内由硬件快速推送到屏幕。这种更加短平快的流水线降低了从 App 提交 Buffer 到屏幕点亮的整体延迟,提升了 UI 交互的跟手性。
-
原生支持多媒体特殊场景(视频与 DRM) 底层显示硬件通常内置了专门的模块,HWC 能够直接利用这些硬件特性:
- 色彩与格式转换: 可以直接接收解码器输出的 YUV 格式视频流并进行硬件级 YUV-to-RGB 转换,无需 GPU 介入。
- 硬件缩放与旋转: 利用硬件 Scaler 处理画中画(PiP)或全屏视频缩放,效率极高。
- DRM 数字版权保护: 播放 Widevine L1 级别的加密视频流时,解密后的数据被保护在安全内存中(Secure Buffer),GPU 无法读取。这种受保护的视频只能通过 HWC 安排硬件 Overlay 通道直接投送到屏幕上。
显而易见,最大的目的是降低GPU芯片的负载,当然也能降低GPU负载、而且还省电、省内存,特别是低配设备,效果会非常明显。另外,省电、省内存扩展到另一个纬度就是降低CPU发热,很多时候,CPU发热会导致降频。
退化检测
具体我们可以通过下面命令收集参数
adb shell dumpsys <span>SurfaceFlinger</span> > <span>/tmp/</span>sf.<span>txt</span>
检索其中不同的SurfaceView,可能会出现多种情况
- usesClientComposition=[true|false],true代表使用Gpu Client
- Client / Device,前者是GPU Client
- Android 4.4 的 HWC_FRAMEBUFFER / HWC_OVERLAY,前者是GPU Clinet
如何退化
前面我们说过,画面过于复杂、透明叠加、Surface数量过多都是导致HWC退化为GPU Client的因素,那么,如何防止退化呢 ?
减少Surface数量
对于一个屏幕而言,减少Surface数量很重要,大部分低配设备Surface不允许超过4个。 这里说的Surface数量仅仅是能和SurfaceFlinger直接通信的Surface,如ViewRootImpl、SurfaceView 、GLSurfaceView等。对于本地的SurfaceTexure、EGL Surface 不受影响。
比如必要时隐藏导航栏、状态栏、减少Overlay Window、Dialog Window等等。
同时避免多屏渲染,避免使用屏幕录制、VirtualDisplay等。
降低刷新率或者视频清晰度
在很多系统中,受限于内存不足的情况,特别是对于1080p、2k、4k这样的视频,加上超高的刷新率(FPS),会导致HWC提交变慢甚至丢帧,部分情况下会丢给GPU Client,借助GPU显存渲染。
对于音视频应用,我们可以从几个维度来降低刷新率和清晰度:
- 480p、720p、1080p、2k、4k,随着清晰度的变大,rgba单帧数据都要好几兆内存、显存空间,因此,不要盲目的在认为越高越好,你首先要保证产品的可用性,其次才是体验。
- 刷新率是一个非常好的解决手段,目前主流的播放器中,MediaPlayer是不支持降帧率的、iJkPlayer似乎支持、ExoPlayer需要自己帧率限制算法,当然也可以实现自适应帧率,借助ai,也不是事。至于MediaPlayer、你还有一种方法,就是通过EGL 中间层丢帧,不过往往能保证播放高清晰度视频,但是性能略差一点,手段复杂一点罢了。
- SurfaceHoder设置FixSize 也是一种手段,可以降低视频缓存大小,不过我测试发现效果并不明显,不如限制清晰度和刷新率。
SurfaceView选择最常用的色彩格式
像素格式(Pixel Format) 与 色彩空间(Color Space / DataSpace) 的不匹配,是多媒体应用(视频播放、K 歌录制、相机预览)中引发 HWC 退化为 GPU Client 最硬核、也是最经典的场景。
- 像素格式(Format): 指 Buffer 的内存布局。例如
RGBA_8888(RGB 各 8 bit)、NV12(YUV 4:2:0 8 bit)、P010(YUV 4:2:0 10 bit)。 - 色彩空间(DataSpace): 指像素的色彩范围与传递函数(EOTF)。例如
Rec.709 / sRGB(标准 SDR)、Rec.2020 + PQ / HLG(HDR10 / Dolby Vision)、Display-P3(广色域)。
对于SurfaceView,如果期望有透明度,就选择RGBA_8888,大概率也不会退化为Client,其他情况可以选择RGBX_8888、YUV420系列就行了,色彩空间理论上本身也是影响RGBA中alpha的值,如果是你自己渲染,这个还是挺可控的,毕竟算法是你写的,如果交给MediaCodec渲染,那这里可能要抉择。
不过这里仍然有一个坑点,可能很多人没注意到,大部分人写GLConfig时,直接写成下面的配置
int<span>[]</span> <span>attribList</span> = {
EGL14.EGL_RED_SIZE, EGL_COLOR_BITLENGTH,
EGL14.EGL_GREEN_SIZE, EGL_COLOR_BITLENGTH,
EGL14.EGL_BLUE_SIZE, EGL_COLOR_BITLENGTH,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL_RECORDABLE_ANDROID, 1,
EGL14.EGL_SURFACE_TYPE, EGL14.EGL_PBUFFER_BIT | EGL14.EGL_WINDOW_BIT,
EGL14.EGL_NONE
}<span>;</span>
EGLConfig<span>[]</span> <span>configs</span> = new EGLConfig[<span>1</span>]<span>;</span>
int<span>[]</span> <span>numConfigs</span> = new int[<span>1</span>]<span>;</span>
EGL14.eglChooseConfig(mEGLDisplay, attribList, /*offset*/ 0, configs, /*offset*/ 0,
configs.length, numConfigs, /*offset*/ 0)<span>;</span>
但这里有个风险,部分设备会因为RGBA不匹配退化为RGBA10101010,导致GPU Client退化,正确的方式是一定要筛选
<span>private</span> <span>static</span> EGLConfig <span>chooseExactEglConfig</span><span>(
EGLDisplay display, <span>int</span> pixelFormat,
<span>int</span> redSize, <span>int</span> greenSize, <span>int</span> blueSize, <span>int</span> alphaSize,
<span>int</span> requiredSurfaceTypes)</span> {
EGLConfig[] configs = <span>new</span> <span>EGLConfig</span>[MAX_EGL_CONFIGS];
<span>int</span>[] configCount = <span>new</span> <span>int</span>[<span>1</span>];
<span>if</span> (!EGL14.eglGetConfigs(
display, configs, <span>0</span>, configs.length, configCount, <span>0</span>)) {
<span>return</span> <span>null</span>;
}
<span>int</span> <span>expectedBufferSize</span> <span>=</span> redSize + greenSize + blueSize + alphaSize;
<span>int</span> <span>count</span> <span>=</span> Math.min(configCount[<span>0</span>], configs.length);
<span>for</span> (<span>int</span> <span>i</span> <span>=</span> <span>0</span>; i < count; i++) {
<span>EGLConfig</span> <span>config</span> <span>=</span> configs[i];
<span>int</span> <span>surfaceTypes</span> <span>=</span> getEglConfigValue(display, config, EGL14.EGL_SURFACE_TYPE);
<span>int</span> <span>renderableTypes</span> <span>=</span> getEglConfigValue(display, config, EGL14.EGL_RENDERABLE_TYPE);
<span>if</span> ((surfaceTypes & requiredSurfaceTypes) != requiredSurfaceTypes
|| (renderableTypes & EGL14.EGL_OPENGL_ES2_BIT) == <span>0</span>) {
<span>continue</span>;
}
<span>if</span> (getEglConfigValue(display, config, EGL14.EGL_RED_SIZE) != redSize
|| getEglConfigValue(display, config, EGL14.EGL_GREEN_SIZE) != greenSize
|| getEglConfigValue(display, config, EGL14.EGL_BLUE_SIZE) != blueSize
|| getEglConfigValue(display, config, EGL14.EGL_ALPHA_SIZE) != alphaSize
|| getEglConfigValue(display, config, EGL14.EGL_BUFFER_SIZE) != expectedBufferSize
|| getEglConfigValue(display, config, EGL14.EGL_NATIVE_VISUAL_ID) != pixelFormat) {
<span>continue</span>;
}
<span>return</span> config;
}
<span>return</span> <span>null</span>;
}
减少Surface/Window 圆角、裁剪等复杂矩阵变化
- --- 场景 A:设置背景模糊 (Android 12+) ---
<span>// 在 Window 或 SurfaceControl 上设置 50px 的背景模糊</span>
<span>window</span>.<span>setBackgroundBlurRadius</span>(<span>50</span>)
--- 场景 B:设置自定义 SurfaceControl 滤镜 ---
<span>// 获取当前 Window 或 SurfaceView 对应的 SurfaceControl</span>
SurfaceControl surfaceControl = mySurfaceView<span>.getSurfaceControl</span>();
<span>// 创建一个 SurfaceFlinger 事务</span>
SurfaceControl<span>.Transaction</span> transaction = new SurfaceControl<span>.Transaction</span>();
<span>// 场景 1:设置 Surface 级别的背景模糊(需截取下层像素,DPU 无法处理)</span>
transaction<span>.setBackgroundBlurRadius</span>(surfaceControl, <span>100</span>);
<span>// 场景 2:设置 Surface 级别的 4x4 颜色转换矩阵 (如夜间模式、跨 Surface 滤镜)</span>
<span>float</span><span>[]</span> colorMatrix = new <span>float</span><span>[]</span> {
<span>1.2</span>f, <span>0.0</span>f, <span>0.0</span>f, <span>0.0</span>f, <span>// R 增益</span>
<span>0.0</span>f, <span>1.0</span>f, <span>0.0</span>f, <span>0.0</span>f, <span>// G</span>
<span>0.0</span>f, <span>0.0</span>f, <span>0.8</span>f, <span>0.0</span>f, <span>// B 减益</span>
<span>0.0</span>f, <span>0.0</span>f, <span>0.0</span>f, <span>1.0</span>f <span>// Alpha</span>
};
<span>float</span><span>[]</span> translation = new <span>float</span><span>[]</span> { <span>0</span>, <span>0</span>, <span>0</span>, <span>0</span> };
transaction<span>.setColorTransform</span>(surfaceControl, colorMatrix, translation);
<span>// 将该 Transaction 提交给 SurfaceFlinger 进程</span>
transaction<span>.apply</span>();
很显然,主要针对RenderNode,单纯的对View ClipPath后者ClipRect并不会引起退化
减少Surface动画或者变换
很简单,动画导致图层裁剪和旋转,HWC不一定有这能力,不过对于90度倍数的旋转还是可以的,因此,对于SurfaceView尽可能按90度的倍数旋转。
总结
本篇主要是SurfaceFlinger性能优化,希望对你有说帮助。
面向 Android 多媒体与系统性能开发者,梳理 HWC 退化原因与防治手段,适合低端设备视频播放、SurfaceView 与多图层场景调优参考。