ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android显示链路全解析:从App绘制到屏幕刷新的四站旅程

Android显示链路全解析:从App绘制到屏幕刷新的四站旅程 1. 先搭骨架一条帧数据到底走了哪四站做Android性能优化或者系统开发的人十有八九都被一个问题拷打过我UI上明明改了颜色屏幕上那一块像素到底是怎么变红的点一下屏幕到画面刷新中间隔了层了什么神仙操作我把这些年的track和调试经验重新梳理了一遍整理成了一条完整链路四个站点每一站都有独立的角色和分工搞懂它们你再回头看那些掉帧、黑屏、多屏适配的问题基本一眼就能锁定凶手。1.1 链路全景图App、SurfaceFlinger、HWC、屏幕的四级流水线整条链路本质上是一条产线一头是App进程里的UI代码另一头是物理显示屏。中间经过四个核心站点。第一站是App进程。这一站负责最开始的原材料加工把你写的Kotlin/Java代码把XML布局把各种Drawable全部变成一张又一张的位图数据。注意这里的位图还只是躺在内存里的RGB数组它会被塞进一块叫Surface的缓冲区域里。这一站的关键角色是Choreographer、ViewRootImpl、RenderThread。第二站是系统级的SurfaceFlinger。这是一个独立的系统进程负责采买质检App把画好的缓冲数据通过Binder扔给SurfaceFlingerSurfaceFlinger拿着所有App窗口的缓冲数据结合窗口的Z轴顺序、透明度、裁剪区域把多张图层Layer合成成一张最终要上屏的画面。所有窗口的Layer都在这个进程里被统一管理任何一个App崩溃掉帧都不会直接弄花别的窗口就是靠这一层挡住的。第三站是Hardware ComposerHWC。这里可以理解成代工厂调度中心。SurfaceFlinger虽然合成了画面但具体用GPU合成还是硬件模块合成是直接送显还是先回读这取决于HWC给出的排产方案。HWC是硬件厂商实现的它最清楚底层屏幕上那几百条扫描线现在什么状态。第四站才是物理显示屏。LCD屏需要背光和液晶偏转OLED屏需要像素自发光这都属于硬件电路的事了。驱动把HWC送来的帧数据按行扫描刷到屏幕上这个过程受刷新率控制——60Hz就是每秒刷60帧120Hz就是每秒刷120帧。这四站连起来就是App画图 → SurfaceFlinger合成 → HWC调度 → 屏幕显示。你后续看任何关于Android显示的文章、源码、trace百分之九十九的概念都能归到这个四站模型里。1.2 一次帧刷新从触发到上屏的完整时间线很多人对这条链路懵不是因为不知道那几个名词而是不知道一次完整的刷新事件在时间上是如何串起来的。我用一次手指滑动列表的场景来走一遍时间线。某个瞬间触屏事件到达App主线程事件处理中调用invalidate()但真正的绘制并不是马上开始的。App侧的调度中枢Choreographer在下一个Vsync信号到达时才会回调doFrame然后触发View的测量、布局、绘制。这里的关键是Vsync像一个发令枪所有App的绘制都是跟着它同步起跑的而不是谁想画就画。doFrame里完成的主线程工作是把View树中所有需要更新的节点生成DisplayList显示指令列表注意这里还没有真正执行GPU绘制。之后主线程把DisplayList交给RenderThreadRenderThread通过OpenGL/Vulkan API把它转成GPU指令真正调用硬件渲染渲染输出的颜色数据写到Surface对应的Buffer里这一步叫dequeueBuffer和queueBuffer。紧接着App通过Binder调用SurfaceFlinger的commitTransaction把这个Buffer连同元数据一起提交。SurfaceFlinger收到后会等待所有参与该帧合成的layer都提交完成然后根据Vsync信号触发合成任务。合成结果是给到HWCHWC决定哪些Layer直接交给硬件混流器叠加哪些丢给GPU做处理。最后帧数据到达显示驱动等待下一个物理刷新周期扫描上屏。从手指滑动到像素更新时间上大概要经过2到3个VSYNC周期。60Hz设备上约33ms到50ms这也是为什么总有人说App端动画画得再好系统一个Vsync错过就多等一帧——这句话背后就是这个时间线。2. 第一站App端是怎么把UI变成像素的2.1 View树的三角色测量、布局、绘制每个Activity窗口的内容根本结构是一棵以DecorView为根的View树。画这棵树不是一次性完成的而是每帧重复三遍角色扮演测量Measure、布局Layout、绘制Draw。测量阶段根ViewDecorView拿着MeasureSpec——它包含了父View给的尺寸约束——遍历整棵树。每个子View根据父约束和自身layout_width/layout_height算出自己想要的尺寸。这个过程我提醒一句测量是自下而上确定尺寸需求父View把约束往下传子View把期望尺寸往上报。很多人一开始理解反了导致自定义View老是写出测量死循环。布局阶段则是自上而下分配位置。父View拿到所有子View测量后的尺寸把它们逐个摆放通过layout(l,t,r,b)确定每个View的最终坐标。到这一步所有View的位置和大小都定了。真正让屏幕显示内容的是绘制阶段。每个View调用onDraw往Canvas上画东西但是要注意这里的Canvas是有状态的画的内容不会立刻变成像素。它变成的是一堆绘制指令被记录成DisplayList节点。比如你画了一个圆角矩形DisplayList里记录的是drawRoundRect(…)这条命令而不是那一块的像素数组。这是整个渲染体系性能好的根基——指令可以缓存可以重放还可以被GPU直接消费。绘制顺序也有讲究先画背景再画自己然后递归画子View最后画前景foreground和滚动条。如果你的页面有重叠区域后绘制的会盖住先绘制的这是View和View之间遮挡关系的来源。2.2 DisplayList与RenderThread渲染移出主线程Android 5.0之前View的绘制指令会在主线程直接通过OpenGL提交给GPU主线程一忙页面立刻卡成PPT。5.0之后引入了RenderThread这就是一个把渲染从主线程摘出去的经典设计。主线程完成View树的测量、布局、绘制指令生成之后DisplayList被同步给RenderThread。RenderThread常驻运行它拿到DisplayList后把它翻译成真正的GPU命令调用底层图形库Skia/OpenGL/Vulkan执行绘制并提交帧缓冲。这样一来主线程画完就立刻可以去处理下一次触摸事件、动画或者布局而GPU侧的实际渲染开销被RenderThread吃掉。这条设计极其重要它决定了你在主线程里执行invalidate()的代价其实很低真正的开销在RenderThread里。用Systrace或者Perfetto抓trace时主线程那一排的Choreographer#doFrame段只是指令生成后面RenderThread里的DrawFrames才是实际渲染。很多日常开发遇到的卡顿打开trace一看主线程很轻松、RenderThread反而占了满满一条就是渲染指令太复杂比如超大Bitmap模糊、复杂阴影、嵌套层级太多。这时候优化思路就要放到降低绘制指令复杂度上。2.3 Surface与BufferQueue应用和系统之间的缓冲通道画好的指令经过GPU执行后像素到底写到哪答案是写到Surface背后的Buffer里。每个窗口都对应一个SurfaceSurface内部是BufferQueue——这名字很直白就是一个装着图形缓冲区的队列生产者App的RenderThread往里放消费者SurfaceFlinger从里面取。BufferQueue默认情况下会预先分配多个缓冲区。早期是双缓冲一个currentBuffer正在显示的一个pendingBuffer正在绘制的。如果生产者绘制速度过快两个Buffer交替不过来就发生阻塞这就是掉帧如果生产者来不及填充消费者取到的是旧Buffer画面重复显示这就叫丢帧但画面不撕裂。再往深一层App端的RenderThread调用dequeueBuffer从BufferQueue里拿走一个空闲Buffer用GPU渲染内容填满它。渲染完成后调用queueBuffer把它排入队列并通过Binder通知SurfaceFlinger我这边有一帧好了你拿去合成吧。SurfaceFlinger则通过acquireBuffer取回这个Buffer参与合成。所以记住这个核心模型生产者是App内部的一堆线程消费者是SurfaceFlinger中间的仓库就是BufferQueue。任何显示相关的黑屏、花屏、卡顿问题先问一句是Buffer生产慢了还是消费堵住了这一个问题能帮你砍掉一半的排查路径。3. 第二站SurfaceFlinger是怎么把多个窗口合成一帧的3.1 Layer堆叠与Z轴排序SurfaceFlinger进程里管理着一个又一个Layer每个Layer里面装着某个App窗口或系统窗口的Surface内容以及它的元数据宽高、变换矩阵、透明度、裁剪区域等。所有这些Layer按Z轴顺序叠在一起最终合成一张屏幕画面。Z轴顺序的来源是窗口管理服务WMS它说了算谁在上谁在下和焦点、Activity状态、Dialog类型都有关系。举个例子你在桌面上弹了个悬浮窗桌面Activity是一个Layer悬浮窗是另一个Layer它们的Z值不同SurfaceFlinger按Z值从低到高逐个合成。悬浮窗为什么能盖住桌面不是靠画的晚而是靠Layer在合成队列里的位置靠前。合成时的关键概念是覆盖区域一个Layer可能完全覆盖另一个那被覆盖的Layer根本不需要参与最终合成。SurfaceFlinger会做可见性计算能省则省这就是为什么你把一个不透明的Activity盖在底下那个Activity上时底层Activity的更新其实是可以被跳过的。3.2 合成策略GPU合成与硬件合成HWC的取舍SurfaceFlinger拿到一堆Layer后并不是自己撸起袖子全用GPU合成就完事了。它会先问一下HWC老哥这些Layer你的硬件模块能直接叠出来不HWC是硬件厂商提供的它知道屏幕对应的硬件混流器Overlay Engine有多少层物理叠加能力。如果Layer数量少、叠加关系简单HWC会直接说这些我全包了那么所有Layer都不进GPU直接被硬件叠加送到显示器。这种方案功耗低、性能好。如果Layer多了或者有变形、阴影、颜色变换等奇怪操作HWC只包一部分剩下的丢给SurfaceFlinger用GPU合成为一个合成好的Layer再交给HWC去叠加。这套协商机制叫合成策略选择每次帧变化时都会重新评估。开发中为什么说半透明效果、圆角背景这类操作费电就是因为它们会打破HWC的全包计划被迫走GPU合成。反过来尽量让页面不透明用硬件加速能直接叠加的路径对流畅度是有好处的。3.3 帧提交与掉帧为什么卡顿总是发生在这一层SurfaceFlinger是全网节奏的控制者之一但它有自己的苦衷。每一帧的合成必须在收到所有参与合成的layer的buffer之后才能开始而各个App提交buffer的时间是有先后的有的App在Vsync前几毫秒才提交有的因为绘制超时还没提交。这里就诞生了等帧机制SurfaceFlinger会设定一个提交截止时间超过这个时间还没等到的layer要么用旧buffer继续合成要么干脆把这帧合成整体推迟到下一个周期。等帧导致的现象就是你看到的卡顿——列表滚动时画面有可能突然跳一下因为某一帧合成晚了中间被硬生生跳过去了。排查帧问题最有效的工具是FrameTimeline或者Systrace里的SurfaceFlinger段。重点看每个Layer的queueBuffer时间点和composition时间点之间的差值。差值超过16.6ms说明这里产生了合成排队的延迟比App端掉帧更隐蔽我早年排查一个第三方输入法卡顿问题官方App侧trace干净得很最后就是定位到SurfaceFlinger的合成等待上输入法那个窗口的Layer每几帧就会迟到几毫秒把帧节奏彻底打乱。4. 第三站Vsync与刷新节奏——流畅度的底层密码4.1 ChoreographerApp侧的帧调度器Android系统里有一个心跳信号叫Vsync。硬件在每次屏幕刷新前后产生一个脉冲驱动层把它上报给系统系统再把它分发给需要同步的进程。App侧的Choreographer就是专门接收这个心跳的。当你调用了View.invalidate()其实只是向Choreographer注册了一个下一帧我要干活的callback。真正开始干活是在下一个Vsync到达时Choreographer回调doFrame依序执行三种回调首先是input事件处理触摸然后是animation动画插值最后是traversal测量布局绘制。这三步全部做完这一帧的主线程工作才算完。这个设计意味着所有App的帧节奏都被Vsync统一指挥。大家同拍起步GPU的利用率才能最大化。如果某个App在主线程里执行了耗时操作盖过了Vsync窗口——比如一个列表里做了JSON解析大文件的IO读写——等到Choreographer回调整体回调队列时已经错过好几个Vsync掉帧就是必然。所以优化App流畅度本质是优化主线程在每个Vsync周期内的执行时间。你打开Profile GPU Rendering开发者选项-GPU渲染模式分析那个条形图上的横线就是16.6ms的目标线超了就说明这个Vsync周期内工作没干完。4.2 双缓冲与三缓冲一帧到底需要几个缓冲区屏幕刷新率是60Hz时一个Vsync周期16.6ms。理论上App必须在16.6ms内完成全部绘制和提交否则buffer就供给不上。但现实中App的活儿经常超过16.6ms于是就产生了缓冲区的博弈。双缓冲初期只有两个缓冲区一个叫back buffer后台绘制区一个叫front buffer前台显示区。App在后台画画完了交换——注意Android里的交换不是推翻指针而是通过BufferQueue的dequeue/queue机制让SurfaceFlinger取走刚画完的并还给App一个空的。但如果App画一帧要20ms这就和16.6ms的刷新周期错位App偶尔会卡在dequeueBuffer上等空Buffer表现为掉帧。为了缓解这个问题Android默认在多数设备上启用了三缓冲BufferQueue里多一个Buffer。App可以连续画两帧,不必等第一个被消费完才能画第二个。三缓冲的本质是用内存换延迟它不会提升帧率的物理上限但能让偶发超时不至于立刻断流。代价是内存占用多一份全屏尺寸的像素数据。实用经验在开发者选项的调试GPU过度绘制之外,帧计时能直观看到双缓冲和三缓冲的区别。三缓冲治标不治本它只是缓冲了主线程的超时真正解决还是要让doFrame里的活少干。4.3 刷新率、帧率与掉帧的换算关系这块很多人眉毛胡子一把抓。刷新率是屏幕物理属性单位Hz它决定屏幕每秒可以刷多少帧。Android 11之前App侧的Choreographer默认跟着刷新率走。帧率是实际成功上屏的帧量单位fps。二者不相等时就出现了掉帧。假设备60Hz如果App每帧耗时都小于16.6ms那一秒正好60帧。如果有一帧耗时30ms那么它跨越了一个半Vsync周期最终结果是这一秒里只有50帧左右甚至更低掉帧10帧。注意这个掉帧不是均匀分布的而是集中在那一帧前后造成可见的卡一下。120Hz高刷屏上一个Vsync周期是8.3ms要求每帧完成时间减半这对主线程的压迫更强。但高刷不是单纯为了顺滑它更是为触控跟手度服务——触摸响应到刷新的延迟从几十毫秒降到几毫秒。做高刷适配时,要检查整个链路的耗时预算而不是只盯着App主线程SurfaceFlinger合成、HWC处理、驱动传输都占时间总延迟必须控制在单个Vsync周期内。5. 第四站从HWC到屏幕物理显示与帧的最终归宿5.1 HWC如何决定合成方案HWC这个模块平时不显山不露水但整条链路的最终决策其实在它手里。它收到SurfaceFlinger的合成请求后会去检查自己管理的硬件叠加层Hardware Overlay Planes是否足够。每一块显示器控制器都有一组物理Overlay层可以不用GPU直接把多个layer源数据叠在一起输出。如果layer数量不超过Overlay层数而且每个layer的格式、缩放、旋转参数都在硬件支持范围内HWC就会选择全硬件合成SurfaceFlinger只当一个调度者。反之如果HWC评估后认为某些layer没法用硬件叠加它会开启一个client合成槽位让SurfaceFlinger用GPU先把这些layer合成为一个纹理再把这一个结果纹理送到硬件Overlay。这个协商结果不是一成不变的会随Layer数量、窗口变换、分辨率变化而动态调整。比如你在画中画模式看电视直播时内容是缩放过的视频层Chromecast会频繁调整合成策略从硬件直接合成切到GPU合成再到混合。开发中如果发现视频场景突然GPU占用飙升大概率就是某个图层把HWC的全硬件方案给破坏了。5.2 色彩管理与Display接口帧数据到了Display层面还要过一遍颜色管理。Android 8.0之后引入了色彩管理框架App侧声明的色彩空间sRGB、Display P3等会被记录在Layer元数据里SurfaceFlinger和HWC在合成时会把它们统一转换到显示器原生的色彩空间常见的是Display P3或sRGB保证不同App的色彩不会串味。如果你的应用涉及图像编辑或视频播放别忘了在Window的ColorSpace上做声明。我调过的一个真实case某视频App在P3屏上播放HDR内容结果画面泛灰就是因为EGLSurface创建时没指定合适的色彩空间链路最末端的Display转换把它当成了sRGB来解析所有高光细节都给压没了。5.3 一图流总结把前面四站串成一张图嘴上说了这么多现在你把整个链路在脑子里画成一张图从左上角到右下角依次是App进程主线程的Choreographer-doFrame测量布局绘制DisplayList → RenderThread渲染 → BufferQueue dequeue/queue → Binder → SurfaceFlingerLayer列表和Z轴排序 → Vsync触发合成 → GPU部分合成或全硬件直接合成 → HWCOverlay协商和分配 → Display驱动 → 物理屏幕逐行刷新。再把时间线叠上去第N个Vsync触发App绘制指令生成第N个Vsync周期内RenderThread完成渲染并提交第N1个Vsync触发SurfaceFlinger合成第N1或N2个Vsync的显示刷新把真实像素扫到屏幕上。从请求绘制到肉眼看到大概走2到3帧也就是33到50ms。这张图里任何一个环节的时间超预算都会以卡顿的形式出现在用户面前。抓trace的时候按这个顺序从上往下看先看App主线程doFrame再看RenderThread然后SurfaceFlinger合成段和HWC提交段基本不会漏掉问题。6. 实战排查链路哪一环最容易出问题6.1 卡顿掉帧的定位方法卡顿排查我一般按三步走。第一步开开发者选项里的GPU渲染模式分析先看柱状图确认是主线程超时还是渲染线程超时。绿色横线是16.6ms基准柱状图超过红线的部分点击datetime能看到具体哪一帧的耗时构成。第二步抓Perfetto或Systrace。重点抓三个区域App主线程里的Choreographer#doFrame段里是否有耗时超长的子树一堆inflate、layout、drawRenderThread的DrawFrames里是否有高负载的draw指令以及SurfaceFlinger侧是否有合成等待。如果App侧干净SurfaceFlinger侧的Waiting for Layer就是元凶。第三步看DropFrame。通过dumpsys SurfaceFlinger --latency可以拿到最近帧的呈现时间戳矩阵里的pending列超过屏幕刷新率对应时间的就是卡顿帧。这个命令还能看到三缓冲的占用情况如果pending频繁接近0说明buffer饥渴。6.2 黑屏和花屏的排查思路黑屏是画面没出来链路里至少有三处可能导致App进程侧Surface没有创建成功——通常是SurfaceView或者TextureView的Surface生命周期管理出错代码里提前释放了SurfaceBufferQueue丢帧太多——画了一帧但合成端一直没收到HWC处于Idle状态——底层显示器没被点亮常见于休眠唤醒场景接错回调。花屏则和缓冲区的数据不完整或格式错位强相关。之前遇到一个OA办公App在部分ARM设备上花屏最后查到是EGL环境初始化时RED_SIZE/GREEN_SIZE/BLUE_SIZE设置和SurfaceFlinger预期的RGBA8888格式不一致渲染出来的数据在合成端解析错位直接变成花屏。这类问题先看dumpsys SurfaceFlinger里的Layer格式再对比EGL配置十有八九能对上。6.3 高刷新率下的常见坑高刷设备普及后最常见的问题是帧节奏混乱。很多App用固定16.6ms的动画插值器在120Hz设备上看起来太快了或者跳帧。正确做法是使用Choreographer的帧时间戳来计算动画进度或者使用ValueAnimator这种基于系统时钟的动画器它们天然适配刷新率。另一个坑是SurfaceFlinger掉到60Hz。部分机型在高负载的场景下会把刷新率整体切回60Hz如果App侧动画还在按120Hz走就产生了掉帧假象。排查时需要dumpsys display查看当前的DisplayMode确认是不是被系统限频。如果是那优化重点就不在App而在于减少整机功耗比如降低后台Activity的绘制频率避免无意义的invalidate。写到最后的一点体会这些年我把这条链路反复看烂了最大的感受是Android显示链路本质上就是一条井然有序的工业化流水线每个环节都有明确的分工也有清晰的交接协议。理解了它你再去看那些玄学卡顿、偶发黑屏、颜色发灰的问题都变成了查流水线上某个工位的作业记录。很多问题在你打开Perfetto/Systrace的那一瞬间就已经猜到了七七八八剩下的只是按链路顺序验证。遇到看不懂的trace段就回到这四站模型里问一句这是App的活还是SurfaceFlinger的活还是HWC/驱动的活方向对了答案基本也就浮出来了。最后再提醒一句自己动手在模拟器或真机上抓一次完整的frame trace把每一段的耗时亲手标一遍比看十篇文章都管用。
返回列表