前言
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去合成,最后调用设备驱动去渲染到Display。
你可能会疑惑,View本身就在Display中,非要绕一圈么,其实这里的误解是Display是虚拟设备,他和物理屏幕存在映射关系,无论是HDMI Display、Wifi Display、Overlay Display,他们都是物理或者逻辑屏幕的虚拟映射。
Android系统中,本身就是支持多屏的,一般Default Display就是主屏幕了。
HWC退化是什么
在SurfaceFlinger中,很多人一提到合成就认为必然通过HWC进行,然而,很多时候往往都会事与愿违,特别是在低端设备,HWC退化会导致很明显的卡顿、掉帧问题。大部分情况大家可能归因为系统性能问题,实则不然,很多时候,往往是使用不当引起的或者架构问题。常见的原因Surface可能存在复杂矩阵变换、hwc不支持特定的像素格式、surface叠层过多的情况,在很多低端设备中很容易退化为GPU Client合成。
GPU Client合成并不是说在app中合成,依然是在SurfaceFlinger中调度GPU实现合成,合成之后渲染输出。
什么情况下会退化为GPU Client呢?
SurfaceFlinger本身属于操作系统程序的一环,肯定会优先读取芯片方案中配置来决定,如果配置是HWC优先的话,优先会提交给HWC模块,HWC不支持就会使用GPU Client兜底。
HWC优点是省电、高性能,但也不是很强大,其直接调用Overlay(Device Composition)叠加渲染,也就是调用屏幕驱动去渲染,正常情况下surfaceflinger会将多个surface layer数据都给hwc。但是在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 SurfaceFlinger > /tmp/sf.txt
检索其中不同的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渲染,那这里可能要抉择。
不过这里仍然有一个坑点,对于使用egl做离屏渲染到需求,我们往往要桥接EGLSurface,但这里很多时候会因为eglconfig匹配问题出现格式退化,进一步导致HWC退化为GPU client合成。
使用egl创建context时,可能很多人没注意到,大部分人写EGLConfig时,直接写成下面的配置
int[] attribList = {
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
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
EGL14.eglChooseConfig(mEGLDisplay, attribList, /*offset*/ 0, configs, /*offset*/ 0,
configs.length, numConfigs, /*offset*/ 0);
但这里有个风险,部分设备会因为RGBA不匹配退化为RGBA10101010或者其他格式,你明明设置的RGBA_8888,但最终的格式确实另一个。很多时候,不是egl的问题,而是芯片厂商适配的问题。
为什么会这样呢?
因为eglconfig有好多配置,配置与配置可能冲突,到设备底层可能会因为不同的解决方式,对eglconfig进行修改。
rgba变成了hwc不能直接overlay的数据,自然导致GPU Client退化。正确的方式是一定要筛选eglconfig,确保获取到正常的eglconfig。
private static EGLConfig chooseExactEglConfig(
EGLDisplay display, int pixelFormat,
int redSize, int greenSize, int blueSize, int alphaSize,
int requiredSurfaceTypes) {
EGLConfig[] configs = new EGLConfig[MAX_EGL_CONFIGS];
int[] configCount = new int[1];
if (!EGL14.eglGetConfigs(
display, configs, 0, configs.length, configCount, 0)) {
return null;
}
int expectedBufferSize = redSize + greenSize + blueSize + alphaSize;
int count = Math.min(configCount[0], configs.length);
for (int i = 0; i < count; i++) {
EGLConfig config = configs[i];
int surfaceTypes = getEglConfigValue(display, config, EGL14.EGL_SURFACE_TYPE);
int renderableTypes = getEglConfigValue(display, config, EGL14.EGL_RENDERABLE_TYPE);
if ((surfaceTypes & requiredSurfaceTypes) != requiredSurfaceTypes
|| (renderableTypes & EGL14.EGL_OPENGL_ES2_BIT) == 0) {
continue;
}
if (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) {
continue;
}
return config;
}
return null;
}
减少Surface/Window 圆角、裁剪等复杂矩阵变化
- --- 场景 A:设置背景模糊 (Android 12+) ---
// 在 Window 或 SurfaceControl 上设置 50px 的背景模糊
window.setBackgroundBlurRadius(50)
--- 场景 B:设置自定义 SurfaceControl 滤镜 ---
// 获取当前 Window 或 SurfaceView 对应的 SurfaceControl
SurfaceControl surfaceControl = mySurfaceView.getSurfaceControl();
// 创建一个 SurfaceFlinger 事务
SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
// 场景 1:设置 Surface 级别的背景模糊(需截取下层像素,DPU 无法处理)
transaction.setBackgroundBlurRadius(surfaceControl, 100);
// 场景 2:设置 Surface 级别的 4x4 颜色转换矩阵 (如夜间模式、跨 Surface 滤镜)
float[] colorMatrix = new float[] {
1.2f, 0.0f, 0.0f, 0.0f, // R 增益
0.0f, 1.0f, 0.0f, 0.0f, // G
0.0f, 0.0f, 0.8f, 0.0f, // B 减益
0.0f, 0.0f, 0.0f, 1.0f // Alpha
};
float[] translation = new float[] { 0, 0, 0, 0 };
transaction.setColorTransform(surfaceControl, colorMatrix, translation);
// 将该 Transaction 提交给 SurfaceFlinger 进程
transaction.apply();
很显然,主要针对RenderNode,单纯的对View ClipPath后者ClipRect并不会引起退化
减少Surface动画或者变换
很简单,动画导致图层裁剪和旋转,HWC不一定有这能力,不过对于90度倍数的旋转还是可以的,因此,对于SurfaceView尽可能按90度的倍数旋转。
总结
本篇主要是SurfaceFlinger性能优化,很多时候都是大家容易忽视的问题。基于目前消费降级推动硬件配置降级的情况,这类优化显然你必须要了解。
好了,本篇就到这里,希望对你有说帮助。