ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android显示链路全解析:从View绘制到SurfaceFlinger合成

Android显示链路全解析:从View绘制到SurfaceFlinger合成 做Android开发久了你会发现很多疑难杂症归根结底都指向同一个地方——显示链路。无论是应用卡顿、掉帧还是黑屏、花屏甚至Surface报错只要你对这条链路没有建立起完整认知排查起来就像在黑屋子里找开关全靠猜。我写过很多性能优化的专项文章但一直缺一篇真正从头到尾讲清楚“一帧画面从App到屏幕到底走了多少路”的内容。这篇就是第1篇目标很明确用一张图带你看懂Android显示完整链路并且把每一段接力中涉及的核心角色、关键机制、常见坑一次讲透。这篇文章适合三类人看应用开发者想弄明白自己写的代码为什么卡framework工程师想厘清View、Surface、SurfaceFlinger之间的协作关系做系统优化和性能测试的同学想建立一份能直接指导排障的链路地图。我会尽量少讲理论空转多讲“这段在系统里到底怎么运转”的实在内容。1. 先画一张总览图Android显示链路的全貌1.1 从一次触摸到屏幕像素数据要经历五个接力区我先给一条概括性链路这也是我每次带新人必画的图。你可以把它当成全文的地图后面的章节全部围绕这条链展开App UI线程 App渲染线程 系统合成服务 硬件设备 [measure/layout/draw] - [DisplayList/RenderThread] - [Surface/BufferQueue] - [SurfaceFlinger] - [HWC/Display]如果展开到关键类链路大致是这样ViewRootImpl - DecorView.dispatchDraw() - HardwareRenderer / RenderThread - DisplayList / RenderNode - Gralloc buffer - Surface.queueBuffer() - BufferQueue - SurfaceFlinger - HWC setLayerBuffer present() - DSI/DP - 屏幕像素注意上面这条线里App进程并不是直接往屏幕上写像素的。View体系做的是“生成画面的内容”真正把像素放到屏幕上是系统服务SurfaceFlinger完成的。这也是Android显示链路里最重要的一句话App只负责生产帧系统负责消费帧硬件负责展示帧。“生产者-消费者”关系贯穿了整条链路。1.2 四个核心角色一张表看懂我习惯用表格来记链路里的核心角色因为它们的所属进程、职责边界太容易混了。总结如下角色所属进程/上下文核心职责关键类/接口View体系App进程UI线程为主测量、布局、绘制生成绘制指令ViewRootImpl、View、CanvasRenderThreadApp进程独立线程把绘制指令转化为GPU命令执行RenderNode树渲染HardwareRenderer、RenderNodeSurface / BufferQueueApp进程与SurfaceFlinger共享传递帧缓冲实现生产消费解耦支持多缓冲SurfaceControl、BufferQueueProducerSurfaceFlingersystem_server进程合并所有App图层、系统UI图层并按Z-order合成一帧SurfaceFlinger、LayerHWCHardware Composer系统服务HAL把合成后的图层交给显示硬件负责时序和帧同步HardwareComposer HAL、Composer HAL画完这张表你再看行业里的名词——比如“走客户端合成还是走设备合成”“图层是否overlay”“BufferQueue有几层缓冲”——都是在说这条链上某个环节的协作方式。链路本身不变变的只是每一站怎么交接。2. 应用侧UI线程怎么把画面画进Buffer2.1 measure/layout/drawView的“测量-排布-绘制”三件套很多应用开发者对卡顿的第一反应是“draw太重了”但实际上ViewRootImpl触发的一帧是从performTraversals()开始的它会依次执行三大流程measure、layout、draw。measure决定每个View要占多大尺寸layout决定子View放哪里draw才真正把内容画到Canvas上。这三步的消耗性质完全不一样。measure里如果嵌套权重复杂连续多次requestLayout代价是遍历整棵View树重新算尺寸这是我在实际项目里见过最多隐藏卡顿的原因——明明没改布局仅仅因为某个输入框弹出键盘就引发了大范围的re-layout。draw则分两种如果走软件绘制就是同步在UI线程里逐个执行onDraw基本没办法避让丢帧如果走硬件加速UI线程只负责构建DisplayList真正的执行放到RenderThread里下一小节细说。这里有个判断技巧在dumpsys gfxinfo的输出里measure/layout耗时对应layout字段draw耗时对应record字段DisplayList执行对应sync和issue字段。如果看到layout字段明显偏高先去排查requestLayout调用次数和布局层级而不是盲目优化View的绘制。2.2 硬件加速与DisplayListRenderThread才是真正干重活的人Android从3.0开始引入硬件加速从4.0之后几乎全面接管了View的绘制。硬件加速的基本逻辑是UI线程在draw阶段不直接执行Canvas操作而是把这些操作记录成RenderNode和DisplayList相当于一份“绘制指令清单”。指令清单交给RenderThread由它负责调用GPU完成真正的栅格化。这个设计解决了一个核心矛盾UI线程既要处理输入事件又要跑业务逻辑如果绘制耗时都压在UI线程里那帧率基本锁死在应用代码的复杂程度上。有了RenderThreadUI线程只做与业务相关的部分——测量、布局、生成指令——耗时的纹理上传、矩阵变换、绘制指令提交全部移到了渲染线程。不过这里有一个很常见的坑不是所有的Canvas操作都能被延迟执行。如果你在onDraw里调用了Canvas.saveLayer()、读取像素的getPixel()、或者View.setLayerType(LAYER_TYPE_SOFTWARE)硬件加速会把路径回退到软件绘制导致RenderThread的优势直接消失。加上很多同学喜欢在onDraw里new对象比如创建Paint、Path虽然现在Android的Canvas已经做了不少优化但高频绘制路径里仍然会出现明显的分配抖动。我的经验是能在构造阶段准备好的资源绝不在onDraw里创建能用invalidate()局部更新绝不整棵View树重绘。另外要补充的一点是绘制指令的生成也不是绝对轻量。当某个View树层级特别深或者一个页面有几十个View同时失效仅遍历和记录指令的耗时就能超过好几毫秒。这也是官方一直推荐扁平化布局、减少无效invalidate的原因。2.3 从draw到enqueueSurface和BufferQueue的交接应用侧绘制的终点并不是“屏幕”而是Surface。Surface本质上是App进程往BufferQueue里投递帧缓冲的入口它背后连接的是BufferQueueProducer。一次完整的提交流程大致是这样应用在准备绘制前从BufferQueue中请求一块空闲bufferdequeueBuffer()。绘制引擎Skia/OpenGL/Vulkan把内容渲染到这块buffer上。帧内容写完后调用queueBuffer()把buffer还给BufferQueue同时带上时间戳和栅栏Fence。BufferQueue通知Consumer通常是SurfaceFlinger有新的buffer可以消费。这一套流程里最容易被忽略的是buffer数量。默认情况下BufferQueue通常分配2到3个buffer。如果App的绘制速度跟不上消费速度dequeueBuffer()就可能阻塞表现出来就是应用的渲染线程卡在“等待buffer”上。反之如果SurfaceFlinger消费速度慢于App生产速度会出现buffer堆积、老帧无法及时释放的问题很多“显示延迟”就是这么来的。实操时可以用一个命令快速查看某App的buffer状态adb shell dumpsys SurfaceFlinger --list adb shell dumpsys SurfaceFlinger --latency layer-namedumpsys SurfaceFlinger --latency会输出最近帧的时间戳每行包括刷新周期内的desiredPresentTime、actualPresentTime、frameReadyTime。通过对比这三个时间能判断帧是App侧开销大还是SurfaceFlinger侧合成慢。后面排障章节再展开。3. 节奏控制VSYNC、Choreographer和BufferQueue3.1 VSYNC显示器的“心跳”决定了所有人的步调屏幕并不是一帧帧随意刷新的它有自己的固定节奏这个节奏就是VSYNC垂直同步信号。传统LCD屏每刷新完一帧会产生一个同步脉冲GPU和SurfaceFlinger都跟这个脉冲对齐避免出现画面撕裂。Android把VSYNC当作全局节拍器所有生产者都要按这个节拍生产帧消费者按同一个节拍做合成和提交。对App来说VSYNC的意义在于UI线程不需要每帧都主动触发绘制而是等待VSYNC回调统一驱动。这样做的最大好处是避免一帧内多次重复绘制减少电量消耗和无效工作。Android系统里负责分发这个节拍的是Choreographer和底层的VSYNC信号源。这里有个知识点值得记住现在的手机普遍支持高刷新率60Hz、90Hz、120Hz甚至更高。切换刷新率不仅仅是屏幕参数变化整个显示链路的节奏周期都会变包括VSYNC周期、App的帧回调间隔、SurfaceFlinger的合成间隔。以前很多应用写死了“16.6毫秒一帧”的假设到了120Hz手机上就会出现动画明显加快、定时器不准的问题。正确的做法是跟着Choreographer.getFrameIntervalNanos()拿动态周期而不是用常量。3.2 ChoreographerApp端的“节拍器”如何安排三类回调Choreographer是App进程里的节拍器。每次VSYNC到来时Choreographer会依次执行三类回调input、animation、traversal。输入事件处理优先动画回调次之最后才是View树遍历与绘制。这个顺序不是随便排的它保证了用户的触摸事件能最快反映到UI更新上。我见过不少同学把Choreographer.FrameCallback当作替代Handler.postDelayed的优化手段这本身没错但要注意利用Choreographer做消息调度时要注意申请和释放FrameCallback的时机。如果在每一帧都重复注册一个回调会让UI线程的工作量翻倍因为一个VSYNC周期内每个回调都可能触发一次doFrame。而且Choreographer的回调只能在UI线程运行。调试掉帧时用Systrace或者Perfetto抓Trace后看Choreographer#doFrame和android.view.ViewRootImpl#draw之间出现的大段“locks”或者“Input”基本就能判断是输入事件耗时太长还是动画回调抢占了帧预算。有一段高亮区间如果在UI线程上占了接近10ms那你其实已经消耗掉60Hz下的一多半帧时间了。还有一个很实际的经验尽量减少在UI线程上做同步IPC或IO。比如SharedPreferences的commit、Binder跨进程读取大文件这些都在Choreographer的帧回调窗口内执行的话会直接挤占绘制时间。很多卡顿并不是绘制本身慢而是UI线程的帧时间预算被业务逻辑吃掉了。3.3 BufferQueue生产消费模型与三重缓冲的意义讲到BufferQueue我用一个生活化类比帮助理解它像一个快递柜。生产者App把包裹帧缓冲放进柜子消费者SurfaceFlinger/HWC从柜子取走上屏。快递柜里的格子数量就叫“缓冲深度”。只有1个格子时生产者还没走消费者就必须把格子腾空两者强同步效率极低2个格子时生产者和消费者可以错开一定节奏3个格子时即我们常说的“三重缓冲”生产者和消费者之间有了更大的弹性空间。三缓冲为什么能减少掉帧因为当App某帧渲染超时如果只有双缓冲dequeueBuffer通常会卡住等消费者释放bufferUI线程等待进一步拖延下一帧。三缓冲多了一个备用格子渲染超时对消费者乃至屏幕刷新率的影响会被缓冲掉App更多时候能够“用提前产的帧掩盖偶尔慢一帧的抖动”。但这也不是越多越好。缓冲数量增加会增加内存、带来显示延迟。Android在大多数场景下用2~3个buffer做默认配置。系统会依据当前App的实际渲染速度动态调整buffer数量所以你会看到dumpsys SurfaceFlinger里同一Layer的BufferQueue“maxBufferCount”可能变化。这里顺便提醒一句如果App端发现eglSwapBuffers或者vkQueuePresentKHR耗时异常高去掉BufferQueue的分析再查一下是否使用了过多的SurfaceView或者叠加层因为每个独立Surface都意味着一个独立的BufferQueue和合成层级。一个界面有多个可实时刷新的Surface比如视频播放区单独用SurfaceView、WebView又单独一个合成压力就会明显上升。4. 系统侧SurfaceFlinger与HWC接过接力棒4.1 SurfaceFlinger把所有图层叠成一张画当App把buffer投递给BufferQueue之后真正决定“这一帧长什么样”的是SurfaceFlinger。它在每次VSYNC到来时把当前屏幕上的所有可见图层Layer按Z-order排序然后做最终的合成。注意这里说的“合成”不是在说把bitmap直接搬一搬。合成的意思是把多个图层的buffer最终叠加生成一帧屏幕画面既要处理透明度、裁剪区域、颜色转换还要考虑GPU还是硬件设备来做。SurfaceFlinger的合成有两种路径客户端合成Client CompositionSurfaceFlinger使用OpenGL ES或Vulkan在GPU上把所有图层合成为一张纹理再送显。设备合成Device Composition通过HWC直接把多个图层buffer交给显示控制器由硬件在扫描输出时完成混合效率更高。系统会根据图层数量、格式、是否带圆角/模糊等属性动态决定走哪条路径。能走设备合成时SurfaceFlinger的负载很小一旦图层属性不满足硬件条件比如某个App开启了屏幕圆角遮罩、全屏模糊就可能回退到客户端合成GPU负载立刻飙升这也是“某层加了个模糊特效导致整机掉帧”的典型原因。我自己排查这类问题时必看dumpsys SurfaceFlinger --debug输出里每个Layer后面的compositionType标志。如果看到大量CLIENT就要盯一下是不是有App在持续更新高分辨率图层或者是否开启了过多带特效的窗口。4.2 HWC硬件合成器的角色和Fence同步HWC是连接SurfaceFlinger和显示硬件之间的纽带它不是“可选项”而是标准组成。HWC以HAL形式运行SurfaceFlinger把待显示的图层列表和buffer handle传给HWCHWC根据硬件能力决定如何合成并返回present的时序和同步栅栏Fence。Fence这个概念在显示链路里很关键它用来同步GPU和显示控制器的工作进度。比如App在queueBuffer时提交了一个Fence表示“这块buffer里的绘制命令还没完全执行完”SurfaceFlinger拿到buffer后不会立刻上屏而是等Fence信号到达才使用。如果Fence等待超时则会出现画面停滞但应用不报错的诡异现象。我做稳定性分析时遇到不少“panel stuck”类的问题根因就是某个buffer的Fence长时间没有signal。分享一个小技巧用adb shell dumpsys SurfaceFlinger --latency看数据时如果frameReadyTime之后隔了非常久才到actualPresentTime大概率问题出在HWC/present阶段而不是App绘制阶段。区分“App慢”还是“系统慢”是性能排障的第一分水岭。4.3 刷新率切换、动态帧率和多窗口链路如何应对变化链路不是一成不变的。现代手机支持多档刷新率系统会根据当前画面类型自动调整看静态页面时降到60Hz省电滑动或游戏时升到120Hz提升流畅度。这个调整机制分散在多个进程中窗口系统根据焦点窗口的“frame rate preference”向上层申请SurfaceFlinger根据运行状态改变合成的VSYNC周期最后显示驱动配置实际刷新率。帧率切换中最常见的问题是“切帧抖动”从60Hz跳到120Hz瞬间如果App侧没有及时适配帧回调周期会出现动画跳变。反过来如果App一直请求高刷新率会给整条链路带来持续的高负载这也是很多“莫名发热”的来源。做检测时可以用dumpsys window | grep mFrameRateOverride查看窗口层面期望的刷新率再对比实际确认系统是否在按预期调度。多窗口和小窗模式下每个可见Surface依然有独立的BufferQueueSurfaceFlinger则会按照窗口变换矩阵做几何投影。一个常见的坑是在分屏或画中画模式下某些隐藏Surface未及时销毁依然在持续提交帧白白消耗合成资源。排查方法很简单dumpsys SurfaceFlinger --list里如果看到很多不存在的窗口Layer那就是有Surface泄漏了。5. 排障实践从掉帧日志到演示数据5.1 一条命令看清当前显示状态dumpsys SurfaceFlinger许多开发者觉得显示链路很黑盒其实调试入口非常直白。第一招就是dumpsys SurfaceFlinger它返回的信息能让你快速了解当前合成状态。常用子命令adb shell dumpsys SurfaceFlinger --list # 列出当前所有Layer adb shell dumpsys SurfaceFlinger --latency name # 查看某Layer的帧时间线 adb shell dumpsys SurfaceFlinger --debug # 查看合成方式与状态 adb shell dumpsys SurfaceFlinger --timestats # 查看合成耗时统计拿--latency来说输出每一行有三个关键时间戳desiredPresentTime是期望上屏时间actualPresentTime是实际上屏时间frameReadyTime是帧内容准备好Fence signal的时间。把它们和VF周期对比就能算出App侧提交延迟和整体呈现jank。如果actualPresentTime - frameReadyTime特别大说明SurfaceFlinger或HWC侧处理慢如果desiredPresentTime和actualPresentTime差异巨大说明App侧节奏错位。这个命令在我做系统移植、判断HDMI投屏卡顿、双屏异显兼容性问题时属于万能起手式。建议做显示优化的同学把它背下来先看这个再决定要不要上大数据工具。5.2 用Systrace/Perfetto定位那多出来的3毫秒定位到具体嫌疑进程之后深度分析靠Systrace老工具或Perfetto新工具。Perfetto能抓GPU activity、surfaceflinger合成、VSYNC调度也能看到渲染线程里的每段函数耗时。抓取方式# 抓取5秒trace perfetto -o /data/local/tmp/trace.perfetto-trace \ -t 5s \ sched freq idle atrace gfx view wm am hal_app \ gpu_mem gpu freq分析时我重点看三段Choreographer.doFrame到DrawFrame之间的UI线程任务确认业务逻辑有没有挤占帧预算。RenderThread上的syncFrameState、uploadTexture、draw耗时确认是否有纹理上传/GPU同步问题。SurfaceFlinger进程的Composite和HWC present耗时判断系统合成侧开销。我经历过一个典型案例某低端机上列表滑动掉帧Trace里UI线程很干净RenderThread也不算重但SurfaceFlinger的合成时间高达10ms。最后发现是暗色主题高斯模糊叠加层触发了客户端合成把图层特效改为硬件支持的纯色遮罩后掉帧立刻消失。这类问题只看App侧根本找不到答案必须靠链路视图整体分析。5.3 高频踩坑BufferQueue溢出、Surface abandoned和黑屏实战中大家遇到最多的报错和现象主要有下面几类我整理成速查表现象/日志通常原因快速排查方向BufferQueue has been abandonedSurface被销毁后仍有人尝试dequeue/queue检查SurfaceView释放逻辑避免异步线程持有SurfaceDequeueBuffer timeout缓冲满且consumer未及时释放或出现卡渲染查看SurfaceFlinger latency判断是消费端瓶颈EGL_BAD_SURFACESurface无效或已销毁排查EGL context和Surface生命周期页面显示一片黑HWC present失败、Fence超时、或合成路径异常查看logcat中HWC错误抓取dumpsys SurfaceFlinger拖动卡片掉帧但CPU/GPU都不忙合成路径回退到客户端合成检查图层是否带不支持的特效局部花屏/闪烁缓冲区复用时内容未同步Fence未正确等待查看GraphicBuffer分配和Fence状态其中“黑屏”是最吓人的。一次线上反馈部分机型启动某个拍照相关的页面直接黑屏绕了很多弯路才定位到是HWC HAL在某种图层格式下返回了错误导致SurfaceFlinger无法正常present。排查黑屏时我通常会先看logcat里有没有HWComposer或DisplayDevice的错误同时用adb shell dumpsys SurfaceFlinger --debug看各Layer当前的visibleRegion和activeBuffer如果Layer其实存在但显示不出来问题往往就在HWC或panel驱动层。5.4 日常自查清单我自己跑性能优化时必看的三板斧虽然链路长但日常优化其实有明确优先顺序。我给自己定了个“三板斧”流程每次排查卡顿问题都从这三步开始第一步查布局层级和过度绘制。打开开发者选项 - 调试GPU过度绘制看页面上有多少红色区域。如果大面积红色那Draw阶段已经超预算了先去拆布局、合并层级。第二步抓一次滑动场景的帧时间。用gfxinfoadb shell dumpsys gfxinfo package framestats重点看Janky frames占比和FrameDuration是否普遍超过VSYNC周期。这一步能快速区分是应用整体卡还是局部卡。第三步用Perfetto定位到具体环节。UI线程慢、RenderThread慢、SurfaceFlinger慢、HWC慢四个可能性要分清。我见过太多同学一卡就优化布局但实际是SurfaceFlinger的合成阶段被某个特效拖住。链路思维的意义就是先定位环节再动手优化。还有个小习惯我会定期清点App进程里存活的Surface数量。异常增多的Surface往往意味着窗口或者动画资源没释放长期积累下来不仅是内存问题还会拖累SurfaceFlinger合成效率。6. 再说些实际经验写了这么多还是要回归到工程直觉。真正理解Android显示链路后你对性能问题的敏感度会完全不一样看到requestLayout你会想到measure开销看到SurfaceView你会想到额外的BufferQueue看到模糊遮罩你会想到合成回退看到VSYNC时间戳对不上你会想到App侧和系统侧的节奏错位。这种“由因推果”的能力只有建立在链路全貌上才可能获得。另外分享一个我排查时很受用的小技巧遇到显示类问题时第一件事不是看代码而是先抓一份dumpsys SurfaceFlinger --latency和Perfetto数据。只要有一份完整的时间账本链路中任何一站的拖延都会客观地暴露出来。别急着靠臆测猜原因显示链路足够复杂让数据说话永远是最高效的方式。最后想说的是Android显示链路的细节远不止这些。如果这篇反响还可以我接下来会继续拆解VSYNC信号的跨进程传递、HWC HAL的调用流程、以及三缓冲在真实设备上的动态调节规则把链路上的每一站做深做透。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进