:从窗口 Buffer 到 SurfaceFlinger 合成与屏幕显示)
书接上回上文是 Activity 启动流程四从 setContentView 到 View 树创建与 Window 挂载距离最后合成图像显示在屏幕上还有一段距离。一. 在 Activity 的 resume 中已经进行 Window 的 addView已经创建了 WindowState。在 Window.setView 内已经预约了绘制 Surface 的任务在 WindowState 创建好之后在performTraversals()中进行 Surface 任务的绘制。这里主要就是 App 进程提交需要合成的页面的信息提交到 BufferQueue 中然后 SurfaceFlinger 进程从这个 BufferQueue 中获取需要绘制的页面的信息最后交给 GPU 或者硬件屏幕进行图像的合成。二、relayoutWindow()窗口如何获得 Surface 和图层先看一下 SurfaceControl 里面负责什么它管理几个变量、成员保存什么支撑的用途成员保存什么支撑的用途mClientSurfaceFlinger 客户端连接对象保持与服务端相关的连接mHandle服务端图层的 Binder 标识指明事务要修改哪个图层mGraphicBufferProducerBuffer 生产者接口建立能够申请、提交 Buffer 的 Surface参考/frameworks/native/libs/gui/include/gui/SurfaceControl.h这里是管理一些关于图层的属性类结构是 frameworks/native/libs/gui/include/gui/LayerState.h中包括 x、y、alpha 设置透明度以及裁剪等等。frameworks/native/libs/gui/SurfaceComposerClient.cpp 这里可以通过以下函数进行 x、y、alpha 之类属性的设置SurfaceComposerClient::Transaction SurfaceComposerClient::Transaction::setPosition( const spSurfaceControl sc, float x, float y); SurfaceComposerClient::Transaction SurfaceComposerClient::Transaction::setAlpha( const spSurfaceControl sc, float alpha); SurfaceComposerClient::Transaction SurfaceComposerClient::Transaction::setCrop_legacy( const spSurfaceControl sc, const Rect crop);调用链如下void doTraversal() performTraversals() 1 relayoutResult relayoutWindow(params, viewVisibility, insetsPending); 1.1 int relayoutResult mWindowSession.relayout(...); // 这里负责创建 SurfaceControl 1.2 mSurface.copyFrom(mSurfaceControl); // 这里是负责让 App 侧的 Surface 关联到可以提交窗口 Buffer 的生产者接口 // App 根据返回的 SurfaceControl取得关联同一生产者接口的 native Surface并让 ViewRootImpl.mSurface 持有它。 // 之后 App 就能通过 Surface 申请、提交窗口 Buffer。 mSurfaceData new Surface(mGraphicBufferProducer, false);relayoutWindow() 在这里完成的是画面提交入口的建立先通过窗口布局请求取得 SurfaceControl再利用它携带的 Buffer 生产者接口建立 App 侧的 Surface。此时还没有绘制画面只是把后续提交画面所需的接口接好了。三、从 View 绘制记录到窗口 Buffer前面的relayoutWindow()已经为 App 建立了 Surface接下来要向它提供窗口画面。在硬件加速路径中ViewRootImpl通过以下调用进入绘制ViewRootImpl.performTraversals() → ViewRootImpl.performDraw() → ViewRootImpl.draw(...) → ThreadedRenderer.draw(...)其中ViewRootImpl.draw()调用mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);这段绘制过程分为两个阶段主线程记录怎样画渲染线程使用这些记录生成窗口画面。3.1 主线程记录 View 的绘制操作ThreadedRenderer.draw()首先更新绘制记录ThreadedRenderer.draw(...) → updateRootDisplayList(...) → updateViewTreeDisplayList(...) → View.updateDisplayListIfDirty()需要重新记录时View.updateDisplayListIfDirty()使用RecordingCanvas调用 View 的绘制逻辑。核心过程可概括为RecordingCanvas canvas renderNode.beginRecording(width, height); // 根据分支执行 draw(canvas) 或 dispatchDraw(canvas) renderNode.endRecording();例如View 绘制背景、文字时会调用画布的矩形、文字等绘制接口。对于记录画布这些调用保存的是操作及参数而不是立即生成完整的窗口像素。以画矩形为例本地代码用DrawRect保存一次绘制struct DrawRect final : Op { static const auto kType Type::DrawRect; DrawRect(const SkRect rect, const SkPaint paint) : rect(rect), paint(paint) {} SkRect rect; SkPaint paint; void draw(SkCanvas* c, const SkMatrix) const { c-drawRect(rect, paint); } };其中rect保存矩形范围paint保存颜色、填充方式等绘制参数。这些操作记录存放在DisplayListData的内存中由SkiaDisplayList持有DisplayListData mDisplayList;记录完成后绘制列表交给对应的 RenderNode 保存供后续渲染使用。3.2 渲染线程使用绘制记录生成窗口画面完成记录后ThreadedRenderer.draw()调用int syncResult syncAndDrawFrame(choreographer.mFrameInfo);这一步通过 Java 本地接口进入渲染实现投递绘制任务。RenderThread 执行任务时进入DrawFrameTask::run() → CanvasContext::draw()CanvasContext::draw()中有三个关键动作下面省略具体参数mRenderPipeline-getFrame(); // 准备本帧渲染目标 mRenderPipeline-draw(...); // 使用绘制记录组织渲染 mRenderPipeline-swapBuffers(...); // 提交本帧在绘制阶段渲染管线使用前面记录的矩形、文字、图片等操作经图形驱动组织图形处理器执行将窗口内容渲染到目标 Buffer 中。这里不会重新调用 Java 层的 View 绘制逻辑而是使用已经记录好的本地绘制操作。最后swapBuffers()进入本帧的提交流程。提交同时带有同步信息消费者需要遵守同步条件才能使用画面不能把函数返回理解为屏幕已经显示。至此本节完成的数据变化是View 的绘制逻辑 ↓ 主线程记录 DisplayListData 中的绘制操作及参数 ↓ 渲染线程组织图形处理器执行 窗口 Buffer 中的画面 ↓ 提交 交给后续消费者处理四 bufferqueue 窗口画面如何传给surfaceflingerApp 画好窗口 Buffer提交它的槽位和本帧信息。队列将这次提交组织成BufferItem。消费者取得 BufferItem关联到实际的 GraphicBuffer。这里有两个关键函数queueBuffer(slot, input, output); acquireBuffer(item, presentWhen, maxFrameNumber);接口输入结果queueBuffer()槽位编号、QueueBufferInput将本次提交入队返回提交反馈acquireBuffer()期望显示时间、帧号限制等输出描述选定帧的BufferItem这里参考了一张网图来源是https://zhuanlan.zhihu.com/p/143841971图像并不是在这里被整张复制进 Binder 请求。这次请求提交槽位和相关信息服务端根据槽位关联对应 Buffer。bufferitem的内部成员变量是什么在frameworks/native/libs/gui/include/gui/BufferItem.h大致如下spGraphicBuffer mGraphicBuffer; // 关联的图像 Buffer int mSlot; // Buffer 对应的槽位编号 uint64_t mFrameNumber; // 本次提交的帧号 spFence mFence; // 同步信号协调生产者写入与消费者使用 int64_t mTimestamp; // 本帧时间戳 Rect mCrop; // Buffer 内容的裁剪区域 uint32_t mTransform; // 翻转、旋转等变换标记 uint32_t mScalingMode; // 缩放模式 Region mSurfaceDamage; // 相比上一帧发生变化的区域五、SurfaceFlinger窗口画面如何合成并显示上一节App 已经通过 BufferQueue 提交窗口画面。SurfaceFlinger 获取对应的 Buffer 后还需要将它与状态栏、导航栏等其他窗口的画面一起组织成显示输出。这里需要先分清SurfaceFlinger 负责组织合成任务实际的像素处理由图形处理器或显示硬件执行。5.1 合成需要两类输入窗口画面和图层属性取得窗口 Buffer只解决了“这个窗口本次显示什么”。窗口放在哪里、叠在谁上面、整体是否透明则由图层属性决定。这些信息通过 SurfaceControl 事务更新不是全部包含在 BufferItem 中。BufferQueue 路径 → 更新图层当前使用的窗口 Buffer SurfaceControl 事务路径 → 更新图层的位置、层级、透明度、裁剪等属性例如Activity 和状态栏分别提供自己的画面。SurfaceFlinger 根据它们对应图层的属性安排状态栏在屏幕顶部显示以及它与 Activity 重叠区域的处理方式。图层已经提前创建这里是更新已有图层的内容和状态不是重新创建图层。5.2onMessageRefresh()组织本次显示的输入SurfaceFlinger 处理刷新消息时进入SurfaceFlinger::onMessageReceived(...) → SurfaceFlinger::onMessageRefresh() → CompositionEngine::present(...)在onMessageRefresh()中先收集显示输出for (const auto [_, display] : displays) { refreshArgs.outputs.push_back( display-getCompositionDisplay()); }再按图层叠放顺序收集图层对象mDrawingState.traverseInZOrder([refreshArgs](Layer* layer) { if (auto layerFE layer-getCompositionEngineLayerFE()) { refreshArgs.layers.push_back(layerFE); } });源码中的layerFE表示图层前端对象合成引擎通过它获取图层的相关状态。最后调用mCompositionEngine-present(refreshArgs);这一阶段准备的是合成任务的输入还没有计算最终图像的像素。5.3Output::present()为一个显示输出安排合成CompositionEngine::present()为各显示输出准备图层和状态再调用output-present(args);这里的output代表一个显示输出而不是一个窗口。实际执行的Output::present()中重点是以下几个调用updateAndWriteCompositionState(refreshArgs); // 其他处理省略 prepareFrame(); finishFrame(refreshArgs); postFramebuffer();它们分别完成调用作用updateAndWriteCompositionState()更新合成状态并向硬件合成器设置相关图层信息prepareFrame()确定本帧的合成策略finishFrame()必要时组织图形处理器合成postFramebuffer()提交显示输出合成并不一定要求图形处理器把所有窗口先画成一张图。根据硬件能力和图层条件可以采用图形处理器合成、显示硬件合成或者两者配合。5.4 图形处理器怎样参与合成需要图形处理器合成时调用链进入Output::finishFrame(...) → Output::composeSurfaces(...) → renderEngine.drawLayers(...)composeSurfaces()准备合成目标和图层绘制参数再调用 RenderEnginerenderEngine.drawLayers(...);可以用下面的输入输出理解这一步输入 需要合成的窗口 Buffer 位置、裁剪、变换、透明度等信息 执行 RenderEngine 和图形驱动组织绘制命令 图形处理器执行采样、变换和颜色混合 输出 合成结果 Buffer例如半透明状态栏覆盖 Activity 时图形处理器根据透明度计算重叠区域的颜色并将结果写入目标 Buffer。因此不是 SurfaceFlinger 已经合成完画面再交给图形处理器重复合成而是 SurfaceFlinger 通过 RenderEngine 使用图形处理器完成这部分工作。对于交给显示硬件处理的图层则提交相应的 Buffer 和显示属性由显示硬件完成叠加。5.5 从软件提交到显示硬件完成合成准备后Output::postFramebuffer()进入显示提交链Output::postFramebuffer() → Display::presentAndGetFrameFences() → HWComposer::presentAndGetReleaseFences(...) → HWC2::Display::present(...) → Composer::presentDisplay(...)源码中的HWComposer、HWC2分别对应硬件合成器封装和第二版硬件合成器接口。Composer::presentDisplay()先写入显示提交命令mWriter.selectDisplay(display); mWriter.presentDisplay(); Error error execute();随后execute()将命令交给硬件合成器服务执行。根据接口版本会调用mClient-executeCommands(...);或者mClient_2_2-executeCommands_2_2(...);这里是 SurfaceFlinger 向硬件合成器服务提交工作的明确边界。后续由厂商显示实现通过内核显示驱动控制硬件具体底层调用取决于设备实现。这个过程通常不是把整张图像用write()写进一个设备节点。图像已经位于 Buffer 内存中软件提交的是缓冲区关联信息、显示配置和同步信息驱动据此配置显示硬件。显示硬件在满足同步条件后读取对应图像按显示时序输出到屏幕。显示提交完成也不等于屏幕已经在同一瞬间显示了这一帧。5.6 回到 Activity 的第一帧至此窗口显示的主线可以串起来App 记录 View 绘制操作 ↓ 渲染线程组织绘制生成窗口 Buffer ↓ 通过 BufferQueue 提交与获取 ↓ SurfaceFlinger 结合图层属性组织合成 ↓ 图形处理器或显示硬件执行像素处理 ↓ 显示硬件输出到屏幕结论这一过程还要求窗口图层满足显示条件。即使已经提交 Buffer如果图层仍然隐藏、完全透明或被其他图层完全遮挡用户依然看不到它。所以“窗口创建成功”“提交了画面”“用户看见窗口”是三个不同的阶段。到显示硬件真正输出有效的窗口画面Activity 的第一帧才最终出现在屏幕上。对于这里比较感兴趣的可以继续去参考这个博主的一系列文章https://zhuanlan.zhihu.com/p/143841971