重学SurfaceFlinger之View绘制到Surface过程剖析

张开发
2026/4/16 18:43:34 15 分钟阅读

分享文章

重学SurfaceFlinger之View绘制到Surface过程剖析
背景在学习马哥的SurfaceFlinger课程后课程中也有详细讲解app的绘制案例到sf的上屏过程不过案例是一个简单纯色native程序。但是真实的安卓app的画面是丰富多彩的会用到我们丰富多彩的View系统进行app的画面绘制基于这样需求马哥就找到了老同事的一篇关于view绘制到sf上屏的详细剖析文章。View绘制到Surface帧数据的绘制过程wms课程中我们知道了应用是如何拿到“画布”的获取到SurfaceControl的接下来我们来看下应用是如何在绘画完一帧后来提交数据的上节中应用的主线程在performTraversals函数中获取到了操作帧缓冲区的Surface对象这个Surface对象会通过RenderProxy传递给RenderThread, 一些关健代码如下performTraversals里初始化RenderThread时会把Surface对象传过去privatevoidperformTraversals(){......if(mAttachInfo.mThreadedRenderer!null){try{hwInitializedmAttachInfo.mThreadedRenderer.initialize(mSurface);........}......}在ThreadedRenderer的初始化中调用了setSurface这个setSurface函数会通过JNI调到native层ThreadedRenderer.java (frameworks\base\core\java\android\view)booleaninitialize(Surface surface)throws OutOfResourcesException{......setSurface(surface);......}android_graphics_HardwareRenderer.cpp (frameworks\base\libs\hwui\jni)staticvoidandroid_view_ThreadedRenderer_setSurface(JNIEnv*env,jobject clazz,jlong proxyPtr,jobject jsurface,jboolean discardBuffer){RenderProxy*proxyreinterpret_castRenderProxy*(proxyPtr);......proxy-setSurface(window,enableTimeout);......}最终可以看到setSurface是通过RenderProxy这个对象向RenderThread的消息队列中post了一个消息在这个消息的处理中会调用Context的setSurface, 这里的mContext是CanvasContext.RenderProxy.cpp (frameworks\base\libs\hwui\renderthread)voidRenderProxy::setSurface(ANativeWindow*window,boolenableTimeout){ANativeWindow_acquire(window);mRenderThread.queue().post([this,winwindow,enableTimeout]()mutable{mContext-setSurface(win,enableTimeout);ANativeWindow_release(win);});}CanvasContext的setSurface会进一步调用到Surface对象的connect方法5.4节和SurfaceFlinger侧协商同步一些参数。上述过程如下图所示image-20210921100926294.png这其中一些关键过程可以在systrace中看到如下图所示image-20210920180755077.png接下来应用主线程收到vsync信号后会开始绘图流程应用主线程会遍历ViewTree对所有的View完成measure, layout, draw的工作我们知道Android的应用界面是由很多View按树状结构组织起来的如下图以微信主界面为例但无论界面多么复杂它都有一个根View叫DecorView, 而纷繁复杂的界面最终都是由一个个View通过树形结构组织起来的。image-20210922173743472.png限于篇幅我们这里不讨论UI线程的measure和layout部分这里只来看下draw部分首先App每次开始绘画都是收到一个vsync信号才会开始绘图这里暂不讨论SurfaceView自主上帧的情况应用是通过Choreographer来感知vsync信号, 在ViewRootImpl里向Choreographer注册一个callback, 每当有vsync信号来时会执行mTraversalRunnable:ViewRootImpl.java (frameworks\base\core\java\android\view)voidscheduleTraversals(){if(!mTraversalScheduled){......mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL,mTraversalRunnable,null);//注册vsync的回调......}}finalclassTraversalRunnableimplements Runnable{Overridepublicvoidrun(){doTraversal();//每次vsync到时调用该函数}}而doTraversal()主要是调用performTraversals()这个函数performTraversals里会调用到draw()函数privatebooleandraw(boolean fullRedrawNeeded){......if(!dirty.isEmpty()||mIsAnimating||accessibilityFocusDirty){......mAttachInfo.mThreadedRenderer.draw(mView,mAttachInfo,this);//这里传下去的mView就是DecorView......}......}上面的draw()函数进一步调用了ThreadedRenderer的draw:ThreadedRenderer.java (frameworks\base\core\java\android\view)voiddraw(View view,AttachInfo attachInfo,DrawCallbacks callbacks){......updateRootDisplayList(view,callbacks);......}privatevoidupdateRootDisplayList(View view,DrawCallbacks callbacks){Trace.traceBegin(Trace.TRACE_TAG_VIEW,Record View#draw());//这里有个trace我们可以在systrace中观察到它......updateViewTreeDisplayList(view);......}privatevoidupdateViewTreeDisplayList(View view){......view.updateDisplayListIfDirty();//这里开始调用DecorView的updateDisplayListIfDirty......}接下来代码调用到DecorView的基类View.java:View.java (frameworks\base\core\java\android\view)publicRenderNodeupdateDisplayListIfDirty(){......finalRecordingCanvas canvasrenderNode.beginRecording(width,height);//这里开始displaylist的record......draw(canvas);......}上面的RecordingCanvas就是扮演一个绘图指令的记录员角色它会将这个View通过draw函数绘制的指令以displaylist形式记录下来那么上面的renderNode又个什么东西呢熟悉Web的同学一定会对DOM Tree和Render Tree不陌生Android里的View和RenderNode是类似的概念View代表的是实体在空间结构上的存在而RenderNode代表它在界面呈现上的存在。这样的设计可以让存在和呈现进行分离便于实现同一存在不同状态下呈现也不同。在Android的设计里View会对应一个RenderNode, RenderNode里的一个重要数据结构是DisplayList, 每个DisplayList都会包含一系列DisplayListData. 这些DisplayList也会同样以树形结构组织在一起。当UI线程完成它的绘制工作后它工作的产物是一堆DisplayListData, 我们可以将其理解为是一堆绘图指令的集合每一个DisplayListData都是在描绘这个View长什么样子所以一个View树也可能理解为它的样子由对应的DisplayListData构成的树来描述image-20210921110021839.png我们再来看下DisplayListData是长什么样子它定义在下面这个文件中RecordingCanvas.h (frameworks\base\libs\hwui)classDisplayListDatafinal{......voiddrawPath(constSkPath,constSkPaint);voiddrawRect(constSkRect,constSkPaint);voiddrawRegion(constSkRegion,constSkPaint);voiddrawOval(constSkRect,constSkPaint);voiddrawArc(constSkRect,SkScalar,SkScalar,bool,constSkPaint);......templatetypenameT,typename...Argsvoid*push(size_t,Args...);......SkAutoTMallocuint8_tfBytes;......}它的组成大体可以看成三个部分第一部分是一堆以draw打头的函数它们是最基本的绘图指令比如画一条线 画一个矩形画一段圆弧等等上面我们摘取了其中几个,后面我们将以drawRect为例来看它是如何工作的 第二部分是一个push模版函数后面我们会看到它的作用 第三个是一块存储区fBytes它会根据需要放大存储区的大小。我们来看下drawRect的实现RecordingCanvas.cpp (frameworks\base\libs\hwui)voidDisplayListData::drawRect(constSkRectrect,constSkPaintpaint){this-pushDrawRect(0,rect,paint);}我们发现它只是push了画 一个Rect相关的参数那么这个DrawRect又是什么呢structOp{uint32_ttype:8;uint32_tskip:24;};structDrawRectfinal:Op{staticconstautokTypeType::DrawRect;DrawRect(constSkRectrect,constSkPaintpaint):rect(rect),paint(paint){}SkRect rect;SkPaint paint;voiddraw(SkCanvas*c,constSkMatrix)const{c-drawRect(rect,paint);}};通过上面代码我们不难发现DrawRect代表的是一段内存布局这段内存第一个字节存储了它是哪种类型后面的部分存储有画这个Rect所需要的参数信息再来看push方法的实现templatetypenameT,typename...Argsvoid*DisplayListData::push(size_t pod,Args...args){......autoop(T*)(fBytes.get()fUsed);fUsedskip;new(op)T{std::forwardArgs(args)...};op-type(uint32_t)T::kType;//注意这里将DrawRect的类型编码Type::DrawRect存进了第一个字节op-skipskip;returnop1;}这里push方法就是在fBytes后面放入这个DrawRect的内存布局也就是执行DisplayListData::drawRect方法时就是把画这个Rect的方法和参数存入了fBytes这块内存中 那么最后fBytes这段内存空间就放置了一条条的绘制指令。通过上面的了解我们知道了UI线程并没有将应用设计的View转换成像素点数据而是将每个View的绘图指令存入了内存中我们通常称这些绘图指令为DisplayList, 下面让我们跳出这些细节再次回到宏观一些的角度。当所有的View的displaylist建立完成后代码会来到RenderProxy.cpp (frameworks\base\libs\hwui\renderthread)intRenderProxy::syncAndDrawFrame(){returnmDrawFrameTask.drawFrame();}DrawFrameTask.cpp (frameworks\base\libs\hwui\renderthread)voidDrawFrameTask::postAndWait(){AutoMutex_lock(mLock);mRenderThread-queue().post([this](){run();});//丢任务到RenderThread线程mSignal.wait(mLock);}这边可以看到UI线程的工作到此结束它丢了一个叫DrawFrameTask的任务到RenderThread线程中去之后画面绘制的工作转移到RenderThread中来DrawFrameTask.cpp (frameworks\base\libs\hwui\renderthread)voidDrawFrameTask::run(){.....context-draw();.....}CanvasContext.cpp (frameworks\base\libs\hwui\renderthread)voidCanvasContext::draw(){......Frame framemRenderPipeline-getFrame();//这句会调用到Surface的dequeueBuffer......booldrewmRenderPipeline-draw(frame,windowDirty,dirty,mLightGeometry,mLayerUpdateQueue,mContentDrawBounds,mOpaque,mLightInfo,mRenderNodes,(profiler()));......waitOnFences();......booldidSwapmRenderPipeline-swapBuffers(frame,drew,windowDirty,mCurrentFrameInfo,requireSwap);//这句会调用到Surface的queueBuffer......}在这个函数中完成了三个重要的动作一个是通过getFrame调到了Surface的dequeueBuffer向SurfaceFlinger申请了画布 第二是通过mRenderPipeline-draw将画面画到申请到的画布上 第三是通过调mRenderPipeline-swapBuffers把画布提交到SurfaceFlinger去显示。那么在mRenderPipeline-draw里是如何将displaylist翻译成画布上的像素点颜色的呢SkiaOpenGLPipeline.cpp (frameworks\base\libs\hwui\pipeline\skia)boolSkiaOpenGLPipeline::draw(constFrameframe,constSkRectscreenDirty,constSkRectdirty,constLightGeometrylightGeometry,LayerUpdateQueue*layerUpdateQueue,constRectcontentDrawBounds,boolopaque,constLightInfolightInfo,conststd::vectorspRenderNoderenderNodes,FrameInfoVisualizer*profiler){......renderFrame(*layerUpdateQueue,dirty,renderNodes,opaque,contentDrawBounds,surface,SkMatrix::I());......}SkiaPipeline.cpp (frameworks\base\libs\hwui\pipeline\skia)voidSkiaPipeline::renderFrame(constLayerUpdateQueuelayers,constSkRectclip,conststd::vectorspRenderNodenodes,boolopaque,constRectcontentDrawBounds,sk_spSkSurfacesurface,constSkMatrixpreTransform){......SkCanvas*canvastryCapture(surface.get(),nodes[0].get(),layers);......renderFrameImpl(clip,nodes,opaque,contentDrawBounds,canvas,preTransform);endCapture(surface.get());......ATRACE_NAME(flush commands);surface-getCanvas()-flush();......}在上面的renderFrameImpl中会把在UI线程中记录的displaylist重新“绘制”到skSurface中然后通过SkCanvas将其转化为gl指令 surface-getCanvas()-flush();这句是将指令发送给GPU执行这其中是如何“翻译”的细节笔者暂时尚未研究这里先不做讨论。总结一下应用通过android的View系统画出第一帧的总的流程如下图所示image-20210920180157716.png首先是UI线程进行measure, layout然后开始draw, 在draw的过程中会建立displaylist树将每个view应该怎么画记录下来然后通过RenderProxy把后续任务下达给RenderThread, RenderThread主要完成三个动作先通过Surface接口向Surfaceflinger申请buffer, 然后通过SkiaOpenGLPipline的draw方法把displaylist翻译成GPU指令 指挥GPU把指令变成像素点数据 最后通过swapBuffer把数据提交给SurfaceFlinger, 完成一帧数据的绘制和提交。这个过程我们可以在systrace上观察到如下图所示小结本章我们沿着代码逻辑学习了应用是如何申请到画布、使用android的View系统如何绘图、绘图完成后如何提交buffer本章所述的逻辑均是指通过android的View系统绘图的过程也可以称其为hwui绘图流程从上面代码流程可以知道hwui的绘图流程是被vsync信号触发的开始于vsync信号到达UI线程调用performTraversals函数 hwui的画面更新是被vsync信号驱动的。原文地址https://mp.weixin.qq.com/s/5LBGvDkA3aVnUsEePCie-Q更多fw实战开发干货请关注下面“千里马学框架”

更多文章