ARTICLE DETAIL

资讯详情

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

Android显示图形系统全景解析:从Surface到屏幕像素的完整链路

Android显示图形系统全景解析:从Surface到屏幕像素的完整链路 很多人第一次接触Android应用开发时都会觉得“把界面写出来”是一件理所当然的事XML里定义一个TextViewActivity里setContentView屏幕就显示了文字。但如果你在Android Studio里点开系统的开发者选项打开“显示Surface更新”或者“GPU渲染模式分析”会看到一排排闪烁的色块和柱状图。这时候你可能会意识到在这层看似简单的界面背后藏着一整套威力巨大的显示图形框架。这就是Android Display Graphics体系。它管的不只是“画一个控件”而是从你手指触摸屏幕到最终显示像素的全链路窗口怎么排队、图层怎么合成、图像数据怎么跨进程传递、什么时候开始绘制、什么时候把帧送出去。无论你是想优化应用卡顿、搞懂SurfaceView与TextureView的区别还是准备进阶系统开发都必须先站在整体框架的视角把地图打开。这篇是系列的第一篇我先不往源码里扎太深重点帮你建立一张完整的概念地图讲清楚这套系统里有哪些核心角色、它们怎么分工、数据怎么流动以及这些知识和日常应用开发到底有什么关系。1. 先有个整体视角显示图形系统到底管了哪些事用一句话概括Android显示图形系统的职责它要把若干个应用进程绘制出来的画面按正确的顺序、在正确的时间、正确地呈现到一块或几块屏幕上。听起来简单实际上要解决三类问题怎么把内容变成可显示的图像数据怎么把这些数据合成为最终画面以及什么时候把最终画面推给屏幕。1.1 显示系统与普通应用开发的关系很多Android开发者常年只跟四大组件和UI控件打交道感觉没必要理解SurfaceFlinger、BufferQueue这些“系统词”。但事实是你写的每一行自定义View、每一个属性动画、每一次启动Activity背后都在调用这套框架。只不过系统做了大量封装让你觉得麻烦不存在了。举个例子你调用invalidate()时系统并不会立刻重绘而是先在你的View树里做标记等待下一个垂直同步信号到来才真正进入measure、layout、draw流程。绘制完成后得到的像素也不是直接写进屏幕而是先放到一个缓冲区里再由系统统一合成。这个过程中的每一环都能解释为什么你的应用会卡顿、为什么有时候按了按钮界面没有立即变化、为什么SurfaceView能“高帧率”播放视频。1.2 三个关键问题画什么、怎么合、什么时候上屏把整个显示系统拆开看其实就是三道题画什么应用进程用自己的渲染引擎Hardware加速下的HWUI或软件绘制Canvas把View树画到一块Surface上生成一帧图像数据。怎么合系统服务SurfaceFlinger拿到各个应用的Surface再结合窗口层级、透明度、变换矩阵把这些图层合成为一帧完整的屏幕画面。什么时候上屏屏幕按照固定频率刷新系统通过VSync信号同步所有生产者保证在上一帧显示完、下一帧准备好时切换缓冲区避免画面撕裂。这三大问题构成了整篇文章的骨架。后续的每一章都是在细化这些问题的答案。2. 从TextView到屏幕像素一次刷新的完整旅行与其背一堆晦涩术语不如先跟着一帧画面的生命周期走一遍。假设你稍微修改了一下TextView的文本然后点击确定整个流程大致是这样主线程执行TextView.setText()触发requestLayout()和invalidate()。下一个VSync到来时Choreographer回调doTraversal()ViewRootImpl开始执行measure/layout/draw。在硬件加速渲染管线下ThreadedRenderer通过HWUI基于OpenGL ES或Vulkan将绘制命令录制为DisplayList并执行渲染命令输出到应用侧Surface对应的缓冲区。应用进程通过BufferQueue机制把填好的图形缓冲区交给SurfaceFlinger。SurfaceFlinger收到新缓冲区后把所有图层的缓冲区取出来计算合成方式通过HWComposer交给显示硬件。显示控制器在下一个刷新周期把最终画面输出到屏幕。这一步一步之间藏着好几套系统组件。下面挑其中最关键的三块展开。2.1 应用侧看到的Surface和Canvas在应用里Surface是一个“画板”的抽象代表一块可被合成、可被显示的图形数据区域。每一种窗口类型比如Activity、Dialog、SurfaceView都对应一个独立的Surface。你在代码里通过Canvas绘制的一切最终都是画在这个Surface上。这里有一个容易混淆的点软件绘制时Canvas是CPU直接往一块内存区域画像素硬件加速时你拿到的Canvas实际是一个“录制画布”它不会立刻产生像素而是把绘制指令记录下来交给渲染线程去执行。所以不要以为onDraw()里面调用了drawXXX()像素就已经生成了。很多性能问题就出在这里比如在onDraw中读取像素会强制软件绘制或者打断硬件管线。2.2 BufferQueue跨进程传递图像数据的管道BufferQueue是连接应用进程和系统合成进程的关键数据结构。它本质上是一个生产者-消费者队列生产者和消费者可以位于不同进程。应用侧Surface是生产者端SurfaceFlinger内部通过消费者端接收缓冲区。通常有2到3个缓冲区循环使用也就是常见的“双缓冲”或“三缓冲”。绘制时生产者调用dequeueBuffer()拿到一块空闲缓冲区绘制完成后调用queueBuffer()把它提交到队列。消费者等队列里有数据后acquireBuffer()取走并显示。显示完毕后缓冲区再还给生产者复用。这中间有两个点值得注意缓冲区内存通过Gralloc分配通常是dma-buf或ion能在进程间共享避免多次拷贝。BufferQueue内部有帧可用回调机制消费者可以在这个回调里唤醒合成任务实现“有数据才合成”。理解BufferQueue的意义在于很多“应用侧已经画完但屏幕上没变化”的现象其实是缓冲区的调度或同步问题而不是本帧没画。2.3 硬件加速渲染管线与HWUI从Android 5.0开始系统默认启用硬件加速。应用进程内的渲染核心是HWUI也常被称为RenderThread的渲染引擎。它接收主线程发来的绘制指令在单独渲染线程中翻译成GPU可执行的命令然后通过OpenGL ES或Vulkan执行。这里的关键概念是DisplayList。View.draw()阶段其实只是构建/更新一棵“绘制树”DisplayList记录要画什么、画在哪里、用什么颜色、转不转角度。真正的光栅化发生在RenderThread上。这样做的好处是主线程不需要等待GPU完成可以继续处理用户输入提升流畅度。另外这一层还承担了部分归并优化例如文本渲染缓存、位图纹理上传、渐变处理等。当你看到某个界面GPU渲染模式分析的柱状线特别高时很可能就是这里的优化没有生效。3. WindowManager与ViewRootImpl窗口系统的“编排者”在显示系统眼里应用所呈现的界面并不是“Activity”这么简单而是“窗口”。窗口是一个相对独立的绘制单元它拥有自己的图层属性位置、大小、层级、透明度等最终由WindowManagerServiceWMS统一管理。3.1 Z-order与窗口类型为什么你看到的界面是叠出来的Android窗口分为三类应用窗口、系统窗口、子窗口。每种窗口又细分为多个类型比如应用窗口的Type范围是1到99系统窗口通常是2000以上。当前台显示哪个Activity窗口就会占据对应的层级状态栏、通知栏、输入法等则属于系统窗口层级更高。最终屏幕上看到的画面就是这些窗口按照Z-order自底向上叠加的结果。Z-order主要由WMS决定受窗口焦点、窗口类型、添加顺序影响。这里有个容易被忽视的细节你的Activity绘制得再好如果Z-order被其它窗口盖住绘制结果也可能不显示或只显示一部分。理解了Z-order你就能明白为什么Dialog和Activity之间的遮盖关系有时会出问题——因为它们的窗口类型不同类型影响了层级基准。3.2 WindowManager.addView()背后发生的事情很多自定义悬浮窗、弹窗开发人员都会用WindowManager.addView()但不知道这行代码的底层流程。我们简单追踪一下当前进程的WindowManagerGlobal收到addView()请求创建ViewRootImpl。ViewRootImpl.setView()会通过Binder调用WindowManagerService.addWindow()。WMS为该窗口分配一个WindowState计算Z-order并向SurfaceFlinger申请对应的Surface通过Session为窗口创建Surface。ViewRootImpl拿到Surface后才能开始真正绘制内容。也就是说你添加一个View到窗口管理器本质上是在系统合成器里注册了一个新的图层来源。不是说你往界面上加了一个View它就能立刻显示而是要等WMS和SurfaceFlinger两边都准备好了并且下一个渲染帧到来时才会真正显示出来。4. SurfaceFlinger全系统的“合成调度中心”很多人第一次接触SurfaceFlinger时会误以为它是Android的渲染引擎。其实它几乎不做复杂的业务渲染它更像一个“合成调度中心”拿着各个App和系统进程已经画好的缓冲区Buffer把它们按照窗口层级关系合成为一帧最终画面然后交给显示硬件。4.1 Layer、BufferQueue与Transaction在SurfaceFlinger内部每个Surface注册进来后会对应一个Layer对象。Layer就是合成的基本单元记录了它当前持有的缓冲区、坐标系、透明度、裁剪区域、变换矩阵等。SurfaceFlinger会在每个VSync周期遍历所有Layer找出“脏”的即缓冲区内容有更新的并纳入合成计划。同时属性的改变不能随便生效因为窗口位置、尺寸的变更必须和缓冲区提交保持同步否则画面会错位或撕裂。所以SurfaceFlinger引入了Transaction机制。事务把一批Layer属性的修改包装在一起通过setTransactionState()提交然后在某个合适的vsync点统一应用。这样的原子性保证让UI过渡更平滑。你平时看到的“窗口动画”“过渡动画”底层都是Transaction在不停调整位置和透明度。4.2 Client合成与设备合成的分界线SurfaceFlinger决定合成方式时会检查能不能把多个Layer交给显示硬件的Overlay Plane直接显示。如果能就走设备合成Device composition主要由HWComposer完成如果不能就把需要参与合成的Layer通过GPU先画出到一个目标缓冲区再交给显示器这叫客户端合成Client composition。实际场景中一个界面可能有状态栏Layer、导航栏Layer、多个应用Layer和壁纸Layer。如果硬件Overlay Plane足够且各Layer之间没有过多混合需求SurfaceFlinger会尽量使用设备合成效率很高。如果GPU过度绘制严重比如一个全屏按钮还带着多个半透明遮罩层就会消耗大量GPU带宽和内存带宽。这解释了为什么设计上要尽量减少图层叠加和半透明区域。5. VSync、Choreographer与刷新率的“握手协议”屏幕并不是“想刷就刷”的它有固定的刷新频率比如60Hz意味着每16.6毫秒刷新一次。如果系统在屏幕刚刷新到一半的时候把新的帧数据切过来画面就会出现撕裂。Android用VSync垂直同步来解决这个问题硬件产生垂直同步信号通知各进程“可以开始准备下一帧了”所有生产者必须在规定时间内完成生产。5.1 Choreographer的三个回调input、animation、traversal应用侧的帧节奏控制器是Choreographer。它会在每个VSync到来时依次处理三个回调CALLBACK_INPUT处理本帧内需要响应的输入事件。CALLBACK_ANIMATION推进当前帧的动画值。CALLBACK_TRAVERSAL执行measure、layout、draw流程。这三个回调的顺序设计保证了输入事件能被及时处理、动画更新统一、绘制任务随后跟上。如果某一项耗时太长后续所有工作都会延迟最终表现为掉帧。没有理解这套机制的人可能会在业务里到处调用invalidate()以为“越早请求重绘就能越快”但实际上无论你怎么调用真正的绘制动作只会在下一个VSync点被调度执行。与其频繁刷新不如把绘制逻辑做轻。5.2 掉帧与“jank”到底是怎么发生的掉帧的专业术语叫Jank指的是“该显示新帧的时刻没有新帧可用只能继续显示旧帧”。举例来说在60Hz屏幕上每个VSync间隔是16.6ms。如果应用完整绘制一帧用了25ms那么第1个VSync开始绘制、绘制到一半时第2个VSync已经到了这时没有新帧输出屏幕只能再显示旧帧绘制到25ms时第3个VSync还没来这一帧要等到第3个VSync才能被取出显示视觉上就会卡一下。所以掉帧不仅是主线程慢的问题渲染线程、缓冲队列、合成管线的任何一环卡住都可能导致Jank。现代性能工具如Perfetto就是围绕这些时间线来分析的。当你看到帧时间段中出现长条红色块时得先区分是主线程耗时、渲染线程耗时还是SurfaceFlinger合成耗时再去对症优化。6. HWC、Display与底层驱动像素的最后一段旅程应用层把帧画好SurfaceFlinger把图层合成了但这还不够——屏幕上显示的信号最终由显示控制器和屏幕驱动产生。这一层对应Android的HAL和内核驱动也是硬件厂商差异最大的地方。6.1 HWC HAL的职责与Overlay PlaneHWComposerHWC是SurfaceFlinger和显示硬件之间的接口封装。SurfaceFlinger计算出合成方案后会调用HWC接口把Layer列表传递下去。硬件负责决定哪些Layer可以放在独立的Overlay Plane上哪个Layer作为主层是否启用硬件缩放等。如果硬件Overlay Plane足够大部分合成工作就不需要GPU参与功耗低、延迟也低。但如果某个Layer格式不被硬件支持需要先混合成另一个格式HWC会要求SurfaceFlinger进行GPU合成。所以你会看到厂商在HAL层做了很多处理比如尝试把多个图层合并、增加Overlay数量、优化裁剪区域。这部分虽然是系统厂商关心的但应用开发者理解后能更好地理解为何某些效果在低端机上格外耗费资源。6.2 Gralloc与Buffer的内存管理再往底层一点是图形缓冲区分配器Gralloc。它属于HAL层负责为BufferQueue分配图形内存并完成CPU/GPU/显示控制器之间的地址映射。常见的实现是基于dma-buf通过Ion或DMA-BUF heaps管理物理内存。一帧图形内存的大小很大比如1080p RGBA8888就需要约8MB。如果频繁分配、映射、释放系统的内存碎片和CPU开销都会上升。所以正常渲染流程里缓冲区是复用和循环的BufferQueue里那几个缓冲区会反复使用。这也是为什么你在自定义相机或图形应用时要谨慎处理每一帧的copy和cleanup——稍不留神就会拖慢整条通道。6.3 Display概念与DisplayManagerService除了屏幕本身Android还抽象了Display概念它代表一个可以被渲染和显示的目标。手机只有一个内置显示但通过有线/无线投屏会出现多个逻辑Display。DisplayManagerService负责管理和发布Display状态。从框架层面看每个Display都有自己的VSync源、刷新率和合成管线。当你在手机上“无线投屏”时SurfaceFlinger可能会同时维护两条合成路径一条输出到内置屏幕一条输出到虚拟显示。很多投屏卡顿的根源就是两层合成任务对GPU和带宽的竞争。理解Display逻辑后再分析Miracast、多屏协同等问题就轻松多了。7. 给Android开发者的实际建议从框架视角看待常见问题讲了这么多框架层面的东西最后还是要回到日常开发。我见过太多人一遇到卡顿就直接看代码看哪个函数耗时多却忽略了显示框架赋予的全局约束。下面我给你划一下重点以及作为应用开发者这整套框架知识到底能带来什么实际收益。7.1 三件你不需要立刻写但必须理解的事第一件事是BufferQueue。它决定了“画完”不等于“显示”中间还隔着缓冲区和合成器。你在相机预览、视频播放、自定义SurfaceView时如果不理解它就很难处理帧率不匹配、画面迟滞等问题。第二件事是VSync。它决定了所有UI动画和绘制的节奏。想实现流畅的逐帧动画应该用Choreographer而不是随便开一个循环去postInvalidateDelayed()。后者很容易打乱系统节奏导致无谓耗电和卡顿。第三件事是合成层级。界面上的每个窗口都是一个图层多层叠加、半透明、阴影都会增加合成压力。优化渲染时优先检查是否有过多透明层、是否能用硬件加速属性、能否减少Surface数量。7.2 你会在哪里用到这些知识渲染卡顿分析打开「开发者选项 → GPU渲染模式分析」或者用Perfetto抓trace能区分是主线程干活太久还是渲染线程没跟上还是SurfaceFlinger合成过重。动画性能优化尽量使用alpha、translation等支持硬件加速的属性避免在动画中触发layout比如修改padding、宽度否则会阻塞主线程。视频/相机开发理解SurfaceView与TextureView的本质差异。前者是独立的Surface有独立的BufferQueue不参与View树硬件合成所以性能更好但不支持变换和滚动后者是普通View树里的纹理灵活但可能多一层拷贝和合成。7.3 关于这份“整体框架”的学习路径这既然是系列第一篇接下来我打算深入几个专题SurfaceFlinger源码视角的合成流程、HWUI渲染管线的实现、BufferQueue的同步与性能、VSync与Choreographer的源码分析。如果你也想啃这块硬骨头我的建议是先装好AOSP源码或至少下载对应系统版本的源码对照着官方文档一起看不要试图一次读懂全部而是抓住一个场景比如“按下Button后屏幕发生了什么”再顺着这个场景去查每一步涉及哪个类。我个人最初的体会是看SurfaceFlinger时觉得很多概念离应用开发很远直到有一次排查一个“页面滑动偶尔卡一下”的问题查了主线程、内存、数据库最后发现是系统合成阶段因为一个全屏圆角背景和窗口透明区导致GPU合成压力过大。那之后我才真正把整条链路串起来。所以不用怕一开始“看不懂源码”框架图先印在脑子里后面你遇到具体问题自然就知道该往哪一层去找答案了。另外有个小技巧用dumpsys SurfaceFlinger和dumpsys window windows看看自己设备上的图层信息能直观观察到每个窗口的层级和Surface调节。这比纯读文档更能建立感觉。把这份整体框架理解到位再往后看任何一块源码你都有一条清晰的主线了。
返回列表