flutter 专题七 Flutter面试之渲染流程

一、 简介

Flutter面试中必问的一个面试题就是渲染相关的话题。作为Google在2018年发布的一款跨平台UI框架,使用Dart作为其开发语言,底层使用Skia图形库进行视图渲染,渲染速度和用户体验堪比原生。

二、Flutter渲染流程

总的来说,Flutter中一帧的渲染可以分为三个过程:请求渲染、绘制和光栅化。

三、请求渲染阶段

Flutter中也是通过调用setState方法来通知刷新UI。

  1. 调用setState方法,将需要刷新的RenderObject加入dirtyList中;
  2. 调用window对象scheduleFrame函数,scheduleFrame函数是一个native函数,Dart层只是一个函数声明,具体逻辑是在C++层实现;
  3. C++层的scheduleFrame函数会调用Animator对象进行RequestFrame,最终会通过JNI调用回到Java层,调用Android系统的Choreographer监听下一个Vsync信号。
    对应的源码如下:
void setState(VoidCallback fn) {..._element.markNeedsBuild(); //通过相应的element来实现更新,关于element,widget,renderOjbect这里不展开讨论}void markNeedsBuild() {...if (dirty)return;_dirty = true;owner.scheduleBuildFor(this);}void scheduleBuildFor(Element element) {...if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {_scheduledFlushDirtyElements = true;onBuildScheduled(); //这是一个callback,调用的方法是下面的_handleBuildScheduled}_dirtyElements.add(element); //把当前element添加到_dirtyElements数组里面,后面重新build会遍历这个数组element._inDirtyList = true;}void _handleBuildScheduled() {...ensureVisualUpdate();}void ensureVisualUpdate() {switch (schedulerPhase) {case SchedulerPhase.idle:case SchedulerPhase.postFrameCallbacks:scheduleFrame();return;case SchedulerPhase.transientCallbacks:case SchedulerPhase.midFrameMicrotasks:case SchedulerPhase.persistentCallbacks:return;}}void scheduleFrame() {if (_hasScheduledFrame || !_framesEnabled)return;...ui.window.scheduleFrame();_hasScheduledFrame = true;}void scheduleFrame() native 'Window_scheduleFrame';//这个方法是Engine实现的,把接口暴露给Framework,调用这个方法通知引擎,需要更新UI,引擎会在下一个vSync的到达的时候通知Framework

四、绘制阶段

  1. Choreographer接受到下一个Vsync信号后,会执行之前注册回调函数,这里最终会调用到C++层Animator的DrawFrame函数;
  2. 在DrawFrame函数中会执行Dart环境中window对象的DrawFrame函数;
  3. 在window对象DrawFrame函数中调用flushLayout对RenderObjectTree进行测量和布局,然后调用flushPaint进行绘制,绘制结束后,会生成一颗LayerTree。
    涉及的代码如下所示。
void _drawFrame() { //Engine回调Framework入口 _invoke(window.onDrawFrame, window._onDrawFrameZone);
}//初始化的时候把onDrawFrame设置为_handleDrawFramevoid initInstances() {super.initInstances();_instance = this;ui.window.onBeginFrame = _handleBeginFrame;ui.window.onDrawFrame = _handleDrawFrame;SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);}void _handleDrawFrame() {if (_ignoreNextEngineDrawFrame) {_ignoreNextEngineDrawFrame = false;return;}handleDrawFrame();}void handleDrawFrame() {_schedulerPhase = SchedulerPhase.persistentCallbacks;//记录当前更新UI的状态for (FrameCallback callback in _persistentCallbacks)_invokeFrameCallback(callback, _currentFrameTimeStamp);}}void initInstances() {....addPersistentFrameCallback(_handlePersistentFrameCallback);}void _handlePersistentFrameCallback(Duration timeStamp) {drawFrame();}void drawFrame() {...if (renderViewElement != null)buildOwner.buildScope(renderViewElement); //先重新build widgetsuper.drawFrame();buildOwner.finalizeTree();}void drawFrame() { //这个方法完成Layout,CompositingBits,Paint,生成Layer和提交给Engine的工作assert(renderView != null);pipelineOwner.flushLayout(); pipelineOwner.flushCompositingBits();pipelineOwner.flushPaint();renderView.compositeFrame(); //生成Layer并提交给EnginepipelineOwner.flushSemantics(); }

从上面代码分析得知,从Engine回调,Framework会build,Layout,Paint,生成Layer等环节。

五、Build

在Flutter应用开发中,无状态的widget是通过StatelessWidget的build方法构建UI,有状态的widget是通过State的build方法构建UI。比如:

//这是官方的demo
class _MyHomePageState extends State<MyHomePage> {int _counter = 0;void _incrementCounter() {setState(() {_counter++;});}//这里就是构建UI,当调用setState后就会调用到这里,重新生成新的widget@overrideWidget build(BuildContext context) {return new Scaffold(...);}
}//从上面代码的分析到,在调用了setState后,最终会调用到buildScope来build
void buildScope(Element context, [VoidCallback callback]) {..._dirtyElements.sort(Element._sort);_dirtyElementsNeedsResorting = false;int dirtyCount = _dirtyElements.length;int index = 0;while (index < dirtyCount) {..._dirtyElements[index].rebuild();index += 1;}for (Element element in _dirtyElements) {assert(element._inDirtyList);element._inDirtyList = false;}_dirtyElements.clear();}void rebuild() {...if (!_active || !_dirty)return;performRebuild();}void performRebuild() {...built = build();...}Widget build() => widget.build(this);

可以看到,buildScope会遍历_dirtyElements,对每个在数组里面的每个element调用rebuild,最终就是调用到相应的widget的build方法。 其实当setState的时候会把相应的element添加到_dirtyElements数组里,并且element标识dirty状态。

六、Layout

在Flutter中应用中,是使用支持layout的widget来实现布局的,支持layout的wiget有Container,Padding,Align等等。在渲染流程中,在widget build后会进入layout环节,下面具体分析一下layout的实现,layout入口是flushLayout。

void flushLayout() {...while (_nodesNeedingLayout.isNotEmpty) {final List<RenderObject> dirtyNodes = _nodesNeedingLayout;_nodesNeedingLayout = <RenderObject>[];for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {//这里是按照在node tree中的深度顺序遍历_nodesNeedingLayout,RenderObject的markNeedsLayout方法会把自己添加到_nodesNeedingLayoutif (node._needsLayout && node.owner == this)//对于需要layout的RenderObject进行layoutnode._layoutWithoutResize();}}...
}
void _layoutWithoutResize() {...performLayout(); //这个方法是计算layout的实现,不同layout widget有不同的实现markNeedsSemanticsUpdate();..._needsLayout = false;markNeedsPaint();
}
//这里就是列出来RenderView的计算布局的实现方式,这个比较简单,就是读取配置里面的大小,然后调用child的layout,其他widget layout的计算布局的方式是非常繁琐复杂的,可以自行分析代码
void performLayout() {assert(_rootTransform != null);_size = configuration.size;assert(_size.isFinite);if (child != null)child.layout(new BoxConstraints.tight(_size));//调用child的layout}//这个方法parent调用child的layout的入口,parent会把限制传给child,child根据限制来layout
void layout(Constraints constraints, { bool parentUsesSize: false }) {...RenderObject relayoutBoundary;if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {relayoutBoundary = this;} else {final RenderObject parent = this.parent;relayoutBoundary = parent._relayoutBoundary;}if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {return;}_constraints = constraints;_relayoutBoundary = relayoutBoundary;if (sizedByParent) {performResize(); }RenderObject debugPreviousActiveLayout;performLayout();//实际计算layout的实现markNeedsSemanticsUpdate();_needsLayout = false;markNeedsPaint();
}
void performResize() {...size = constraints.biggest;switch (axis) {case Axis.vertical:offset.applyViewportDimension(size.height);break;case Axis.horizontal:offset.applyViewportDimension(size.width);break;}
}//这是标记为layout为dirty,把自己添加到渲染管道(PipelineOwner)里面
void markNeedsLayout() {if (_relayoutBoundary != this) {markParentNeedsLayout();} else {_needsLayout = true;if (owner != null) {return true;}());owner._nodesNeedingLayout.add(this);owner.requestVisualUpdate();}}}

七、Paint

当需要描绘自定义的图像的时候,可以通过继承CustomPainter,实现paint方法,然后在paint方法里面使用Flutter提供接口可以实现复杂的图像。 后面我们会详细讲一下Paint流程的实现,此处不再过多赘述。

八、Composited Layer

Composited Layer就是把所有layer组合成Scene,然后通过ui.window.render方法,把scene提交给Engine,到这一步,Framework向Engine提交数据基本完成了。Engine会把所有的layer根据大小,层级,透明度计算出最终的显示效果,通过Openg Gl接口渲染到屏幕上。

void compositeFrame() {Timeline.startSync('Compositing', arguments: timelineWhitelistArguments);try {final ui.SceneBuilder builder = new ui.SceneBuilder();layer.addToScene(builder, Offset.zero);final ui.Scene scene = builder.build();ui.window.render(scene);scene.dispose();assert(() {if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled)debugCurrentRepaintColor = debugCurrentRepaintColor.withHue(debugCurrentRepaintColor.hue + 2.0);return true;}());} finally {Timeline.finishSync();}}void addToScene(ui.SceneBuilder builder, Offset layerOffset) {addChildrenToScene(builder, offset + layerOffset);}void addChildrenToScene(ui.SceneBuilder builder, Offset childOffset) {Layer child = firstChild;while (child != null) {child.addToScene(builder, childOffset);child = child.nextSibling;}}

九、光栅化阶段

  1. Dart层绘制结束后,会调用window的render方法将LayerTree同步到C++环境中,window的render也是一个native方法,具体的实现在C++层
  2. C++层的Render方法会调用Animator对象的Render方法,在Render方法中会将LayerTree加入到LayerTreePipeLine中,这是一个典型的生产者消费者模式。
  3. Dart UI线程负责将绘制好的LayerTree加入pipeline中,GPU线程负责从pipeline中获取LayerTree进行光栅化,pipeline中最多存在2个LayerTree。
  4. CPU线程从pipeline中获取到LayerTree后,会调用Rasterizer的Draw方法进行光栅化,这里的光栅化实际上是通过递归遍历LayerTree将每一个Layer通过SKCanvas绘制到FrameBuffer。
  5. FlutterActivity启动时会创建GLContext,也就是OpneGL的上下文环境,当光栅化需要创建SKCanvas时,会通过GLContext获取FBO(FrameBufferObject),然后通过FBO创建SKSurface,在通过SKSurface创建SKCanvas。
  6. 当LayerTree绘制结束后,会通过SwrapBuffer方法将数据提交给显示器。

十、完整流程

十一、Paint阶段

Flutter的绘制主要包括两步:第一步是在Dart遍历执行RenderObject的paint方法,完成不同RenderObejct的绘制,生成一个LayerTree。第二步是在C++层将这颗LayerTree最终渲染到屏幕。

首先,我们看一下,Dart层LayerTree的生成过程。

  1. 创建PictureLayer,添加到RootLayer,形成一个LayerTree,这里以只有两个节点的LayerTree为例
  2. 创建Dart层的Canvas对象,首先会在C++层创建SKPictureRecord,然后通过SKPictureRecord创建一个SKCanvas,Dart层的Canvas对象只是C++中SKCanvas对象的一个代理。Dart环境中在Canvas对象上所有绘制操作,都会被记录到SKPictureRecord里面
  3. 遍历RenderObjectTree,调用RenderObject对象的paint方法,不同的RenderObeject重写paint方法,通过Canvas对象,进行绘制自己。
  4. 遍历结束后,调用SKPictureRecord的endRecording方法,结束记录,并且生成一个SKPicture对象,在Dart层对应一个Picture对象。SKPicture可以看一做是一帧屏幕截图,源码中SKPicture也是可以直接转成Image。
  5. 将Dart层生成的Picture对象赋值给PictureLayer,这样Dart层的LayerTree创建完成。
  6. 将Dart层的LayerTree同步到C++,通过GPU线程进行光栅化处理。

接下来,是C++层渲染LayerTree过程。

  1. FlutterActivity启动时,会在C++层完成GL环境的初始化和GLConetxt的创建
  2. FlutterSurfaceView的Surface创建完成后会同步到C++,通过这个Surface在C++层创建AndroidEGLSurface,作为GL的目标渲染Surface
  3. 通过AndroidEGLSurface创建SKSurface,在这个SKSurface上绘制的东西都会代理到AndroidEGLSurface
  4. 通过SKPicture创建SKCanvas,然后遍历LayerTree,通过SKCanvas绘制所有的Layer节点,完成光栅化处理,最终将光栅化后的数据刷新到手机屏幕上完成渲染。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/6850.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

深入理解 TCP 的握手与挥手机制:为何握手 3 次,挥手 4 次?

在网络通信的世界里&#xff0c;TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;是一种非常重要的协议&#xff0c;它确保了数据在网络中的可靠传输。而 TCP 的连接建立&#xff08;握手&#xff09;和连接断开&#xff08;挥手&#xff09…

Python-数据爬取(爬虫)

在数据驱动的时代&#xff0c;Python以其强大的数据处理能力和丰富的库资源&#xff0c;成为数据爬取的首选语言。通过Python&#xff0c;你可以轻松地从网页中抓取所需的数据&#xff0c;无论是价格信息、新闻内容还是用户评论&#xff0c;都能一一收入囊中。使用requests库发…

基于51单片机水位监测控制报警仿真设计

基于51单片机水位监测控制报警仿真设计 1. 主要功能&#xff1a;2. 讲解视频&#xff1a;3. 仿真4. 程序代码5. 设计报告6. 设计资料内容清单&&下载链接资料下载链接&#xff1a; 基于51单片机水位监测控制报警仿真设计( proteus仿真程序设计报告讲解视频&#xff09; …

JavaFX在Linux aarch64上运行

1.适配Jdk Linux开发项目安装在麒麟系统&#xff0c;无法安装&#xff0c;经查询因cpu架构不同导致无法运行 https://www.oracle.com/sg/java/technologies/downloads/#java21 该链接可下载jdk21,Linux aarch64版本。 2.适配Javafx模块 替换jdk之后&#xff0c;JavaFX仍无…

3D区块多重渐变围栏

这里主要用到的就是threejs的shader&#xff0c;至于其他知识点&#xff0c;可以参考json生成3d区域 下面的主要代码&#xff1a; import * as THREE from three; import { OrbitControls } from three/addons/controls/OrbitControls.js import { EffectComposer } from th…

【NLP】使用 SpaCy、ollama 创建用于命名实体识别的合成数据集

命名实体识别 (NER) 是自然语言处理 (NLP) 中的一项重要任务&#xff0c;用于自动识别和分类文本中的实体&#xff0c;例如人物、位置、组织等。尽管它很重要&#xff0c;但手动注释大型数据集以进行 NER 既耗时又费钱。受本文 ( https://huggingface.co/blog/synthetic-data-s…

Git代码托管(三)可视化工具操作(1)

常见的可视化操作工具有 一、官方网页 如码云、gitlab&#xff0c;自带了常见的git操作。 以码云为例&#xff1a; 1、创建分支&#xff1a; 进入分支目录&#xff0c;点击 新建分支 按钮&#xff0c; 在弹出框中输入新分支名称&#xff0c;点击确定即可一键创建分支&…

STL学习-无序容器-unordered set和unorderde multiset

1.定义及初始化 #include <unordered set> #include <iostream> using namespace std; //输出s中的所有元素 template<typename T> void Show(const T& s) { for(auto&x:s) cout << x<<" ";cout << endl; } int main()…

鸿蒙(Harmony)实现滑块验证码

在Android和ios两端已经使用的滑块验证码框架还未适配鸿蒙版&#xff0c;于是需要自己去实现类似如下的滑块验证码&#xff1a; 那么实现这样的验证码主要涉及到几个内容&#xff1a; 1、自定义弹窗 2、base64图片转换 3、滑动组件与滑块的联动&#xff0c;以及横移距离转换…

《华为工作法》读书摘记

无论做什么事情&#xff0c;首先要明确的就是做事的目标。目标是引导行动的关键&#xff0c;也是证明行动所具备的价值的前提&#xff0c;所以目标管理成了企业与个人管理的重要组成部分。 很多时候&#xff0c;勤奋、努力并不意味着就一定能把工作做好&#xff0c;也并不意味…

三维测量与建模笔记 - 3.3 张正友标定法

上图中&#xff0c;提到了世界坐标系在张正友标定法中的设计&#xff0c;可以理解为将世界坐标系的原点放到了棋盘格左上角点的位置&#xff0c;并且棋盘格平面上所有点的Z为0&#xff0c;将Z规定为0的话&#xff0c;可以简化掉一个维度&#xff08;列向量r3&#xff09;。去掉…

【课程总结】day34:多模态大模型之ViT模型、CLIP模型论文阅读理解

前言 在【课程总结】day31&#xff1a;多模态大模型初步了解一文中&#xff0c;我们对多模态大模型的基本原理有了初步了解&#xff0c;本章内容将通过论文阅读理解&#xff0c;更进一步理解多模态大模型中所涉及的 Vit 架构、Transformer在视觉应用的理念以及 Clip图像与文本…

国药准字生发产品有哪些?这几款不错

头秃不知道怎么选的朋友们看这&#xff0c;基本上市面上火的育发精华我都用了个遍了&#xff0c;陆陆续续也花了有大几w了&#xff0c;都是真金白银总结出来的&#xff0c;所以必须要给掉发人分享一些真正好用的育发产品&#xff0c;大家可以根据自己实际情况来选择。 1. 露卡菲…

golang分布式缓存项目 Day 1

注&#xff1a;该项目原作者&#xff1a;https://geektutu.com/post/geecache-day1.html。本文旨在记录本人做该项目时的一些疑惑解答以及部分的测试样例以便于本人复习。 LRU缓存淘汰策略 三种缓存淘汰策略 FIFO&#xff08;First In, First Out&#xff09;先进先出 原理&…

Android View事件分发

目录 1.什么是View事件分发&#xff1f; 2.事件的类型 3.事件的发生 4.事件分发的方法 4.1 dispatchTouchEvent() 4.2 onTouchEvent() 4.3 onInterceptTouchEvent() 5.滑动冲突 5.1 外部拦截法 5.2内部拦截法 6.onTouch的执行高于onClick 7. onTouch()和onTouchEve…

Elasticsearch常用接口_添加数据

插入es数据&#xff1a;_index/_type/ POST { "tabTitle": "森图表_test", "chtTabTitle": "森图表_test", "status": 0 } 注意&#xff1a;Elasticsearch 6.0.0及更高版本中&#xff0c;索引只能包含一个映射类型

springboot养老院信息管理系统-计算机设计毕业源码30958

目 录 摘要 1绪论 1.1 研究背景 1.2 研究意义 1.3论文结构与章节安排 2 系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据新增流程 2.2.2 数据删除流程 2.3 系统功能分析 2.3.1 功能性分析 2.3.2 非功能性分析 2.4 系统用例分析 2.5本章小结 3 系统总体设…

OkHttp网络请求框架

添加依赖 在 build.gradle 文件中添加 OkHttp 依赖&#xff1a; dependencies {implementation("com.squareup.okhttp3:okhttp:4.10.0") }使用OkHttp发起GET请求 同步请求 public class MainActivity extends AppCompatActivity {// Used to load the okhttptes…

基于sealos部署的集群部署dashboard

1、下载yaml文件进行部署 rootmaster-1:~# wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml2、修改service的访问模式 rootmaster-1:~# vim recommended.yaml kind: Service apiVersion: v1 metadata:labels:k8s-app: kube…

手机贴膜气泡识别组件定位图像分割系统:快速图像识别

手机贴膜气泡识别组件定位图像分割系统源码&#xff06;数据集分享 [yolov8-seg-C2f-Parc&#xff06;yolov8-seg-KernelWarehouse等50全套改进创新点发刊_一键训练教程_Web前端展示] 1.研究背景与意义 项目参考ILSVRC ImageNet Large Scale Visual Recognition Challenge …