Android 防治HWC退化方法

文章来源声明: 原文作者:时光少年; 来源站点:掘金; 原文链接:https://juejin.cn/post/7688689006075756580; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

面向 Android 多媒体与系统性能开发者,梳理 HWC 退化原因与防治手段,适合低端设备视频播放、SurfaceView 与多图层场景调优参考。

前言 --

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共同工作流程

deepseek_mermaid_20260924_c9ba4b.png

为什么要治理

  • 释放 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性能优化,希望对你有说帮助。