当前位置: 首页 > news >正文

Flutter-->Widget上屏之路

本文主要介绍Flutter中创建一个Widget到屏幕上渲染出Widget内容的路程.

拾用本文您将获得:

  • Widget是什么
  • Element是什么
  • RenderObject是什么

附加Buff:

  • Widget直接渲染成图片
  • 文本String的绘制
  • 图片ui.Image的绘制

这一切都要从runApp方法开始说起, 如果你还不知道runApp是什么, 建议从
https://docs.flutter.dev/ui 开始阅读…

runApp

runApp方法就是进入Flutter世界的入口, 方法参数也是唯一的参数就是一个Widget

void runApp(Widget app) {final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();_runWidget(binding.wrapWithDefaultView(app), binding, 'runApp');
}

那么这个名为appWidget是怎样到界面上的呢? 开始吧…

Element

要想将Widget渲染上屏, 首先就需要将Widget变换成Element.

所以在runApp方法的执行链上, 很容易就能跟踪到下面的代码:

void attachToBuildOwner(RootWidget widget) {final bool isBootstrapFrame = rootElement == null;_readyToProduceFrames = true;// 请注意这里_rootElement = widget.attach(buildOwner!, rootElement as RootElement?);if (isBootstrapFrame) {SchedulerBinding.instance.ensureVisualUpdate();}
}

关键代码widget.attach(buildOwner!, rootElement as RootElement?)

/// 代码来自[RootWidget.attach]
RootElement attach(BuildOwner owner, [ RootElement? element ]) {if (element == null) {owner.lockState(() {element = createElement();assert(element != null);element!.assignOwner(owner);});owner.buildScope(element!, () {element!.mount(/* parent */ null, /* slot */ null);});} else {element._newWidget = this;element.markNeedsBuild();}return element!;
}

上述代码,就是将一个Widget变换成Element的关键代码. 请注意上面的代码, 因为这玩意在另一个类中原封不动的也出现过.

那就是RenderObjectToWidgetAdapter如下:

/// 代码来自[RenderObjectToWidgetAdapter.attachToRenderTree]
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {if (element == null) {owner.lockState(() {element = createElement();assert(element != null);element!.assignOwner(owner);});owner.buildScope(element!, () {element!.mount(null, null);});} else {element._newWidget = this;element.markNeedsBuild();}return element!;
}

Widget变换成Element同样也是将Widget渲染成图片的关键步骤.

到这一步WidgetsBinding.rootElement就赋值完成了, 接下来就是等待屏幕信号刷新帧,开始渲染了…

上述attachToBuildOwner方法中, 有一行代码SchedulerBinding.instance.ensureVisualUpdate()就是用来安排刷新帧的, 触发之后, 等待屏幕信号即可.

/// 代码来自[SchedulerBinding.ensureVisualUpdate]
void ensureVisualUpdate() {switch (schedulerPhase) {case SchedulerPhase.idle:case SchedulerPhase.postFrameCallbacks:scheduleFrame();return;case SchedulerPhase.transientCallbacks:case SchedulerPhase.midFrameMicrotasks:case SchedulerPhase.persistentCallbacks:return;}
}
/// 代码来自[SchedulerBinding.scheduleFrame]
void scheduleFrame() {if (_hasScheduledFrame || !framesEnabled) {return;}assert(() {if (debugPrintScheduleFrameStacks) {debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');}return true;}());ensureFrameCallbacksRegistered();platformDispatcher.scheduleFrame();_hasScheduledFrame = true;
}void ensureFrameCallbacksRegistered() {platformDispatcher.onBeginFrame ??= _handleBeginFrame;platformDispatcher.onDrawFrame ??= _handleDrawFrame;
}

PlatformDispatcher.onBeginFrame

调用链路SchedulerBinding.ensureVisualUpdate->SchedulerBinding.scheduleFrame->SchedulerBinding._handleBeginFrame->SchedulerBinding.handleBeginFrame->SchedulerBinding._invokeFrameCallback

平时通过SchedulerBinding.scheduleFrameCallback方法安排的帧回调就是在SchedulerBinding._invokeFrameCallback方法中执行的.

PlatformDispatcher.onDrawFrame

调用链路SchedulerBinding.ensureVisualUpdate->SchedulerBinding.scheduleFrame->SchedulerBinding._handleDrawFrame->SchedulerBinding.handleDrawFrame->SchedulerBinding._invokeFrameCallback

平时通过SchedulerBinding.addPersistentFrameCallbackSchedulerBinding.addPostFrameCallback方法安排的帧回调就是在这里进行处理的.

之后Flutter通过无限循环执行PlatformDispatcher.onBeginFramePlatformDispatcher.onDrawFrame方法渲染出精美的软件界面.

读到这里, 你是否注意到和WidgetsBinding.rootElement似乎一毛钱关系都没有呢?

不慌, 它在这里…

WidgetsFlutterBinding.ensureInitialized

还记得runApp方法吗?

void runApp(Widget app) {final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();_runWidget(binding.wrapWithDefaultView(app), binding, 'runApp');
}

这方法头一句就是WidgetsFlutterBinding.ensureInitialized, 来看看它的神秘面纱.

/// 代码来自[WidgetsFlutterBinding.ensureInitialized]
static WidgetsBinding ensureInitialized() {if (WidgetsBinding._instance == null) {WidgetsFlutterBinding();}return WidgetsBinding.instance;
}

聪明的你, 应该看出来了, 就是创建了一个单例WidgetsFlutterBinding对象. 您可千万不要被它的表象所迷惑, 这玩意可是一个巨兽…

class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding

当您创建WidgetsFlutterBinding对象后会执行父类的构造方法BindingBase

BindingBase() {if (!kReleaseMode) {FlutterTimeline.startSync('Framework initialization');}assert(() {_debugConstructed = true;return true;}());assert(_debugInitializedType == null, 'Binding is already initialized to $_debugInitializedType');//注意这里initInstances();assert(_debugInitializedType != null);assert(!_debugServiceExtensionsRegistered);initServiceExtensions();assert(_debugServiceExtensionsRegistered);if (!kReleaseMode) {developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});FlutterTimeline.finishSync();}
}

会触发BindingBase.initInstances方法, 此方法会依次执行:

  • GestureBinding.initInstances 主要用来执行屏幕手势回调处理

void initInstances() {super.initInstances();_instance = this;platformDispatcher.onPointerDataPacket = _handlePointerDataPacket;
}
  • SchedulerBinding.initInstances在开发阶段用来计算帧率时间等

void initInstances() {super.initInstances();_instance = this;if (!kReleaseMode) {addTimingsCallback((List<FrameTiming> timings) {timings.forEach(_profileFramePostEvent);});}
}
  • ServicesBinding.initInstances主要用于平台服务支持等

void initInstances() {super.initInstances();_instance = this;_defaultBinaryMessenger = createBinaryMessenger();_restorationManager = createRestorationManager();_initKeyboard();initLicenses();SystemChannels.system.setMessageHandler((dynamic message) => handleSystemMessage(message as Object));SystemChannels.accessibility.setMessageHandler((dynamic message) => _handleAccessibilityMessage(message as Object));SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);SystemChannels.platform.setMethodCallHandler(_handlePlatformMessage);platformDispatcher.onViewFocusChange = handleViewFocusChanged;TextInput.ensureInitialized();readInitialLifecycleStateFromNativeWindow();initializationComplete();
}
  • PaintingBinding.initInstances没想到吧, Flutter原生就有图片缓存池

void initInstances() {super.initInstances();_instance = this;_imageCache = createImageCache();shaderWarmUp?.execute();
}
  • SemanticsBinding.initInstances平台一些语义,无障碍服务等

void initInstances() {super.initInstances();_instance = this;_accessibilityFeatures = platformDispatcher.accessibilityFeatures;platformDispatcher..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged..onSemanticsActionEvent = _handleSemanticsActionEvent..onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;_handleSemanticsEnabledChanged();
}
  • RendererBinding.initInstances Flutter引擎渲染调度核心

void initInstances() {super.initInstances();_instance = this;_rootPipelineOwner = createRootPipelineOwner();platformDispatcher..onMetricsChanged = handleMetricsChanged..onTextScaleFactorChanged = handleTextScaleFactorChanged..onPlatformBrightnessChanged = handlePlatformBrightnessChanged;addPersistentFrameCallback(_handlePersistentFrameCallback);initMouseTracker();if (kIsWeb) {addPostFrameCallback(_handleWebFirstFrame, debugLabel: 'RendererBinding.webFirstFrame');}rootPipelineOwner.attach(_manifold);
}

注意, 注意, 注意, 这个addPersistentFrameCallback(_handlePersistentFrameCallback)方法就是无限循环渲染的关键. addPersistentFrameCallback方法, 在前面已经介绍过了, 是不是忘记了? 往上翻一翻~~

  • WidgetsBinding.initInstances 一些和Widget有关的操作

void initInstances() {super.initInstances();_instance = this;assert(() {_debugAddStackFilters();return true;}());// Initialization of [_buildOwner] has to be done after// [super.initInstances] is called, as it requires [ServicesBinding] to// properly setup the [defaultBinaryMessenger] instance._buildOwner = BuildOwner();buildOwner!.onBuildScheduled = _handleBuildScheduled;platformDispatcher.onLocaleChanged = handleLocaleChanged;SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);SystemChannels.backGesture.setMethodCallHandler(_handleBackGestureInvocation,);assert(() {FlutterErrorDetails.propertiesTransformers.add(debugTransformDebugCreator);return true;}());platformMenuDelegate = DefaultPlatformMenuDelegate();
}

去掉杂念, 让我们关注 _handlePersistentFrameCallback方法:

/// 代码来自[RendererBinding._handlePersistentFrameCallback]
void _handlePersistentFrameCallback(Duration timeStamp) {drawFrame();_scheduleMouseTrackerUpdate();
}/// 代码来自[RendererBinding.drawFrame]

void drawFrame() {rootPipelineOwner.flushLayout();rootPipelineOwner.flushCompositingBits();rootPipelineOwner.flushPaint();if (sendFramesToEngine) {for (final RenderView renderView in renderViews) {renderView.compositeFrame(); // this sends the bits to the GPU}rootPipelineOwner.flushSemantics(); // this sends the semantics to the OS._firstFrameSent = true;}
}

注意到drawFrame方法了吗? 此方法在WidgetsBinding.drawFrame被重写了:


void drawFrame() {assert(!debugBuildingDirtyElements);assert(() {debugBuildingDirtyElements = true;return true;}());TimingsCallback? firstFrameCallback;bool debugFrameWasSentToEngine = false;if (_needToReportFirstFrame) {assert(!_firstFrameCompleter.isCompleted);firstFrameCallback = (List<FrameTiming> timings) {assert(debugFrameWasSentToEngine);if (!kReleaseMode) {// Change the current user tag back to the default tag. At this point,// the user tag should be set to "AppStartUp" (originally set in the// engine), so we need to change it back to the default tag to mark// the end of app start up for CPU profiles.developer.UserTag.defaultTag.makeCurrent();developer.Timeline.instantSync('Rasterized first useful frame');developer.postEvent('Flutter.FirstFrame', <String, dynamic>{});}SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback!);firstFrameCallback = null;_firstFrameCompleter.complete();};// Callback is only invoked when FlutterView.render is called. When// sendFramesToEngine is set to false during the frame, it will not be// called and we need to remove the callback (see below).SchedulerBinding.instance.addTimingsCallback(firstFrameCallback!);}try {//注意此处if (rootElement != null) {buildOwner!.buildScope(rootElement!);}super.drawFrame();assert(() {debugFrameWasSentToEngine = sendFramesToEngine;return true;}());buildOwner!.finalizeTree();} finally {assert(() {debugBuildingDirtyElements = false;return true;}());}if (!kReleaseMode) {if (_needToReportFirstFrame && sendFramesToEngine) {developer.Timeline.instantSync('Widgets built first useful frame');}}_needToReportFirstFrame = false;if (firstFrameCallback != null && !sendFramesToEngine) {// This frame is deferred and not the first frame sent to the engine that// should be reported._needToReportFirstFrame = true;SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback!);}
}

注意到buildOwner!.buildScope(rootElement!);了吗?

Element被丢到BuildOwner对象中, 然后在super.drawFrame方法中调用了PipelineOwner.flushPaint完成一帧渲染的所有流程.

此流程在将Widget直接渲染成图片如出一辙,

直接将Widget渲染成图片代码如下:

Future<ui.Image> buildImage(Widget widget) async {final repaintBoundary = RenderRepaintBoundary();final view = ui.PlatformDispatcher.instance.implicitView ??RendererBinding.instance.renderView.flutterView;//渲染树根final renderView = RenderView(view: view,child: RenderPositionedBox(alignment: Alignment.center,child: repaintBoundary,),configuration: ViewConfiguration.fromView(view),);//管道final pipelineOwner = PipelineOwner()..rootNode = renderView;renderView.prepareInitialFrame();//管道对象final buildOwner = BuildOwner(focusManager: FocusManager());//根元素final rootElement = RenderObjectToWidgetAdapter<RenderBox>(container: repaintBoundary,child: Directionality(textDirection: TextDirection.ltr,child: widget,)).attachToRenderTree(buildOwner);//构建树buildOwner..buildScope(rootElement)..finalizeTree();//渲染树pipelineOwner..flushLayout()..flushCompositingBits()..flushPaint();final image = await repaintBoundary.toImage();return image;}

总结

runApp方法的参数app是怎么到界面上的?

  • 使用RootWidget包裹app Widget
  • 然后使用RootWidget.attach方法将Widget转变成RootElement
  • Element被丢到BuildOwner对象中
  • 然后调用PipelineOwner.flushPaint完成一帧渲染

到这里, 我们还有一个东西没有介绍RenderObject, 它要来了…

RenderObject

首先, 并不是所有的Element都有RenderObject,

Element又是由Widget变换来的, 所以并不是所有的Widget都需要RenderObject.

这你纠正一点, 在ElementrenderObject是一个get方法, 所以Element能获取到renderObject, 但不一定有值…

/// 代码来自[Element.renderObject]
RenderObject? get renderObject {Element? current = this;while (current != null) {if (current._lifecycleState == _ElementLifecycle.defunct) {break;} else if (current is RenderObjectElement) {return current.renderObject;} else {current = current.renderObjectAttachingChild;}}return null;
}

Flutter系统里面, 只有RenderObjectElement才有renderObject, RenderObjectWidget会默认创建RenderObjectElement, 所以…

/// 代码来自[RenderObjectElement.renderObject]

RenderObject get renderObject {assert(_renderObject != null, '$runtimeType unmounted');return _renderObject!;
}
RenderObject? _renderObject; //注意这里

这里顺便说一下, 平时使用的Text小部件, 文本InlineSpan是通过渲染对象RenderObject->RenderParagraph使用TextPainter绘制出来的.
平时使用的Image小部件, 图片ui.Image是通过渲染对象RenderObject>RenderImage使用paintImage方法绘制出来的

Element会经历->Element.mount->Element.update->Element.unmount可能不全, 但最核心的就这几个生命周期的回调.

RootElement根元素的mount方法在RootWidget.attach中的BuildOwner.buildScope方法中调用, 代码在之前已经提到过, 这里再来一次, 不劳烦翻页了.

/// 代码来自[RootWidget.attach]
RootElement attach(BuildOwner owner, [ RootElement? element ]) {if (element == null) {owner.lockState(() {element = createElement();assert(element != null);element!.assignOwner(owner);});owner.buildScope(element!, () {//注意这里element!.mount(/* parent */ null, /* slot */ null);});} else {element._newWidget = this;element.markNeedsBuild();}return element!;
}

而之后子元素Elementmount方法就会在inflateWidget方法中调用了:

Element inflateWidget(Widget newWidget, Object? newSlot) {...final Element newChild = newWidget.createElement();...//注意这里newChild.mount(this, newSlot);...return newChild;} finally {if (isTimelineTracked) {FlutterTimeline.finishSync();}}
}

方法调用链:RootElement.mount->RootElement._rebuild->Element.updateChild->Element.inflateWidget->Widget.createElement->Element.mount

火车就这样开起来了…

RenderObjectElement._renderObject对象就是在RenderObjectElement.mount方法中调用RenderObjectWidget.createRenderObject方法赋值的.

/// 代码来自[RenderObjectElement.mount]

void mount(Element? parent, Object? newSlot) {super.mount(parent, newSlot);assert(() {_debugDoingBuild = true;return true;}());//注意此处_renderObject = (widget as RenderObjectWidget).createRenderObject(this);assert(!_renderObject!.debugDisposed!);assert(() {_debugDoingBuild = false;return true;}());assert(() {_debugUpdateRenderObjectOwner();return true;}());assert(slot == newSlot);attachRenderObject(newSlot);super.performRebuild(); // clears the "dirty" flag
}

然后RenderObject.paint方法会被调用, 用来绘制, 里面有熟悉的canvas对象, 这对于Android开发的同学, 再熟悉不过了把?

/// 代码来自[RenderObject.paint]
void paint(PaintingContext context, Offset offset) { final canvas = context.canvas; 
}

RenderObject对象中有:

  • performLayout方法, 用来实现布局(类似Android中的onMeasureonLayout)
  • paint方法, 用来实现自绘(类似Android中的onDraw)
  • handleEvent方法, 用来处理手势事件(类似Android中的onTouchEvent)

我将在之后的文章中介绍Flutter中的自定义控件:

  • 自定义自绘Widget(类似于自定义Android中的View)
  • 自定义布局Widget(类似于自定义Android中的ViewGroup)

总结

Widget是什么?

用来变换成Element的配置对象.

Element是什么?

用来创建最终的RenderObject对象.

RenderObject是什么?

使用Canvas绘制的, 界面上能看到的都是绘制出来的. 其余类其实都是控制在什么地方绘制用的.

至此文章就结束了! 感谢读者的宝贵时间~


群内有各(pian)种(ni)各(jin)样(qun)的大佬,等你来撩.

联系作者

点此QQ对话 该死的空格 点此快速加群
在这里插入图片描述

在这里插入图片描述

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • git cherry-pick 合并单个提交
  • 【OSCP系列】OSCP靶机-Dawn1(原创)
  • ESP32 出现 failed to load RF 报错
  • 高级java每日一道面试题-2024年8月25日-框架篇[Spring篇]-Spring框架中请举例解释@Required注解?
  • 从头到尾快速学习一遍Linux,高级工程师多年实践实战经验精华总结和实例示例,第四章:高阶使用
  • 云计算实训30——自动化运维(ansible)
  • Javascript——JSDoc 风格的注释语法 为参数添加说明
  • <数据集>斯坦福狗狗识别数据集<目标检测>
  • 面向对象09:instanceof和类型转换
  • 华为数通方向HCIP-DataCom H12-821题库(更新单选真题:1-10)
  • Spring中的AopUtils
  • <C++> 二叉树进阶OJ题
  • C++ JAVA源码 HMAC计算 openssl 消息认证码计算 https消息防篡改 通信安全
  • Vulkan 学习(6)---- vkBuffer 创建
  • Flask-SQLAlchemy 和 Alembic 的结合
  • 收藏网友的 源程序下载网
  • 《Java编程思想》读书笔记-对象导论
  • 【MySQL经典案例分析】 Waiting for table metadata lock
  • 【划重点】MySQL技术内幕:InnoDB存储引擎
  • 0基础学习移动端适配
  • Angular js 常用指令ng-if、ng-class、ng-option、ng-value、ng-click是如何使用的?
  • Apache Zeppelin在Apache Trafodion上的可视化
  • Java 11 发布计划来了,已确定 3个 新特性!!
  • miniui datagrid 的客户端分页解决方案 - CS结合
  • node学习系列之简单文件上传
  • Python利用正则抓取网页内容保存到本地
  • Stream流与Lambda表达式(三) 静态工厂类Collectors
  • 从零开始在ubuntu上搭建node开发环境
  • 如何利用MongoDB打造TOP榜小程序
  • 如何学习JavaEE,项目又该如何做?
  • 如何用vue打造一个移动端音乐播放器
  • 小程序 setData 学问多
  • 一、python与pycharm的安装
  • 【运维趟坑回忆录 开篇】初入初创, 一脸懵
  • 继 XDL 之后,阿里妈妈开源大规模分布式图表征学习框架 Euler ...
  • ​ 无限可能性的探索:Amazon Lightsail轻量应用服务器引领数字化时代创新发展
  • ​​快速排序(四)——挖坑法,前后指针法与非递归
  • ​软考-高级-系统架构设计师教程(清华第2版)【第12章 信息系统架构设计理论与实践(P420~465)-思维导图】​
  • ​中南建设2022年半年报“韧”字当头,经营性现金流持续为正​
  • # dbt source dbt source freshness命令详解
  • #laravel 通过手动安装依赖PHPExcel#
  • ()、[]、{}、(())、[[]]等各种括号的使用
  • (10)工业界推荐系统-小红书推荐场景及内部实践【排序模型的特征】
  • (3)(3.2) MAVLink2数据包签名(安全)
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (LeetCode C++)盛最多水的容器
  • (Redis使用系列) SpringBoot 中对应2.0.x版本的Redis配置 一
  • (笔记)Kotlin——Android封装ViewBinding之二 优化
  • (分布式缓存)Redis分片集群
  • (附源码)ssm码农论坛 毕业设计 231126
  • (佳作)两轮平衡小车(原理图、PCB、程序源码、BOM等)
  • (十一)JAVA springboot ssm b2b2c多用户商城系统源码:服务网关Zuul高级篇
  • (四)Android布局类型(线性布局LinearLayout)
  • (限时免费)震惊!流落人间的haproxy宝典被找到了!一切玄妙尽在此处!
  • .net core 6 集成 elasticsearch 并 使用分词器