做图形系统相关开发这些年,被问得最多的问题其实就一个:屏幕上的一张UI,到底是怎么从App里的代码变成像素的?尤其是到了Android 16这个版本,图形栈涉及的东西更多了,HDR、可变刷新率、多窗口、屏幕折叠形态……每一个新特性背后都牵扯着从App到内核到显示驱动的整条链路。如果对系统架构没有一个整体认识,遇到性能问题基本只能靠猜。
这篇我打算把Android 16的图形系统架构完整拆一遍,从App侧的绘制、系统侧的合成,到屏幕侧的显示,把整条流水线讲清楚。适合做系统开发、应用性能优化、游戏引擎适配,以及准备系统架构相关工作的朋友。哪怕你只做上层应用,理解这条链路之后,再看卡顿、掉帧、发热这类问题,思路也会完全不一样。
1. 为什么Android图形系统要搞这么复杂
经常有人问,Android图形栈为什么这么多层?画一个按钮而已,有必要绕这么大一圈吗?我的答案是:有必要,而且不是一般的必要。原因不复杂:图形系统要解决的,不仅仅是“把图画出来”,而是“把无数个App各自画的东西,在同一块屏幕上正确、稳定、高效地拼接起来”,还要管安全、管功耗、管不同硬件之间的差异。这本质上是一个多生产者、单消费者的实时调度系统。
1.1 一个App在屏幕上画一帧,要经过哪几道门
先打个比方。你在餐厅点菜,后厨做菜,服务员端菜,这叫一帧。但Android的实际情况是:楼下几十个餐厅同时做菜,每道菜都要送到同一个收银台上,由一台合成机器把它们拼成一份套餐,再由一个专门的传送带送到你桌上。App之间互相看不见,更不能直接写在同一个屏幕上,否则一个App崩溃或画错就会污染另一个App的内容,这在安全上是绝对不能接受的。
所以Android把图形链路拆成了三个大层次:
- App层负责“画”:调用Canvas、OpenGL ES或者Vulkan,把UI、图片、游戏场景渲染到一块内存缓冲区里。这部分主要由HWUI、Skia和GPU驱动完成。
- 系统层负责“合”:SurfaceFlinger拿到所有App绘制好的缓冲区,把它们按窗口层级、透明度、变换关系合成在一起,生成一帧完整的画面。这里还可能把一部分工作交给硬件合成器(HWC)来做,目的就是省电、省性能。
- 显示层负责“送”:合成后的画面通过显示控制器,按照屏幕刷新率被主动推送到显示面板上。Android里的Vsync、Display、HWC HAL都在这层。
这三个层次彼此通过BufferQueue、Binder、共享内存等机制衔接,每一层都有自己的调度节奏。理解了这道分层,后面所有细节都有地方安放。
1.2 Android 16图形栈的整体面貌
到了Android 16,这套分层的大骨架并没有变,依然是App绘制、SurfaceFlinger合成、HWC显示三层结构。但在细节上,能明显感觉到两个趋势。
一个是Vulkan的地位越来越高。在较早的系统版本里,Skia渲染默认走OpenGL ES后端;而现在的系统里,Vulkan后端已经成为GPU加速渲染的推荐路径。对于GPU厂商来说,Vulkan的驱动模型更贴近现代GPU,能减少驱动层的魔法和转换,对功耗、性能都更友好。Android 16的图形调度对Vulkan的支撑力度也在增强,游戏和复杂动画的渲染路径更依赖这套新接口。
另一个是帧率的精细化调度。现在屏幕已经不只是60Hz了,90Hz、120Hz甚至可变刷新率越来越普遍。Android 16里,App可以更准确地表达自己对帧率的需求,系统也会根据内容动态调整刷新率,这就让Vsync和帧调度变得更复杂,绝不是“每秒固定触发60次”那么简单。后面我会专门说这块。
这一层架构的总体目标归纳起来,就是延迟要低、吞吐要稳、功耗要可控。所有模块的设计,都是围绕这三个指标展开的。
2. 核心引擎拆解:SurfaceFlinger、HWUI与Vulkan
在图形系统里,有三个引擎是你无论如何都绕不开的:负责系统合成的SurfaceFlinger,负责应用UI绘制的HWUI和Skia,以及底层图形API代表的Vulkan(也包括OpenGL ES)。它们各自干各自的活,但边界有时候会让新人困惑。比如SurfaceFlinger是系统进程里的服务,而HWUI是每个App进程里都在跑的渲染引擎。
2.1 SurfaceFlinger到底在合成什么东西
SurfaceFlinger是图形系统的“总装车间”。每个可见窗口在系统侧都会对应一个Surface,SurfaceFlinger手里维护着这些Surface的图层(Layer)信息,包括位置、大小、旋转角度、透明度、圆角裁剪、色彩空间等。当App绘制完一帧并提交缓冲区后,SurfaceFlinger会把这些缓冲区拿过来,配合图层的属性,统一合成输出。
为什么要“统一合成”?因为屏幕只有一块,但应用可能有几十个。如果不让一个中心化的服务来管,让App各画各的,画面就会互相冲突。而且很多系统UI,比如状态栏、导航栏、壁纸、系统弹窗,它们的绘制优先级、安全区域都需要被统一管理。
SurfaceFlinger的合成过程不是简单地从上往下叠图,它得处理的东西包括:
- 多图层混色(Blend)
- 缩放、平移、旋转
- 裁剪到窗口可见区域
- 色彩空间转换,比如HDR内容映射到SDR屏幕
- 透明度和过渡动画
在业界,它被设计成一个响应型事件驱动系统,平时大部分时间处于等待状态,一旦某个Layer有新的缓冲区提交,或者系统属性发生变化,它就结束等待、主动或者由硬件信号触发去做一次合成。这种设计的本质是为了省电——没事的时候尽量睡。
2.2 HWUI和Skia在App里的绘制链路
再往App内部看,绝大部分Android UI是通过HWUI来绘制的。你可以把它理解成一个专门为Android UI优化的渲染引擎,它把View体系的绘制请求转换为GPU指令。而HWUI真正的渲染后端是Skia,Skia负责把画布相关的操作,比如drawRect、drawBitmap、drawText,转换为具体的GPU绘制指令。
这个过程里有一个非常重要的组件叫RenderThread。老的Android版本里,UI绘制和主线程的View遍历是混在一起的,一个复杂的界面就容易把主线程拖垮。后来系统把“发起绘制命令”和“真正执行绘制命令”拆开了,RenderThread专门在后台执行渲染工作。主线程只做View的测量、布局和生成显示列表,RenderThread拿到显示列表后异步执行GPU指令。这样即使渲染耗时长,也不会完全卡死主线程的输入响应。
Skia的绘制后端有两种主要路径:一种走OpenGL ES,一种走Vulkan。传统上OpenGL ES是默认选项,因为它兼容性最好;但Vulkan后端的优势正在显现,它减少了驱动层的隐式状态管理,对多线程渲染的支持也更好。Android 16上,如果你关注过AOSP的代码提交记录,会发现Skia和HWUI在越来越多地把GL路径边缘化,Vulkan路径被当成主车道来打造。
2.3 Vulkan在系统级的位置
很多游戏开发者都已经很熟悉Vulkan了,但它在Android 16系统架构里的角色其实远不止“给游戏用”。系统自己也在用Vulkan做合成相关的加速,部分厂商的SurfaceFlinger合成路径会用Vulkan替代老的GLES合成。也就是说,Vulkan不只是一套“画画的API”,它已经成为整个图形系统性能基础的一部分。
为什么系统偏爱Vulkan?最大的原因是可控性。OpenGL ES是一个高层的状态机,驱动要处理大量的隐式同步和状态转换;而Vulkan把显式同步、命令缓冲、队列管理全部暴露给开发者,虽然用起来麻烦,但换来的是可预测的性能。对于Android这样一个需要适配大量GPU的平台来说,Vulkan能帮助减少驱动层的不确定性,对功耗和帧率稳定性都有帮助。
当然,Vulkan的缺点也很明显:学习成本高、代码量大、驱动要求更严格。所以系统里也不是所有路径都一步到位切到Vulkan,很多老设备、老应用仍然走GLES;但Android 16的趋势是明确的——新特性优先支持Vulkan,老路径慢慢退居二线。
3. BufferQueue、Gralloc与共享内存:图形数据的高速公路
有了引擎,还得有“数据管道”。一个App渲染完的画面,怎么交给SurfaceFlinger?SurfaceFlinger合成完,怎么交给屏幕?答案就是BufferQueue。这个概念你必须在脑海里扎下根,因为几乎所有图形性能问题,追踪到最后都会落到这上面。
3.1 BufferQueue的生产者消费者模型
BufferQueue(缓冲区队列)本质上是一个“生产者-消费者”模型。App是生产者,SurfaceFlinger是消费者。生产者的职责是从队列里申请一块空闲缓冲区,把内容画进去,然后把这块缓冲区提交入队;消费者的职责是从队列里取走一块已提交的缓冲区,做合成或者显示,用完后再把它还给队列复用。
这个循环里有几个关键状态:
- DEQUEUED:生产者已经拿到缓冲区,正在往里面绘制,此时缓冲区归生产者独占。
- QUEUED:生产者绘制完毕,把缓冲区放入队列,等待消费者处理。
- ACQUIRED:消费者把缓冲区取走,正在使用中。
- FREE:缓冲区处于空闲状态,可供下一次dequeue。
这四种状态之间的切换,是整个图形系统最核心的同步逻辑。BufferQueue的核心价值在于复用:内存缓冲区被反复使用,不需要每帧重新分配,这在性能上是决定性的。设想一下,如果你每帧都从hardware allocator里申请一块新内存,那光分配和释放的开销就能吃掉一大截性能。
至于缓冲区的调度策略,不同的生产者和消费者场景需求不同。普通应用窗口的缓冲区队列已经加入了“最大缓存帧数”、同步栅栏(fence)等机制,用来协调CPU和GPU的进度,避免消费者拿到一个还没画完的缓冲区。
3.2 Gralloc和图形内存分配
BufferQueue管理的是缓冲区的“生命周期”,但真正分配图形的物理内存的,是另一个核心模块:Gralloc(全称Graphics Allocator)。它对上提供一个稳定的分配接口,对下通过HAL(硬件抽象层)对接GPU和显示控制器,由厂商的驱动决定内存分配在哪块区域。
为什么不让BufferQueue直接malloc一块内存?因为图形缓冲区是给GPU、显示控制器和硬件合成器用的,它们对内存有特殊要求。比如某些内存需要连续物理地址,某些需要能从显示控制器直接读取,某些需要被CPU、GPU、DPU(显示处理单元)同时访问。普通的内存分配器满足不了这些硬件的对齐和缓存一致性要求。所以Gralloc的职责就是和硬件驱动配合,分配出满足整个图形流水线需求的缓冲区。
在Android 16的系统里,图形缓冲区还有一个非常重要的身份:它们是通过Binder共享出去的跨进程对象。App进程里的渲染线程把缓冲区填好后,系统会把这个缓冲区关联的文件描述符通过Binder传给SurfaceFlinger进程。这里面的内存共享基于内核的dmabuf等机制实现,可以做到物理内存在进程间零拷贝传递,而不是真的把像素数据复制一份过去。这也是整套系统能维持高性能的关键所在。
3.3 生产者消费者的跨进程协作细节
你可以把BufferQueue想象成一个共享快递柜:消费者把快递柜清空出来,生产者把东西放进去并锁上门,消费者再次打开取出。快递柜本身放在公共区域,双方都能访问,但取放规则由管理者制定。
这套模型还有一个容易被忽略的重点:同步栅栏(fence)。GPU在绘制缓冲区时不一定是立即完成的,CPU提交完命令就去做别的事了。如果SurfaceFlinger在GPU还没画完的时候就去读缓冲区,就会拿到乱七八糟的画面。为了解决这个问题,生产者会在queue缓冲区的时候带上一个fence对象,消费者在acquire缓冲区的时候先等待这个fence,等GPU工作完成后才开始合成。这就让CPU、GPU之间可以并行工作,而不是互相空等。
实际上,我在排查不少问题的时候发现,很多诡异的画面撕裂、花屏问题,根源往往不在渲染代码本身,而是fence的传递或者超时逻辑出了问题。后面我会专门列一些常见问题。
4. Vsync机制与帧调度:Android的“节拍器”
图形系统最终要跟屏幕的刷新节奏对齐。屏幕是60Hz就每秒刷新60次,是120Hz就每秒刷新120次。如果App想画就画、想提交就提交,你和屏幕之间就会发生错位:屏幕可能两帧显示同一内容,也可能在刷新中途换帧导致画面撕裂。Android用Vsync(垂直同步)信号来解决这个问题。
4.1 Vsync信号从哪里来,发到哪里去
Vsync信号的核心来源是显示硬件。屏幕在完成一帧刷新后,硬件会发出一个脉冲信号;这个信号会被HWC或者显示控制器接收,随后由系统转换成软件事件,分发给不同进程。
在Android 16的架构里,Vsync的分发有两个主要方向。一个方向是发给各个App进程,App收到后启动画面绘制;另一个是发给SurfaceFlinger,SurfaceFlinger收到后触发合成和提交显示。也就是说,生产者和消费者的节拍器是可以分开调节的,两个节拍器之间还可以有相位偏移,让整个流水线像接力赛一样,每个人在各自的赛道上不会互相撞车。
App进程的Vsync最终会转化成Choreographer的回调。做过Android性能优化的朋友都知道Choreographer,它负责在每一帧的固定阶段回调三种任务:第一种是输入事件处理,第二种是动画更新,第三种是View测量、布局和绘制(Traversal)。这个顺序保证了你在onDraw里看到的动画值一定是最新的。
4.2 刷新率变化与三缓冲
以前60Hz的时代,帧调度相对简单:每16.6毫秒来一次Vsync,App在这段时间里完成一帧渲染,SurfaceFlinger在下个Vsync完成合成。但Android 16时代,屏幕刷新率已经变成一个可以动态调整的变量。手机可以根据内容切换90Hz、120Hz,甚至可以降到1Hz来省电。
可变刷新率带来一个新问题:如果系统切到120Hz,但App只按60Hz来提交帧,那中间就有很多Vsync是空跳的;如果切到低刷新率但突然出现一个高帧率游戏,系统的响应速度就可能跟不上。所以现在的调度器需要根据App的行为、正在显示的内容类型、功耗预算,动态调整刷新率。这套机制既有系统侧的算法,也有App通过DisplayManager等接口上报的偏好,两边配合才能做到既省电又不卡。
还有一个和Vsync强相关的概念是“三缓冲”。老Android设备上容易掉帧,一个原因是只有双缓冲,App正在画的缓冲区、SurfaceFlinger正在用的缓冲区,两个坑被占满了,App就不得不等。后来加入了第三个缓冲区,相当于多了一个缓冲位,当瞬时负载波动时,App不至于立刻被堵死。三缓冲的本质是用增加一帧延迟换取更平滑的帧率。你在游戏帧率曲线里看到那种持续30帧但帧间时间很均匀的情况,往往背后就是缓冲深度的功劳。
4.3 Frame Pacing:让帧率更均匀的机制
Android系统其实一直在解决“平均帧率好看但实际卡顿”的问题。你可能会看到某款游戏平均60FPS,但体感却卡顿明显,这是因为帧间隔不均匀,有的帧8毫秒完成,有的帧30毫秒才完成,人眼对瞬时卡顿远比对平均帧率敏感。
系统侧的Frame Pacing(帧节奏控制)就是为了缓解这个问题。它让App不要在任何一帧上提前跑太远,也不要提交过晚,而是尽量让每帧都落在正确的Vsync时间点上。这样,渲染时间波动很大的工作负载也能表现出平滑的画面。Android 16上对这类帧调度机制做了更多细化,尤其是在游戏场景里,配合ADPF(Android Dynamic Performance Framework),App可以获取到系统的预测性帧调度信息,动态调整渲染负载,把波动抹平。
实际调优时,我建议大家多关注systrace里每一帧的提交时间和Vsync的相位差,不要只盯着FPS数字看。这个问题我在后面的排查部分还会展开。
5. 合成与显示:从SurfaceFlinger到屏幕的最后一百米
App画好了帧,BufferQueue完成了传递,接下来就是SurfaceFlinger干活和屏幕显示了。这一段的效率和效果,直接决定你看到的画面是不是顺滑、有没有色偏、HDR亮不亮、屏幕省不省电。
5.1 SurfaceFlinger的合成策略
SurfaceFlinger拿到各个Layer的缓冲区后,需要考虑怎么“合成”出一帧。合成这个词听起来技术性很强,但实际上就是决定“把哪些图层直接交给硬件,把哪些图层先用GPU混合一次”。
硬件合成器(HWC)通常能直接处理一定数量的图层。比如屏幕上有一个视频层、一个UI层、一个状态栏层,如果硬件有对应的硬件图层(Hardware Layer),SurfaceFlinger就可以让HWC直接把这些硬件图层混合并输出到屏幕。这个过程完全不经过GPU,功耗极低。但如果图层数目超过了硬件能力,或者某些效果(比如复杂的圆角、阴影、模糊)没法用纯硬件图层来表达,SurfaceFlinger就必须先把这些图层用GPU合成成一张纹理,再交给HWC输出。
这就是为什么在性能调优时,经常有人强调“减少过度绘制”或者说“减少Layer数量”。每多一个图层,SurfaceFlinger的工作量和HWC的压力都会增加。尤其是窗口动画期间,如果一个页面把多个Surface都变成了可见Layer,那些复杂效果有时会造成GPU和DPU的双重开销,掉帧非常明显。
5.2 HWC驱动的能力协商
HWC是图形系统里最容易被忽略但又最关键的模块。每一代Android系统的图形架构演进,都绕不开HWC和SurfaceFlinger的协议升级。在Android 16里,HWC依然是显示链路里负责“最后一公里”的部件:SurfaceFlinger把合成后的帧交给HWC,HWC负责把帧在正确的时间点呈现到屏幕上。
HWC和SurfaceFlinger之间有一套固定协议,SurfaceFlinger会告诉HWC当前有哪些Layer、每个Layer的buffer handle是什么、合成模式是什么、是否做翻转裁剪,以及一个关键的present fence。HWC驱动的能力各不相同,有的硬件只能处理一两个图层,有的能处理很多个,还有的只支持特定格式(比如HDR内容)。SurfaceFlinger会根据HWC的能力动态调整处理策略。
如果你做驱动或系统适配,遇到屏幕显示异常、帧率异常、休眠唤醒后花屏这些问题,第一步就是去看HWC的systrace段,确认SurfaceFlinger有没有发错layer信息、HWC有没有报错。很多“屏幕闪烁”的疑难杂症,最后都定位到了HWC和驱动之间的同步问题,而不是SurfaceFlinger本身。
5.3 显示链路、回读与色彩管理
在Android 16上,一个必须单独拿出来说的概念是色彩管理与HDR。以前Android屏幕大多只支持SDR,一套8位RGB就够用了。但现在越来越多的屏幕原生支持HDR10、HDR10+甚至杜比视界,App也开始请求使用广色域和HDR渲染。
图形系统在显示HDR内容时,需要做好几件事:一是保证HDR内容在渲染时工作在正确的色彩空间,不丢失信息;二是合成多个图层时,SDR跟HDR内容要正确混合,不能让SDR内容看起来发灰或者过曝;三是由HWC在输出到屏幕时,根据屏幕能力做色调映射(tonemapping)。在Android 16的系统架构里,色彩管理已经渗透进了BufferQueue、SurfaceControl和HWC协议中,一个Layer在创建的时候,就需要明确声明它的色彩空间和HDR元数据。
这块调起来其实有不少坑,最常见的现象是:HDR视频播放时,旁边的UI文字显得过亮刺眼,或者整体画面发白。这就是因为SDR和HDR内容在合成时没有做正确的混合处理,或者驱动对元数据支持不完整。遇到这种问题,光靠上层改代码解决不了多少,必须沿着Data Space、HDR metadata、HWC能力协商这一条链路往下查。
6. 跨进程协作:从SystemServer到App再到HAL
图形系统不是独立运行的,它跟SystemServer里的一堆系统服务、App进程的UI线程、硬件驱动之间有着频繁的交叉调用。理解这些跨进程关系,是真正看懂Android 16图形架构的最后一环。
6.1 WindowManager、DisplayManager和SurfaceFlinger之间的分工
你打开一个App的时候,屏幕上的窗口是怎么建立的?首先是Activity经过AMS(Activity管理服务)和WindowManager(WMS)确认窗口属性,WMS创建对应的WindowState,并通过SurfaceControl接口向SurfaceFlinger申请创建对应的Layer。SurfaceFlinger返回一个SurfaceControl的句柄,App端拿到这个句柄后,才能跟对应的缓冲区队列建立连接。
DisplayManager则负责管理屏幕设备。一个手机上可能有主屏、副屏、外接屏,Android 16上还支持折叠屏多个显示状态。DisplayManager负责枚举显示设备、管理DisplayDevice,并和SurfaceFlinger的Display模块协同,决定每个Display上显示哪些Layer。
这里的关键是:所有跨进程通信都通过Binder完成。SurfaceFlinger拥有真正的Layer数据,其他进程持有的只是控制句柄,真正修改图层属性、提交缓冲区的操作,都要通过Binder调用到SurfaceFlinger进程完成。这也意味着,如果Binder调用过于频繁,或者SurfaceFlinger进程被某个耗时操作拖住,整个系统的UI都会受到影响。
6.2 三进程协作的典型流程
我们以最简单的一个按钮点击为例,走一遍跨进程协作流程。
- 点击触摸屏,InputFlinger把触摸事件发给App的主线程。
- Choreographer在下一个Vsync回调中触发View的重绘。
- HWUI把绘制任务提交给RenderThread,RenderThread通过Vulkan或GLES完成GPU渲染。
- 渲染结束后,缓冲区入队到BufferQueue,同时向SurfaceFlinger发送一个Binder通知。
- SurfaceFlinger收到通知后,在下一次Vsync时,把这张新缓冲区跟其他窗口的缓冲区进行合成。
- HWC按屏幕刷新率把最终画面呈现到面板上。
这个过程涉及Input、App进程、GPU、BufferQueue、SurfaceFlinger、HWC等多个模块,任何一环出现延迟,都会反映成用户可感知的卡顿。在日常优化里,我喜欢用Perfetto去抓一整段流程,然后逐段看时间消耗:输入分发用了多久,App draw用了多久,GPU渲染用了多久,SurfaceFlinger合成用了多久,HWC presentation等了多久。找出时间都花在哪一段,问题就解决了一大半。
6.3 与近期系统架构热点的关系
最近“系统架构设计师”这个方向在技术社区讨论度很高,其实Android图形系统就是一个很好的案例:它具备一个大型分布式系统该有的分层、模块解耦、同步机制、故障隔离和性能工程,而且你可以直接看源码、直接抓trace验证,是学习系统架构不可多得的好素材。甚至你在研究分布式交换机、低空管控平台这类大规模系统的架构时,里面遇到的调度、负载均衡、生产者消费者模型、背压控制等问题,在Android图形栈里都能找到对偶的对应。
从这个角度看,搞懂Android图形架构,不只是为了调一个App的流畅度,它培养的是你对“复杂系统如何组织”的判断力。
7. 常见问题与排查技巧实录
最后分享一些我在实际开发中经常遇到的问题和排查思路。这些坑在官方文档里很难找到现成答案,但几乎每个做图形优化的人都会碰到。
7.1 App数据流卡顿,不知道该看哪里
这个场景最常见。游戏帧率不稳定,滑动列表掉帧,一开始大家都会去查App自身的代码,改布局、优化图片加载,但改了半天效果有限。我的经验是:先抓Perfetto,不要瞎猜。Perfetto里能看到App的Vsync回调时间、绘制耗时、GPU队列排队情况、BufferQueue的状态变化、SurfaceFlinger合成耗时和HWC的present时间。
拿到数据后按这几条原则排查:
- 如果App的Draw和RenderThread耗时都很低,但SurfaceFlinger的合成间隔不均匀,问题很可能在系统侧,要么是Layer太多,要么是HWC压力大。
- 如果App的Vsync回调本身就频繁延迟,那要回去看主线程的message queue,或者看Choreographer的callback耗时,多半是应用逻辑把主线程抢占了。
- 如果GPU时间很长,先用GPU Inspector或者systrace里的GPU track定位是哪个pass耗时,再决定是减少绘制指令还是降低特效等级。
7.2 帧率正常但显示内容卡顿
这听起来很矛盾,但其实很常见。比如你跑一个60帧的App,SurfaceFlinger合成步骤也正常,但画面就是一顿一顿的。这时候要重点检查帧间隔到底是不是均匀的。我对团队里的新人常说一句话:比起帧率,帧间隔更能反映真实体验。有的设备会在某个时间段强制切到低刷新率节奏,导致App和屏幕的Vsync阶段错位,App画好帧后要等好几个Vsync才被present出去,表现就是“有时候突然卡一下”。
遇到这种问题,先在Pefetto里对比App的Vsync和HWC的Vsync是不是同一个节奏,再看设备的刷新率切换策略是否过于激进。比如某些应用场景下,系统为了省电会把120Hz切成60Hz,但对游戏来说这个操作本身就是灾难。
7.3 黑屏、闪烁和花屏问题
这类问题通常不是上层代码的问题,而是要往硬件合成和内存同步的方向查。我遇到过不少黑屏案例,最后都归宿到HWC被给了一个未完成GPU写入的buffer,也就是fence没等对;还有的是Gralloc分配的内存被错误复用,缓冲区还没读取完就被重新dequeue去写,导致花屏。
排查这类问题时,强烈建议抓一下HWC的trace和驱动日志,确认有没有明显的错误事件。如果是自研设备,最好找驱动工程师一起看HWC协议交互,很多问题其实出在HAL层的能力协商。上层应用开发者遇到这类问题,可以尝试关闭硬件层合成(强制走GPU合成),如果症状消失,基本可以判断是HWC或者驱动那一段的问题。
7.4 值得记住的三个排查心得
第一,图形问题“动胜于猜”。不要凭感觉改代码,先把trace抓出来,用数据定位到具体模块,再去看代码。第二,同步和fence问题是最难复现也最难定位的,一定要保留现场信息,抓trace时把fence相关的dump也一并抓下来。第三,很多“疑难杂症”其实是多帧累积的调度问题,只看单帧截图是看不出来的,必须看一段连续时间线上的行为。
我个人在实际操作中最深的体会是:Android图形系统的性能瓶颈,往往不是某一个模块不够快,而是模块之间的衔接节奏没有对齐。你能把BufferQueue的状态、Vsync的相位、SurfaceFlinger的合成间隔这几个点对上,就已经超过大多数人了。新版系统更新再频繁,这套底层逻辑也不会变,花时间把基础架构啃透,是回报率最高的投入。