
1. 显示链路里最容易被忽视的隐形合成器HWC2到底管什么做Android图形开发的人十有八九都经历过这种场景应用层帧率跑满60fpsGPU负载也没爆但屏幕上就是掉帧、卡顿或者功耗高得吓人。查了一圈最后问题落到SurfaceFlinger和Display硬件之间的那层协议上——没错就是HWC2。HWC2全称Hardware Composer 2是Android 8.0开始引入、Android P9.0上全面铺开的显示合成硬件抽象层。它的职责用一句话说清楚帮SurfaceFlinger把多个图层合成为一帧画面然后交给显示控制器Display Controller扫描输出。这个合成动作可以在硬件里做也可以在GPU里用OpenGL ES做。HWC2的存在就是让系统尽量把活交给硬件而不是每次都让GPU去合成因为硬件合成器通常功耗更低、延迟更小而且不占用GPU带宽。1.1 它不是一个硬件而是一套协商接口很多人第一次接触HWC2会有一个误解以为它是某个具体的显示芯片方案。实际上HWC2是一套定义在hardware/interfaces/graphics/composer/2.x和hardware/libhardware/include/hardware/hwcomposer2.h里的C接口规范。各家SoC高通、三星、联发科、海思的显示驱动团队按照这套规范实现自己的Composer HALSurfaceFlinger只跟这套接口打交道不关心底层到底是哪家的Display Controller。这套接口里最有意思的部分是协商机制每帧画面来了之后SurfaceFlinger不会自己拍板决定哪些图层用硬件合成、哪些用GPU合成而是先把这一帧的所有图层信息打包给HWC2让HWC2根据硬件能力比如支持多少层overlay、是否支持格式转换、是否有缩放限制来决定哪些图层它能吃下。然后SurfaceFlinger再根据HWC2的反馈把HWC2吃不下的图层用GPU合成。这个递清单、等回话、再分工的过程是整个图形显示系统里最核心的帧级交互。1.2 谁需要啃这块内容如果你是做系统开发的需要移植/调试显示驱动、SurfaceFlinger相关问题、帧率异常、屏幕撕裂、多屏异显HWC2是绕不开的一块。如果你是做应用开发的理解HWC2能帮你搞明白为什么某些效果比如实时模糊、多个Surface叠加会莫名掉帧——多半是图层数量或格式超出了硬件合成能力被迫走了GPU合成甚至绕了弯路。这篇文章我会从HWC2的对象模型、逐帧流程、同步机制到排查手段一条线讲透尽量用干活的视角讲不堆术语。2. HWC2相对HWC1的骨架变化对象模型与API设计思路想理解HWC2最好先知道它从HWC1那里继承了什么、改掉了什么。HWC1时代Android 7.0及以前的接口设计比较一次一帧HWC1对外暴露prepare()和set()两个核心调用每次刷新前SurfaceFlinger调prepare()把所有图层状态传下去HWC1返回每层的合成方式SurfaceFlinger再调set()真正提交。这套设计在单屏时代问题不大但到了双屏、多屏、虚拟显示场景HWC1就显得很别扭——所有display的信息全都揉在一个全局状态里扩展性和并发性都很差。HWC2把模型彻底重构成了三层对象Device、Display、Layer。一个Device对应一个Composer HAL实例一个Display对应一块物理屏幕或一个虚拟显示Layer对应显示链路上的一层Surface内容。所有API都围绕这三个对象展开不再有全局性的prepare all。2.1 从函数指针到按对象操作的接口范式HWC2的接口定义在hwc2_device_t结构体里核心函数指针有这些函数作用createVirtualDisplay/destroyVirtualDisplay创建/销毁虚拟显示validateDisplay让HWC校验某个Display上的所有图层getChangedCompositionTypes查询哪些图层被HWC要求换成CLIENT合成acceptDisplayChanges接受HWC返回的合成类型变更presentDisplay把合成结果提交给显示控制器setLayerState更新某个Layer的属性状态setClientTarget设置Client合成的目标BuffersetOutputBuffer设置虚拟显示的输出BuffergetDisplayConfigs/setActiveConfig查询/切换显示模式setVsyncEnabled控制vsync回调registerCallback注册hotplug等事件回调看到这里你会发现HWC2的设计思路很接近面向对象每个调用都会显式传入hwc2_display_t或hwc2_layer_t句柄而不是像HWC1那样全靠全局状态。这个变化带来的直接好处是多Display之间的操作可以独立进行比如主屏在present的同时副屏可以独立validate互不阻塞。2.2 合成类型谁吃不下就标记出来HWC2定义了五种合成类型分布在HWC2_COMPOSITION_*系列枚举里HWC2_COMPOSITION_DEVICE硬件直接合成的图层这是最理想的状态。HWC2_COMPOSITION_CLIENT需要SurfaceFlinger用GPU先合成好的图层。HWC2_COMPOSITION_SOLID_COLOR纯色图层很多硬件能直接渲染不需要真实Buffer。HWC2_COMPOSITION_CURSOR光标图层硬件有专门的cursor overlay通道移动光标时不需要重新合成所有图层。HWC2_COMPOSITION_SIDEBAND带外视频流比如HDMI输入直通SurfaceFlinger完全不参与内容生成。我见过不少人在看dumpsys时看到某个图层类型是CLIENT就慌其实CLIENT不等于出问题——只要CLIENT图层的数量和面积在可控范围内对性能影响有限。真正要警惕的是本该DEVICE合成的普通视频层被强制降级成CLIENT那种情况下帧拷贝开销会明显抬高。2.3 Hardware Composer 2.0与2.1、2.2、2.3的版本差异Android P上默认使用Composer接口的2.2/2.3版本取决于厂商实现从HAL层面看2.1版本是把1.x时代的一些能力重新整理成标准API2.2加入了setLayerPerFrameMetadataHDR元数据、setColorMode等2.3主要是增加了一些CTS测试所需的查询能力。实际开发中你更多接触到的是打包在hardware/interfaces/graphics/composer/2.2里的HIDL服务HWC2的这块实现已经从传统的hwcomposer.hlibhardware接口迁移到了HIDL binderized接口但概念模型一脉相承。3. 每一帧的分工谈判SurfaceFlinger和HWC2如何协作合成搞清楚对象模型之后我们来看最核心的逐帧流程。这一节我会把每一帧从App提交到屏幕点亮之间的谈判过程拆开讲这也是面试里最喜欢问的部分。3.1 一次完整的帧调度时间线假设现在有一个视频应用在播放视频系统导航栏、状态栏也在显示桌面壁纸在底层。这一帧开始时的图层栈大概有壁纸、视频Surface、状态栏、导航栏。SurfaceFlinger的doComposition()过程大致如下收集图层状态SurfaceFlinger遍历所有可见的Layer把每个Layer的Buffer、变换矩阵、裁剪区域、Alpha值打包成HWC2::Layer的属性绑定到对应的Display上。调用validateDisplaySurfaceFlinger把当前Display上的所有Layer交给HWC2询问这些层你能合成多少。HWC2内部评估Composer HAL遍历所有Layer对比硬件overlay通道的能力数量、格式、缩放、旋转限制返回每个Layer的推荐合成类型。这个返回是协商结果不是最终结果——SurfaceFlinger会判断这个结果是否可接受。处理变更请求如果HWC2返回了一些图层为CLIENTSurfaceFlinger把这些图层找出来通过getChangedCompositionTypes拿到具体列表然后acceptDisplayChanges接受变更接着用GPURenderEngine把所有CLIENT图层合成到一个叫client target的Buffer里。再次validate设置完client target之后SurfaceFlinger会再调一次validateDisplay这次HWC2看到的图层列表多了一个已合成好的client target剩下的图层如果都能DEVICE合成就通过校验。setLayerState presentDisplay最终提交。setLayerState把每个Layer的最新状态写进硬件presentDisplay触发显示控制器扫描输出这一帧。这段流程里有个细节值得注意一次present之前可能会发生多次validate。第一次validate是试探HWC2告诉SF哪些层它接不住SF用GPU接住之后再validate一次确认。所以你在trace里如果看到validateDisplay被调了两次不要觉得是bug这是正常的分工协商。3.2 client target到底是什么角色client target是HWC2里特别容易混淆的一个概念。它本质上是一个BufferSurfaceFlinger用GPU把所有CLIENT类型的图层合成到这个Buffer里。在HWC2看来这个client target就是一个已经画好的大图层可以被当作一个普通的DEVICE图层直接送去显示。这里有个坑client target不能是任意Buffer它的宽高必须与Display的分辨率一致经过scaler调整格式必须符合Display支持的格式。很多厂商的HWC实现里client target还必须带正确的ACQUIRE_FENCE否则硬件可能读取时机不对出现画面撕裂或花屏。3.3 虚拟显示与主屏的区别HWC2里虚拟显示Virtual Display的流程跟物理屏略有不同物理屏由显示控制器自动扫描输出虚拟显示需要SurfaceFlinger或上层应用主动取走合成好的Buffer通常用于屏幕录制、Miracast、多屏扩展。虚拟显示没有vsync和hotplug但同样要走validateDisplay和presentDisplay区别是presentDisplay之前必须调用setOutputBuffer指定输出Buffer。我在做屏幕录制类功能时踩过一个坑虚拟显示的presentDisplay如果不调用合成结果永远出不来但HWC2并不会报错它只是静默地忽略这帧。后来我加了严格的setOutputBuffer调用时序检查确保在present前Buffer已经设置好问题才解决。4. 帧边界上的三把锁acquire/release/retire fence 的来龙去脉聊HWC2不聊fence等于白聊。fence是整个显示同步机制的基石也是项目里最容易出诡异bug的地方。我曾经见过一个每隔几秒闪一下黑帧的问题查了三天最后定位到release fence提前signal导致Buffer被应用提前写入新内容HWC还在读旧内容。4.1 三种fence各自看住什么Fence类型语义典型用途Acquire FenceBuffer被HWC读取前必须等待的同步信号保证GPU/CPU渲染写完Buffer后HWC才开始读Release FenceHWC读完Buffer后返回的同步信号SurfaceFlinger据此将Buffer归还给生产方允许复用Retire Fence显示控制器真正扫描输出完成后的同步信号衡量帧真正上屏的时间点用于统计帧延迟这三者的关系可以类比餐厅出餐Acquire fence是菜还没做好服务员不能端走Release fence是服务员已经把菜端上桌了后厨可以继续用这个盘子做下一道菜Retire fence是顾客已经吃完了这道菜的服务彻底结束。4.2 SurfaceFlinger里的fence流转在SurfaceFlinger的帧流程里每一层Buffer从BufferQueue取出来时BufferQueue会附带一个acquire fence。SurfaceFlinger把这个fence随Layer状态一起传给HWC2表示这个Buffer要等fence signal之后才能读。HWC2在validateDisplay和presentDisplay过程中会记录这些fence合成完成后为每个Layer生成release fence。release fence的传递链很重要HWC2产生的layer release fence需要被SurfaceFlinger写回BufferQueue这样生产方App的CPU/GPU才能知道这个Buffer我什么时候可以重新渲染。如果这条链断了最常见的结果是App侧一直拿不到Buffer开始丢帧更隐蔽的结果是Buffer在HWC还没读完时就被覆盖画面出现闪烁。4.3 retire fence与帧率统计retire fence是判断这一帧到底什么时候真正上屏的唯一可靠信号。SurfaceFlinger在present之后拿到retire fenceChoreographer的帧时间统计会基于它进行计算。dumpsys SurfaceFlinger里的frame stats和gfxinfo的janky数据本质上都依赖retire fence的精确时间。我调试过一台设备gfxinfo显示帧间隔非常均匀但肉眼看明显卡顿最后发现是HWC实现里retire fence提前signal了导致统计认为帧已上屏真实扫描输出却慢了一拍。这种fence时序错误非常隐蔽只能通过对比硬件vsync时间戳和retire fence时间戳来定位。5. 实战调优用dumpsys和systrace判断合成是否真的走硬件纸上谈兵没意思这一节直接给排查手段。拿到一台设备第一件事就是看当前帧的合成策略到底长什么样。5.1 dumpsys SurfaceFlinger的逐层解读执行adb shell dumpsys SurfaceFlinger输出里有一大段描述每个Display和Layer状态的内容。重点关注这些字段Layer 0x... (SurfaceView[应用名]) ... composition type: DEVICE ...composition type字段直接告诉你这个图层HWC判定的合成方式。DEVICE表示硬件合成CLIENT表示GPU合成。在正常播放视频且不做特殊效果的场景下视频图层和UI图层都应该是DEVICE如果看到大面积CLIENT说明HWC2的协商结果不理想需要分析是硬件能力限制还是Buffer格式不匹配。另一个值得看的是display相关的viewport、frame和sourceCrop字段。HWC2对缩放、裁剪能力有限制如果某个Layer的sourceCrop和frame尺寸差异过大HWC很可能会把它降级为CLIENT因为硬件缩放器scaler的数量和缩放倍数都有限。5.2 systrace抓一次完整的present链systrace是看帧流程时间线最直观的手段。抓取gfx、view、sched分类的trace在SurfaceFlinger进程里你会看到这几个关键节点onMessageReceived触发合成validateDisplay调用HWCpresentDisplay提交GPU合成如果有CLIENT层draw/flush相关标记如果validateDisplay到presentDisplay之间的间隔明显变大说明HWC2在内部处理上耗时偏高可能是驱动层的overlay分配逻辑有问题如果presentDisplay之后等了很久才出现retire fence则可能是显示控制器的扫描排队问题。还有一个容易忽略的点systrace里看HWC调用是否走了binder。Android P上Composer服务如果是以HIDL方式运行validateDisplay会跨进程调用trace里能看到明显的binder transaction开销。这本身不一定是问题但如果你发现每帧在binder上花了超过1ms就需要评估是否值得优化成passthrough模式同进程直接调用。5.3 FrameStats与掉帧归因adb shell dumpsys gfxinfo package framestats可以拿到App侧每帧的详细时间数据。有一个技巧如果你看到FRAME_COMPLETED时间稳定但INTENDED_VSYNC到APP_SWAP_BUFFER之间出现异常抖动问题往往不在HWC而在App的渲染线程如果App侧一切正常但屏幕实际显示不跟手那就要重点查HWC的present和retire链路。6. 移植和调试HWC2时的高频坑位与排查路径最后这部分我把我这些年调HWC2遇到的高频问题整理成几条排查路径每一条都是血泪教训。6.1 validate总是失败的几类根因最容易遇到的是新板子BringUp阶段validateDisplay返回值一直是HWC2_ERROR_BAD_LAYER或者HWC2_ERROR_LAYER_CLIENT_SLOT。这类问题通常有四个高发原因Layer的BufferHandle不合法HWC2要求Layer绑定的Buffer必须是经过gralloc分配的、带正确格式和对齐的Buffer。很多情况下是上层传入了一个已释放的Buffer句柄HWC在访问时直接崩溃或返回错误。排查方法是打印每个Layer绑定Buffer的handle值跟gralloc分配记录做对照。Transform标志位设置错误HWC2的setLayerTransform接受的是HWC2_TRANSFORM_*位标志跟SurfaceFlinger内部使用的ui::Transform不是同一套。常见错误是直接把ROT_90的SurfaceFlinger值传给HWC导致HWC把旋转方向理解错校验直接失败。转换标准是看hardware/interfaces/graphics/common/1.x里的Transform定义。BlendMode不受支持部分硬件不支持HWC2_BLEND_MODE_PREMULTIPLIED以外的混合模式或者强制要求所有Layer都要设置明确的BlendMode。HWC2规范里BlendMode是可选设置的但有些驱动实现偷懒没设就报错。稳妥做法是每个Layer都显式设置BlendMode。Layer数量超过硬件overlay上限很多中端SoC的硬件overlay通道只有4~5条一旦Layer数量超限多余的必须降级为CLIENT合成。这不是bug是硬件能力边界。但如果降级的Layer恰好是大面积视频层就会明显影响功耗。优化思路是提前在应用层合并Surface或者在SF层把静态图层合成到缓存Buffer里。6.2 花屏和撕裂的排查思路花屏问题十有八九跟fence或Buffer生命周期有关。我建议按以下顺序排查先确认是否只在特定分辨率/刷新率下出现。如果是重点查HWC2的display config和scaler配置。再确认是否与App相关。如果换其他应用不花屏重点看该应用的Buffer格式和连帧提交方式。如果花屏是间歇性的优先怀疑release fence提前signal导致Buffer在HWC读取期间被写坏。在HWC驱动里加fence时间戳日志比较acquire/release/retire三者时间差是最直接的验证手段。如果是屏幕上半部分和下半部分显示内容不同步那多半是tearing说明显示控制器的scanline同步没生效检查vsync和framebuffer的commit时序。6.3 HDR和宽色域相关的坑Android P开始HDR相关支持成为HWC2的重要功能点。setLayerPerFrameMetadata用于传递HDR静态元数据SDR/HDR亮度范围、最大CLL、最大FALL等。实际项目里最常见的坑是HDR视频的Buffer格式是HAL_PIXEL_FORMAT_RGBA_1010102或HAL_PIXEL_FORMAT_YV12等特殊formatHWC驱动如果没做对应处理会直接把该Layer降级为CLIENT导致HDR效果丢失画面发灰。排查方法是看dumpsys里HDR层的composition type如果显示CLIENT大概率是驱动不支持该Buffer格式的硬件合成。此时需要驱动侧补上格式支持或者在SurfaceFlinger侧配置setDimmingRatio和setLayerGenericMetadata做兜底。6.4 多屏异显场景下的display id管理Android P支持多Display后HWC2的getDisplayType和hotplug回调成了多屏逻辑的关键。很多方案做双屏扩展时经常遇到副屏无输出或主副屏混淆的问题根因多半是HWC驱动在枚举物理显示时没有正确上报HWC2_DISPLAY_TYPE_PHYSICAL的display id导致SurfaceFlinger不知道哪块屏是主屏。建议在驱动BringUp阶段就把各Display的getDisplayName和getDisplayType打出来认真核对。还有个细节副屏的分辨率必须是HWC上报的config之一setActiveConfig传不对会直接导致黑屏和回调异常。6.5 一个容易忽略的性能细节damage regionHWC2支持setLayerDamageRegion来告知硬件这一帧只有哪部分区域更新了。很多驱动在合成时利用damage region做局部刷新省掉全屏扫描的功耗。但如果damage region计算错误——比如漏报了某个更新区域——副效应就是屏幕上出现残留的鬼影。这类问题形态诡异切应用时上一帧内容残留在屏幕上几秒才消失排查时第一反应往往是怀疑buffer queue或缓存逻辑很少有人想到是damage region漏报。验证办法是把debug.sf.disable_damage设为1强制全帧刷新如果鬼影消失就确认是damage region的问题。注意这个属性在user版本可能被限制调试版上才方便用。最后说几句实在话HWC2这块内容框架层代码不算多但联动的东西特别广BufferQueue、gralloc、RenderEngine、显示驱动随便哪一环出问题表象都可能落在合成策略异常或者掉帧上。我个人的习惯是入手新平台的第一件事就是把dumpsys SurfaceFlinger的合成结果和systrace的present链路存一份基线后续调优改了什么配置对比基线看差异比凭空猜高效得多。还有一个小技巧想分享在开发阶段可以给HWC驱动加一个挂在validateDisplay里的debug节点每帧打印返回的合成类型统计——比如device: 3, client: 1, cursor: 1。别小看这一行日志它能在你改动应用层代码后立刻暴露出合成策略变化很多性能问题就是这么被提前逮住的。如果你也在移植或调试HWC2建议先把这套统计日志加进去它会是整个调试过程里最省力的一把尺子。