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

Flutter视图原理之三棵树的建立过程

目录

    • 三棵树的关系
    • 树的构建过程
      • 1.updateChild函数(element的复用)
      • 2.inflateWidget函数
      • 3.mount函数
        • 3.1 componentElement的实现
        • 3.2 RenderObjectElement的实现
          • 3.2.1 attachRenderObject函数
      • 4.performRebuild函数
    • 总结三棵树创建流程

三棵树的关系

Flutter 中存在 Widget 、 Element 、RenderObject 三棵树,其中 Widget与 Element 是一对多的关系 ,Element 与 RenderObject 是一一对应的关系。

Element 中持有Widget 和 RenderObject , 而 Element 与 RenderObject 是一一对应的关系(除去 Element 不存在 RenderObject 的情况,如 ComponentElement是不具备 RenderObject),当 RenderObject 的 isRepaintBoundary 为 true 时,那么个区域形成一个 Layer,所以不是每个 RenderObject 都具有 Layer 的,因为这受 isRepaintBoundary 的影响。

Flutter 中 Widget 不可变,每次保持在一帧,如果发生改变是通过 State 实现跨帧状态保存,而真实完成布局和绘制数组的是 RenderObject , Element 充当两者的桥梁, State 就是保存在 Element 中。

一个可能的三棵树实例如下:
在这里插入图片描述

先看下widget继承关系:
在这里插入图片描述

对应的element继承关系:

在这里插入图片描述

通过上图可以看出,每个widget对应一个element类型,但是只有renderObjectElement类持有renderObject,只有renderObjectWidget才能创建render对象。因此flutter视图的三棵树结构,widget和element是一一对应的,但是renderObjectElement才对应一个render节点。

树的构建过程

flutter视图的入口是:

    runApp(const MyApp());class MyApp extends StatelessWidget {const MyApp({super.key});Widget build(BuildContext context) {return MaterialApp(/....省略);}}

跟踪进入runApp函数:

void runApp(Widget app) {//1 final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();assert(binding.debugCheckZone('runApp'));//2binding..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))//3..scheduleWarmUpFrame();
}
  1. 第一步首先确认engine绑定flutter framework,是个阻塞过程
  2. scheduleAttachRootWidget 将app组件添加到根视图树上
  3. 向native平台层请求绘制一帧的信号

先看第二步:

  void attachRootWidget(Widget rootWidget) {final bool isBootstrapFrame = rootElement == null;_readyToProduceFrames = true;_rootElement = RenderObjectToWidgetAdapter<RenderBox>(container: renderView,debugShortDescription: '[root]',child: rootWidget,).attachToRenderTree(buildOwner!, rootElement as RenderObjectToWidgetElement<RenderBox>?);if (isBootstrapFrame) {SchedulerBinding.instance.ensureVisualUpdate();}}

RenderObjectToWidgetAdapter有两个成员,child:孩子widget,container:提供render功能容器,将rootWidget作为的child元素,renderView作为container。重点是attachToRenderTree函数,

  RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {if (element == null) {//。。。省略owner.buildScope(element!, () {element!.mount(null, null);});} else {//。。。省略}return element!;}

第一次element为空,那么进入第一个判断,首先调用buildScope,内部会回调element!.mount方法,

void buildScope(Element context, [ VoidCallback? callback ]) {if (callback == null && _dirtyElements.isEmpty) {return;}try {_scheduledFlushDirtyElements = true;callback();_dirtyElements.sort(Element._sort);_dirtyElementsNeedsResorting = false;int dirtyCount = _dirtyElements.length;int index = 0;while (index < dirtyCount) {final Element element = _dirtyElements[index];element.rebuild();index += 1;//。。。省略}return true;}());}}

首先回调callback,接着对脏元素集合调用rebuild方法。第一次肯定列表是空的,那么应该执行callback方法,也就进入了element.mount方法中,

  void mount(Element? parent, Object? newSlot) {assert(parent == null);super.mount(parent, newSlot);_rebuild();assert(_child != null);}

mount方法也是个关键函数,函数的注释:

将此元素添加到给定父级的给定插槽中的树中。
当新创建的元素添加到
树是第一次。使用此方法初始化状态
取决于有父母。独立于父级的状态可以
更容易在构造函数中初始化。
此方法将元素从“初始”生命周期状态转换为
“活动”生命周期状态。
重写此方法的子类可能也希望重写
[update], [visitChildren], [RenderObjectElement.insertRenderObjectChild],
[RenderObjectElement.moveRenderObjectChild],以及
[RenderObjectElement.removeRenderObjectChild]。
此方法的实现应从调用继承的
方法,如 ‘super.mount(parent, newSlot)’。

注:其中newSlot参数,是parent renderObject的插槽位置对象,parent的子renderObject组成一个顺序列表或者是单个节点,插槽的结构是列表中的索引位置和前一个插槽元素的对象,parent将插槽传递给child保存起来,child可以确认自己在parent的布局位置。

首先调用_rebuild()方法,

RenderObjectToWidgetElement类


void _rebuild() {_child = updateChild(_child, (widget as RenderObjectToWidgetAdapter<T>).child, _rootChildSlot);
}

Element类

Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {//。。。省略final Element newChild;if (child != null) {bool hasSameSuperclass = true;// When the type of a widget is changed between Stateful and Stateless via// hot reload, the element tree will end up in a partially invalid state.// That is, if the widget was a StatefulWidget and is now a StatelessWidget,// then the element tree currently contains a StatefulElement that is incorrectly// referencing a StatelessWidget (and likewise with StatelessElement).//// To avoid crashing due to type errors, we need to gently guide the invalid// element out of the tree. To do so, we ensure that the `hasSameSuperclass` condition// returns false which prevents us from trying to update the existing element// incorrectly.//// For the case where the widget becomes Stateful, we also need to avoid// accessing `StatelessElement.widget` as the cast on the getter will// cause a type error to be thrown. Here we avoid that by short-circuiting// the `Widget.canUpdate` check once `hasSameSuperclass` is false.//1bool hasSameSuperclass = true;final int oldElementClass = Element._debugConcreteSubtype(child);final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);hasSameSuperclass = oldElementClass == newWidgetClass;if (hasSameSuperclass && child.widget == newWidget) {// We don't insert a timeline event here, because otherwise it's// confusing that widgets that "don't update" (because they didn't// change) get "charged" on the timeline.if (child.slot != newSlot) {updateSlotForChild(child, newSlot);}newChild = child;} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {if (child.slot != newSlot) {updateSlotForChild(child, newSlot);}child.update(newWidget);newChild = child;} else {deactivateChild(child);// The [debugProfileBuildsEnabled] code for this branch is inside// [inflateWidget], since some [Element]s call [inflateWidget] directly// instead of going through [updateChild].newChild = inflateWidget(newWidget, newSlot);}} else {// The [debugProfileBuildsEnabled] code for this branch is inside// [inflateWidget], since some [Element]s call [inflateWidget] directly// instead of going through [updateChild].newChild = inflateWidget(newWidget, newSlot);}return newChild;}

1.updateChild函数(element的复用)

更新element对象分为下面几种情况:

  • child不为空
    element的创建,首先判断旧的child element元素是否和新的widget元素class类型匹配,对应匹配关系如下:
elementwidgethasSameSuperclass
StatefulElementStatefulWidgetY
StatelessElementStatelessWidgetY
  1. 如果匹配hasSameSuperclass,并且element.widget和新传递进来的newWidget对象相同,那么说明widget是复用的(我们知道widget是不可变的,每次都要新建widget,所以在使用const定义的widget的情况下,widget使用的常量对象,符合这个判断),对child做slot更新,newChild更新为child。
  2. 如果匹配hasSameSuperclass,并且widget.canUpdate判断成立,这个判断判断element对应的widget和newWidget对应的class类型和key是否相同(这种情况下,如果给widget定义了globalKey,并且参数使用const定义的话,那么判断是可以成立的),对child做slot更新,调用child.update(newWidget),newChild更新为child。
    2.1 child.update方法,首先更新element的widget,然后根据子类的覆写实现,statelessElement的实现是直接调用element的rebuild方法,statefullElement实现是更新state的widget并且回调didUpdateWidget钩子函数,然后调用rebuild方法
  static bool canUpdate(Widget oldWidget, Widget newWidget) {return oldWidget.runtimeType == newWidget.runtimeType&& oldWidget.key == newWidget.key;}
  1. 如果旧的element无法更新的话,需要解除绑定关系,将旧的element回收,用于后面的视图build复用;然后inflateWidget根据newWidget新建一个element。
  void deactivateChild(Element child) {assert(child._parent == this);child._parent = null;child.detachRenderObject();owner!._inactiveElements.add(child); // this eventually calls child.deactivate()}
  • child为空
  1. inflateWidget根据newWidget新建一个element

2.inflateWidget函数

Element inflateWidget(Widget newWidget, Object? newSlot) {//。。。s省略try {final Key? key = newWidget.key;if (key is GlobalKey) {final Element? newChild = _retakeInactiveElement(key, newWidget);if (newChild != null) {newChild._parent == nullnewChild._activateWithParent(this, newSlot);final Element? updatedChild = updateChild(newChild, newWidget, newSlot);return updatedChild!;}}final Element newChild = newWidget.createElement();newChild.mount(this, newSlot);return newChild;}}
  1. 首先判断newWidget是否拥有globalKey,如果有的话尝试从复用池中获取拥有相同key的element元素,然后对该元素进行updateChild操作,这样又回到上面的起点了,这样看起来element更新是深度递归的。
  2. 如果没有key,那么直接创建一个新的element,然后element进行mount挂载操作,也就是将element加入到这个element树中。

3.mount函数

重点看看mount函数:

  void mount(Element? parent, Object? newSlot) {assert(_lifecycleState == _ElementLifecycle.initial);assert(_parent == null);assert(parent == null || parent._lifecycleState == _ElementLifecycle.active);assert(slot == null);_parent = parent;_slot = newSlot;_lifecycleState = _ElementLifecycle.active;_depth = _parent != null ? _parent!.depth + 1 : 1;if (parent != null) {// Only assign ownership if the parent is non-null. If parent is null// (the root node), the owner should have already been assigned.// See RootRenderObjectElement.assignOwner()._owner = parent.owner;}assert(owner != null);final Key? key = widget.key;if (key is GlobalKey) {owner!._registerGlobalKey(key, this);}_updateInheritance();attachNotificationTree();}
  1. 首先对该widget进行参数判断,因为是新的widget,所以依赖关系都是空的,这里需要进行parent关联操作,生命周期设置,树的深度设置,owner是和parent使用的同一个,注册全局key。
  2. _updateInheritance,保存来自parent的_inheritedElements对象,这个对象集合里面包含着这棵树所有的inheritedElement(如果自己也是inheritedElement,也要将自己加入集合),这个类型的element具有从上下文继承数据的功能,可以根据类型读取数据,对应类型数据改变,可以通知所有依赖这个数据的inheritedElement去更新视图。
  3. attachNotificationTree,保存parent的_notificationTree集合,如果自己是NotifiableElementMixin类型,会将自己加入到parent._notificationTree集合中,这个集合是接受通知集合,一般的widget也不需要这个功能(不去深究)。

注: 从上面代码分析可知,mount是element从initial -> active的时间点。

mount是element基本接口,子类会对其进行复写,并且super调用

3.1 componentElement的实现

componentElement类,是负责组合子element的作用的,相当于Android View视图中的viewGroup,但是它也没有绘制功能,仅仅是负责排列组合子element。

component Element的复写:

  void mount(Element? parent, Object? newSlot) {super.mount(parent, newSlot);firstBuild();}void _firstBuild() {rebuild(); // This eventually calls performRebuild.}void rebuild({bool force = false}) {//。。。省略if (_lifecycleState != _ElementLifecycle.active || (!_dirty && !force)) {return;}//。。。省略try {performRebuild();}}

会进行一次rebuild操作,内部直接调用的performRebuild()

3.2 RenderObjectElement的实现

RenderObjectElement类,是起绘制作用的element,相当于Android View视图中的view,视图的正真的绘制操作都在里面实现。

RenderObjectElement复写:

  void mount(Element? parent, Object? newSlot) {super.mount(parent, newSlot);_renderObject = (widget as RenderObjectWidget).createRenderObject(this);attachRenderObject(newSlot);super.performRebuild(); // clears the "dirty" flag}
  • 由RenderObjectWidget创建RenderObject,这个RenderObject是正真实现绘制功能的类。
  • attachRenderObject函数,主要是将parent.newSlot传递给自己,然后和parent建立关联,这个关联主要是renderTree的关联。
3.2.1 attachRenderObject函数
  void attachRenderObject(Object? newSlot) {assert(_ancestorRenderObjectElement == null);_slot = newSlot;_ancestorRenderObjectElement = _findAncestorRenderObjectElement();_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();if (parentDataElement != null) {_updateParentData(parentDataElement.widget as ParentDataWidget<ParentData>);}}
  • _ancestorRenderObjectElement
  RenderObjectElement? _findAncestorRenderObjectElement() {Element? ancestor = _parent;while (ancestor != null && ancestor is! RenderObjectElement) {ancestor = ancestor._parent;}return ancestor as RenderObjectElement?;}

向上递归获取第一个RenderObjectElement类型的祖先,然后将当前子renderObject插入到这个祖先RenderObjectElement的孩子中或者孩子队列中。

可以看出也不是所有的element都是RenderObjectElement类型的,componentElement以及它的子类就不是,那么就会被跳过继续向上递归。
进而也就有了文章开头的那三棵树的关系,widget和element是一对一关系的,build过程中,element创建一定需要widget去配置或者更新,renderObject的创建只有RenderObjectWidget才有这个接口功能,因此像componentElement类型的就没有renderObject。

SingleChildRenderObjectElement类的实现,这个类只包含一个单独的renderObject,直接赋值子child,:

  void insertRenderObjectChild(RenderObject child, Object? slot) {final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject as RenderObjectWithChildMixin<RenderObject>;assert(slot == null);assert(renderObject.debugValidateChild(child));renderObject.child = child;assert(renderObject == this.renderObject);}

MultiChildRenderObjectElement类的实现,这个类包含多个renderObject,按照自己定义的排列规则排列子renderObject,将child插入到子child队列中对应的位置,上面说了slot已经将child的位置定下来了,可以根据slot的位置,将child插入到指定位置。

  void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject = this.renderObject;assert(renderObject.debugValidateChild(child));renderObject.insert(child, after: slot.value?.renderObject);assert(renderObject == this.renderObject);}void insert(ChildType child, { ChildType? after }) {adoptChild(child);_insertIntoChildList(child, after: after);}void adoptChild(RenderObject child) {setupParentData(child);markNeedsLayout();markNeedsCompositingBitsUpdate();markNeedsSemanticsUpdate();child._parent = this;if (attached) {child.attach(_owner!);}redepthChild(child);}

adoptChild函数所作的事情是,绑定parentData数据,child根据parent的布局数据layout的时候有用,接下来就是标记parent需要重新layout,child和parent进行关联。


void _insertIntoChildList(ChildType child, { ChildType? after }) {final ParentDataType childParentData = child.parentData! as ParentDataType;_childCount += 1;assert(_childCount > 0);if (after == null) {// insert at the start (_firstChild)childParentData.nextSibling = _firstChild;if (_firstChild != null) {final ParentDataType firstChildParentData = _firstChild!.parentData! as ParentDataType;firstChildParentData.previousSibling = child;}_firstChild = child;_lastChild ??= child;} else {final ParentDataType afterParentData = after.parentData! as ParentDataType;if (afterParentData.nextSibling == null) {// insert at the end (_lastChild); we'll end up with two or more childrenassert(after == _lastChild);childParentData.previousSibling = after;afterParentData.nextSibling = child;_lastChild = child;} else {// insert in the middle; we'll end up with three or more children// set up links from child to siblingschildParentData.nextSibling = afterParentData.nextSibling;childParentData.previousSibling = after;// set up links from siblings to childfinal ParentDataType childPreviousSiblingParentData = childParentData.previousSibling!.parentData! as ParentDataType;final ParentDataType childNextSiblingParentData = childParentData.nextSibling!.parentData! as ParentDataType;childPreviousSiblingParentData.nextSibling = child;childNextSiblingParentData.previousSibling = child;assert(afterParentData.nextSibling == child);}}}

插入操作,以下几种情况:

  1. 前驱为空,那么当前插入的child是队列的第一个元素,直接插入;
  2. 前驱不为空,如果前驱的next指针为空,那么前驱是尾部元素,child插入到尾部;
  3. 前驱不为空,并且前驱在队列中间,那么将child插入到前驱的后面。
  • parentDataElement
  ParentDataElement<ParentData>? _findAncestorParentDataElement() {Element? ancestor = _parent;ParentDataElement<ParentData>? result;while (ancestor != null && ancestor is! RenderObjectElement) {if (ancestor is ParentDataElement<ParentData>) {result = ancestor;break;}ancestor = ancestor._parent;}}

向上递归获取第一个parentDataElement类型的祖先,然后将当前子renderObject传递给祖先,祖先会验证自己的data和子child的data的数据是否一致,不一致会标记脏,下一帧会重新layout,否则不管。

  void _updateParentData(ParentDataWidget<ParentData> parentDataWidget) {if (applyParentData) {parentDataWidget.applyParentData(renderObject); }}void applyParentData(RenderObject renderObject) {//。。。s省略if (needsLayout) {markNeedsLayout();}}

其中实现了ParentDataWidget的子类有:
在这里插入图片描述
这些布局也说明了一个问题,子child的布局属性变化,会导致parent布局重新layout,这样好像会影响性能。

4.performRebuild函数

performRebuild也是element的基本接口,

  void performRebuild() {_dirty = false;}

不同的子类也会有不同的实现,component Element的复写:

  void performRebuild() {Widget? built;try {built = build();}try {_child = updateChild(_child, built, slot);}}Widget build();
  1. 调用build方法,构建出本element所需要的widget组件;widget的build函数由子类实现提供,componentElement的子类主要有StatelessElement,StatefulElement,ProxyElement三个,
    1.1 StatelessElement使用child的widget来构建,Widget build() => (widget as StatelessWidget).build(this);
    1.2 ProxyElement直接使用child Widget build() => (widget as ProxyWidget).child;
    1.3 StatefulElement使用state来创建 Widget build() => state.build(this);

  2. 调用updateChild,用新构建出的widget去更新旧的child element元素,这个updateChild方法上面已经讲过,可以知道,如果element的子element还有child的话,一直递归调用updateChild函数,可以推测出,element树的构建也是深度递归进行的

总结三棵树创建流程

上面梳理了整个树创建的过程,调用链:updateChild -> inflateWidget -> mount -> performRebuild -> (child -> updateChild递归调用)

假如有以下widget树:

return Container(decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(8.0.r)),border: Border.all(color: CtrColor.lineRegular, width: 1),),child: const Row(children: [Image(image: AssetImage("static/images/ic_net_error.png")),Text("data")],));

可以画出整个树创建的流程图:
在这里插入图片描述

相关文章:

Flutter视图原理之三棵树的建立过程

目录 三棵树的关系树的构建过程1.updateChild函数&#xff08;element的复用&#xff09;2.inflateWidget函数3.mount函数3.1 componentElement的实现3.2 RenderObjectElement的实现3.2.1 attachRenderObject函数 4.performRebuild函数 总结三棵树创建流程 三棵树的关系 Flutt…...

详细解析冒泡排序,JS如何基本实现的。

目录 冒泡排序是什么: 使用冒泡排序是为了什么: DEMO示例: 冒泡排序是什么: 冒泡排序&#xff08;Bubble Sort&#xff09;是一种简单的比较排序算法&#xff0c;它通过多次遍历待排序的元素&#xff0c;比较相邻元素的大小&#xff0c;如果它们的顺序不正确就交换它们&…...

如何消除CSDN博文代码中自动添加的行号

哪里有自定义目录标题 编写CSDN博文&#xff0c;使用代码块的linux命令行&#xff0c;预览时没有代码行号&#xff0c;但发布文章后自动添加了行号。 git clone https://github.com/mikel-brostrom/yolo_tracking.git cd yolo_tracking pip install -v -e .为什么预览和发布的…...

定制效果在线定制印刷系统源码 DIY在线定制系统源码 云印刷定制系统源码手机、PC端实时互通

支持各类产品的在线定制&#xff0c;无论是水杯雨伞U盘还是T恤衬衫四件套&#xff0c;均可轻松进行定制 独创制作间概念&#xff0c;同一套模板可以重复对应不同制作间 手机、PC端实时互通&#xff0c;客户可通过任意途径进行图片上传、编辑&#xff0c;一方修改另一方即时可见…...

算法|每日一题|同积元组|哈希统计

1726.同积元组 原题地址&#xff1a; 力扣每日一题&#xff1a;同积元组 给你一个由 不同 正整数组成的数组 nums &#xff0c;请你返回满足 a * b c * d 的元组 (a, b, c, d) 的数量。其中 a、b、c 和 d 都是 nums 中的元素&#xff0c;且 a ! b ! c ! d 。 class Solution …...

最新AI创作系统ChatGPT网站H5源码V2.6.4+搭建部署教程+支持GPT4.0+支持ai绘画(Midjourney)/支持Prompt预设应用

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统AI绘画系统&#xff0c;支持OpenAI GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署…...

最新!两步 永久禁止谷歌浏览器 Google Chrome 自动更新

先放效果图&#xff1a; CSDN这个问题最火的大哥的用了没用 像他这样连浏览器都打不开 为什么要禁止chrome自动更新 看到很多搞笑的大哥&#xff0c;说为啥要禁止&#xff1b; 我觉得最大的原因就是chromedriver跟不上chrome的自动更新&#xff0c;导致我们做selenium爬虫的…...

在Java中线程和进程的区别

在Java中&#xff0c;线程和进程的区别与一般的操作系统环境下类似&#xff0c;但在Java语言层面上也有一些特点。下面是在Java中线程和进程的区别&#xff1a; 定义&#xff1a;在Java中&#xff0c;进程是指一个正在运行的应用程序实例&#xff0c;而线程是进程中的执行单元。…...

【高危安全通告】Oracle 10月月度安全漏洞预警

近日&#xff0c;安全狗应急响应中心关注到Oracle官方发布安全公告&#xff0c;共披露出在Oracle Weblogic中存在的6个高危漏洞。 漏洞描述 CVE-2023-22069&#xff1a;Oracle Weblogic 远程代码执行漏洞 Oracle WebLogic Server存在远程代码执行漏洞&#xff0c;该漏洞的CVS…...

卷王问卷考试系统SurveyKing,开源调查问卷和考试系统源码

卷王问卷考试系统/SurveyKing是一个功能最强大的开源调查问卷和考试系统&#xff0c;可以快速部署&#xff0c;并适用于各行业。该系统提供了在线表单设计、数据收集、统计和分析等功能&#xff0c;支持20多种题型&#xff0c;多种创建问卷方式和多种问卷设置。 无论您是需要进…...

uniapp开发微信小程序,webview内嵌h5,h5打开pdf地址,解决方案

根据公司要求&#xff0c;让我写一个h5&#xff0c;后续会嵌入到合作公司的微信小程序的webview中&#xff0c;如果是自己公司微信小程序&#xff0c;可以采取先下载下来pdf&#xff0c;然后通过wx.openDocument&#xff0c;进行单纯的预览操作&#xff0c;这个可以根据这个老哥…...

Swift使用Embassy库进行数据采集:热点新闻自动生成器

概述 爬虫程序是一种可以自动从网页上抓取数据的软件。爬虫程序可以用于各种目的&#xff0c;例如搜索引擎、数据分析、内容聚合等。本文将介绍如何使用Swift语言和Embassy库编写一个简单的爬虫程序&#xff0c;该程序可以从新闻网站上采集热点信息&#xff0c;并生成一个简单…...

【AIGC核心技术剖析】改进视频修复的传播和变压器(动态滤除环境中的物体)

基于流的传播和时空变压器是视频修复&#xff08;VI&#xff09;中的两种主流机制。尽管这些组件有效&#xff0c;但它们仍然受到一些影响其性能的限制。以前基于传播的方法在图像域或特征域中单独执行。与学习隔离的全局图像传播可能会由于光流不准确而导致空间错位。此外&…...

Win系统VMware虚拟机安装配置(二)

系统的安装得分两个步骤&#xff0c;第一步得配置一台电脑&#xff0c;选配 cpu&#xff0c;内存&#xff0c;磁盘&#xff0c;网卡等硬 件。第二步才是安装系统。 一、配置电脑 1、 进入 VMware 双击 VMware 图标&#xff0c;看到如下界面。 2、 自定义新的虚拟机 3、…...

基于枚举实现的观察者模式

文章目录 前言一、观察者1.定义一个观察者接口2.察者接口具体实现类 二、主题1.定义一个主题接口2.主题接口具体实现类 三、枚举维护观察者1.定义枚举类维护观察者 四、观察者模式测试1.定义观察者模式测试接口2.观察者模式测试运行结果 前言 本文介绍使用枚举的方式实现的观察…...

基于神经网络的图像识别研究

基于神经网络的图像识别是计算机视觉领域的一个热门研究方向&#xff0c;尤其是深度学习技术的兴起。以下是一些与基于神经网络的图像识别相关的关键主题和研究方向&#xff1a; 1. 卷积神经网络&#xff08;CNN&#xff09;&#xff1a; CNN是图像识别领域最重要的神经网络之…...

基于SSM的工资管理系统

基于SSM的工资管理系统 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringSpringMVCMyBatisVue工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 登录界面 管理员界面 通知公告 考勤管理 工资管理 请假管理 摘要 基于SSM&#xff08;Spring、S…...

微服务负载均衡实践

概述 本文介绍微服务的服务调用和负载均衡&#xff0c;使用spring cloud的loadbalancer及openfeign两种技术来实现。 本文的操作是在微服务的初步使用的基础上进行。 环境说明 jdk1.8 maven3.6.3 mysql8 spring cloud2021.0.8 spring boot2.7.12 idea2022 步骤 改造Eu…...

php定时任务

PHP实现执行定时任务的几种思路详解_php 精准定时任务_我是高手高手高高手的博客-CSDN博客 1.Linux服务器上使用CronTab定时执行php 我们先从相对比较复杂的服务器执行php谈起。服务器上安装了php&#xff0c;就可以执行php文件&#xff0c;无论是否安装了nginx或Apache这样的…...

2.2 如何使用FlinkSQL读取写入到文件系统(HDFS\Local\Hive)

目录 1、文件系统 SQL 连接器 2、如何指定文件系统类型 3、如何指定文件格式 4、读取文件系统 4.1 开启 目录监控 4.2 可用的 Metadata 5、写出文件系统 5.1 创建分区表 5.2 滚动策略、文件合并、分区提交 5.3 指定 Sink Parallelism 6、示例_通过FlinkSQL读取kafk…...

AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

设计模式和设计原则回顾

设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...

VB.net复制Ntag213卡写入UID

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

day52 ResNet18 CBAM

在深度学习的旅程中&#xff0c;我们不断探索如何提升模型的性能。今天&#xff0c;我将分享我在 ResNet18 模型中插入 CBAM&#xff08;Convolutional Block Attention Module&#xff09;模块&#xff0c;并采用分阶段微调策略的实践过程。通过这个过程&#xff0c;我不仅提升…...

AtCoder 第409​场初级竞赛 A~E题解

A Conflict 【题目链接】 原题链接&#xff1a;A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串&#xff0c;只有在同时为 o 时输出 Yes 并结束程序&#xff0c;否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...

根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:

根据万维钢精英日课6的内容&#xff0c;使用AI&#xff08;2025&#xff09;可以参考以下方法&#xff1a; 四个洞见 模型已经比人聪明&#xff1a;以ChatGPT o3为代表的AI非常强大&#xff0c;能运用高级理论解释道理、引用最新学术论文&#xff0c;生成对顶尖科学家都有用的…...

AI书签管理工具开发全记录(十九):嵌入资源处理

1.前言 &#x1f4dd; 在上一篇文章中&#xff0c;我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源&#xff0c;方便后续将资源打包到一个可执行文件中。 2.embed介绍 &#x1f3af; Go 1.16 引入了革命性的 embed 包&#xff0c;彻底改变了静态资源管理的…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存

文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...

网站指纹识别

网站指纹识别 网站的最基本组成&#xff1a;服务器&#xff08;操作系统&#xff09;、中间件&#xff08;web容器&#xff09;、脚本语言、数据厍 为什么要了解这些&#xff1f;举个例子&#xff1a;发现了一个文件读取漏洞&#xff0c;我们需要读/etc/passwd&#xff0c;如…...