透過源碼理解Flutter中widget、state和element的關系

1、framework源碼組成

Flutter中widget、state、element的源碼位于framework.dart中,整個文件6693行(版本Flutter 3.12.0-14.0.pre.28)。整個代碼可劃分為若干部分,主要包括key、widget、state、element四部分。

1.1 key

關于key的代碼65行到272行,這里的key包括ObjectKey、GlobalKey、LabeledGlobalKey、GlobalObjectKey。整個key體系的代碼還包括key.dart這個文件,里面包括Key、LocalKey、UniqueKey和ValueKey。Key是GlobalKey和LocalKey的抽象基類。LabeledGlobalKey和GlobalObjectKey是GlobalKey的抽象子類。ObjectKey、UniqueKey和ValueKey是LocalKey的三個具體子類。

1.2 widget

關于widget的代碼274行到1922行。包括10個抽象類:Widget、StatelessWidget、StatefulWidget、ProxyWidget、ParentDataWidget、InheritedWidget、RenderObjectWidget、LeafRenderObjectWidget、SingleChildRenderObjectWidget、MultiChildRenderObjectWidget。這些類,大體可以把Widget分為組合式、渲染、功能性三種。

1.3 state

State的代碼在823附近。State是一個抽象類。State首先起著一個樞紐的作用,它持有widget,也持有element。從state里獲取context,只是簡單返回持有的element。另一方面,State對外提供了widget的生命周期:initState、didUpdateWidget、reassemble、deactivate、activate、dispose、didChangeDependencies。這些生命周期方法是系統提供給我們的鉤子。如果我們要主動發起渲染請求的話,就要調用State提供給我們的setState方法。而build則是我們告訴系統如何渲染這個widget的地方。前者提供時機,后者提供內容。

1.4 BuildContext

BuildContext是一個抽象類,代碼位于2129-2485行。Element實現了BuildContext。

1.5 BuildOwner

BuildOwner位于2511-3168行。

1.6 element

element相關的代碼位于3259行到6597行之間。接近一半的代碼量,可以看出element是核心部分。

2 StatelessWidget和StatefulWidget

Widget是一個抽象類,里面關鍵的三個東西:

final Key? key;Element createElement();static bool canUpdate(Widget oldWidget, Widget newWidget) {return oldWidget.runtimeType == newWidget.runtimeType&& oldWidget.key == newWidget.key;}

我們要關注下canUpdate這個方法的實現,決定我們能否復用這個widget是由這個widget的runtimeType和key決定的。runtimeType表明了widget的類型,不同類型的widget是不能復用的。key是我們人為指定的一個值,它可以在同類型widget之間產生個體差異,以便我們或者渲染系統找到它。不設置的時候就是null,這時候只比較runtimeType就行。

我們先來介紹widget中的組合式子類:StatelessWidget和StatefulWidget。StatelessWidget創建的element是StatelessElement:

@overrideStatelessElement createElement() => StatelessElement(this);

而StatefulWidget創建的是StatefulElement,并且還能創建state:

@overrideStatefulElement createElement() => StatefulElement(this);@protected@factoryState createState();

StatelessWidget和StatefulWidget仍然是抽象類,需要我們子類化。根據源碼,我們發現StatefulWidget和StatelessWidget只負責創建對應element,并不持有它。而Statefulwidget只負責創建state,同樣也并不持有它。

3 RenderObjectWidget

RenderObjectWidget有三個抽象子類LeafRenderObjectWidget、SingleChildRenderObjectWidget、MultiChildRenderObjectWidget。分別代表了沒有孩子、只有一個孩子和有多個孩子的三種RenderObjectWidget。源碼我們不再展開,想必你也猜到了是一個啥都沒,一個有個child,最后一個有個children屬性罷了。

相比于前面的StatelessWidget,RenderObjectWidget返回自己獨特的RenderObjectElement,并且還多了一個RenderObject:

RenderObject createRenderObject(BuildContext context);@protectedvoid updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }@protectedvoid didUnmountRenderObject(covariant RenderObject renderObject) { }

RenderObjectWidget我們后面在RenderObjectElement的performRebuild里講到了,它會調用updateRenderObject進行更新。這里我們無法展開講updateRenderObject,需要去看具體子類里的實現。?

3.1 ColoredBox

我們以SingleChildRenderObjectWidget的一個具體子類ColoredBox為例:

final Color color;@overrideRenderObject createRenderObject(BuildContext context) {return _RenderColoredBox(color: color);}@overridevoid updateRenderObject(BuildContext context, RenderObject renderObject) {(renderObject as _RenderColoredBox).color = color;}

我們發現它創建的RenderObject是一個私有類_RenderColoredBox。而我們前面提到的updateRenderObject在這里只是設置一下新的顏色。

我們再轉去看_RenderColoredBox的實現:

class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {_RenderColoredBox({ required Color color }): _color = color,super(behavior: HitTestBehavior.opaque);/// The fill color for this render object.////// This parameter must not be null.Color get color => _color;Color _color;set color(Color value) {if (value == _color) {return;}_color = value;markNeedsPaint();}@overridevoid paint(PaintingContext context, Offset offset) {// It's tempting to want to optimize out this `drawRect()` call if the// color is transparent (alpha==0), but doing so would be incorrect. See// https://github.com/flutter/flutter/pull/72526#issuecomment-749185938 for// a good description of why.if (size > Size.zero) {context.canvas.drawRect(offset & size, Paint()..color = color);}if (child != null) {context.paintChild(child!, offset);}}
}

主要是根據顏色在canvas上繪制背景色和child。設置新顏色會引起markNeedsPaint。markNeedsPaint相關代碼在RenderObject里。

3.2 markNeedsPaint

void markNeedsPaint() {if (_needsPaint) {return;}_needsPaint = true;// If this was not previously a repaint boundary it will not have// a layer we can paint from.if (isRepaintBoundary && _wasRepaintBoundary) {if (owner != null) {owner!._nodesNeedingPaint.add(this);owner!.requestVisualUpdate();}} else if (parent is RenderObject) {parent!.markNeedsPaint();} else {// If we are the root of the render tree and not a repaint boundary// then we have to paint ourselves, since nobody else can paint us.// We don't add ourselves to _nodesNeedingPaint in this case,// because the root is always told to paint regardless.//// Trees rooted at a RenderView do not go through this// code path because RenderViews are repaint boundaries.if (owner != null) {owner!.requestVisualUpdate();}}}

這里出現了新的Owner:PipelineOwner。markNeedsPaint標記自己需要重新繪制,如果自己是繪制邊界,就把自己加入需要繪制的節點列表里。如果不是繪制邊界,就調用父節點的markNeedsPaint。這里只是簡單標記和放入列表,真正執行繪制的時機是在WidgetsBinding.drawFrame里的flushPaint:

void drawFrame() {buildOwner!.buildScope(renderViewElement!); // 1.重新構建widgetsuper.drawFrame();//下面幾個是在super.drawFrame()執行的pipelineOwner.flushLayout();          // 2.更新布局pipelineOwner.flushCompositingBits();     //3.更新“層合成”信息pipelineOwner.flushPaint();               // 4.重繪if (sendFramesToEngine) {renderView.compositeFrame();            // 5. 上屏,將繪制出的bit數據發送給GPU}
}

3.3 markNeedsLayout

上面的代碼里,我們也看到了布局是在這之前的flushLayout執行的。RenderBox源碼里PipelineOwner通過markNeedsLayout標記、收集需要布局節點:

void markNeedsLayout() {if (_needsLayout) {return;}if (_relayoutBoundary == null) {_needsLayout = true;if (parent != null) {// _relayoutBoundary is cleaned by an ancestor in RenderObject.layout.// Conservatively mark everything dirty until it reaches the closest// known relayout boundary.markParentNeedsLayout();}return;}if (_relayoutBoundary != this) {markParentNeedsLayout();} else {_needsLayout = true;if (owner != null) {owner!._nodesNeedingLayout.add(this);owner!.requestVisualUpdate();}}}void markParentNeedsLayout() {assert(_debugCanPerformMutations);_needsLayout = true;assert(this.parent != null);final RenderObject parent = this.parent!;if (!_doingThisLayoutWithCallback) {parent.markNeedsLayout();} else {assert(parent._debugDoingThisLayout);}assert(parent == this.parent);}

我們發現布局標記和繪制標記的實現是類似的,都需要標記自身,都需要向上尋找布局或者繪制的邊界。PipelineOwner最終對其調用performLayout和markNeedsPaint:

void flushLayout() {try {while (_nodesNeedingLayout.isNotEmpty) {final List<RenderObject> dirtyNodes = _nodesNeedingLayout;_nodesNeedingLayout = <RenderObject>[];dirtyNodes.sort((RenderObject a, RenderObject b) => a.depth - b.depth);for (int i = 0; i < dirtyNodes.length; i++) {if (_shouldMergeDirtyNodes) {_shouldMergeDirtyNodes = false;if (_nodesNeedingLayout.isNotEmpty) {_nodesNeedingLayout.addAll(dirtyNodes.getRange(i, dirtyNodes.length));break;}}final RenderObject node = dirtyNodes[i];if (node._needsLayout && node.owner == this) {node._layoutWithoutResize();}}// No need to merge dirty nodes generated from processing the last// relayout boundary back._shouldMergeDirtyNodes = false;}for (final PipelineOwner child in _children) {child.flushLayout();}} finally {_shouldMergeDirtyNodes = false;}}void _layoutWithoutResize() {RenderObject? debugPreviousActiveLayout;try {performLayout();markNeedsSemanticsUpdate();} catch (e, stack) {_reportException('performLayout', e, stack);}_needsLayout = false;markNeedsPaint();}

performLayout這個方法在RenderBox里實現為空,需要子類自行實現。

前面ColoredBox這個例子里我們在updateRenderObject里改變顏色并不會引起布局變化。現在我們找一個RenderPositionedBox的源碼來看看。

3.3 RenderPositionedBox

RenderPositionedBox是Align使用的renderObject。我們看看它的updateRenderObject實現:

void updateRenderObject(BuildContext context, RenderPositionedBox renderObject) {renderObject..alignment = alignment..widthFactor = widthFactor..heightFactor = heightFactor..textDirection = Directionality.maybeOf(context);}

再進到RenderPositionedBox的set alignment實現:

set alignment(AlignmentGeometry value) {if (_alignment == value) {return;}_alignment = value;_markNeedResolution();}void _markNeedResolution() {_resolvedAlignment = null;markNeedsLayout();}

我們發現設置新的alignment,會引起markNeedsLayout的調用。

4 三個功能性widget:ProxyWidget、ParentDataWidget、InheritedWidget

暫不展開

5 StatefulElement和StatelessElement

我們再跳到StatelessElement構造方法:


class StatelessElement extends ComponentElement {/// Creates an element that uses the given widget as its configuration.StatelessElement(StatelessWidget super.widget);@overrideWidget build() => (widget as StatelessWidget).build(this);
}

StatelessElement自身可以通過build返回子element對應的widget。

而StatefulElement構造方法:

 /// Creates an element that uses the given widget as its configuration.StatefulElement(StatefulWidget widget): _state = widget.createState(),super(widget) {state._element = this;state._widget = widget;}

我們發現在創建element的時候,會先調用widget的createState創建state,并指向它,然后state就伸出兩只手,一只手拉著widget,另一只手拉著element。element里面有一個重要的方法:

  Widget build() => state.build(this);

這里我們可以認為state build出來的是element持有的widget的“child”。事實上,無論StatelessElement還是Statefulwidget,它們都沒child這個概念,但是對應的element是有一個child的屬性的。所以我們姑且這么看待它們的關系。這里把element傳進去,只是因為我們可能需要用到element樹一些上下文信息。

3.1 setState

現在看看我們的老朋友,state里的setState方法的實現:

  void setState(VoidCallback fn) {_element!.markNeedsBuild();}void markNeedsBuild() {if (dirty) {return;}_dirty = true;owner!.scheduleBuildFor(this);}void scheduleBuildFor(Element element) {_dirtyElements.add(element);element._inDirtyList = true;}void rebuild() {performRebuild();}void performRebuild() {_dirty = false;}

完整的流程如下:

中間省略一些代碼,我們直接跳到performRebuild實現。對于基類Element的實現,只是簡單標記為dirty。Element分為渲染和組件兩種類型,前者與渲染相關,后者用于組成其他element。

3.2 performRebuild

對于跟渲染相關的RenderObjectElement的performRebuild,則需要更新它的renderObject:

void _performRebuild() {(widget as RenderObjectWidget).updateRenderObject(this, renderObject);super.performRebuild(); // clears the "dirty" flag}

對于跟組件相關的ComponentElement的performRebuild實現:

  void performRebuild() {Widget? built;try {built = build();} catch (e, stack) {} finally {// We delay marking the element as clean until after calling build() so// that attempts to markNeedsBuild() during build() will be ignored.super.performRebuild(); // clears the "dirty" flag}try {_child = updateChild(_child, built, slot);} catch (e, stack) {_child = updateChild(null, built, slot);}}

這里的核心是會調用build方法創建新的widget,然后使用這個widget去更新child element。從前面的代碼中我們可以看到statefulElement和statelessElement的build實現是有差異的。但是返回的widget,其實都是它們的child對應的widget。在多個渲染周期,child element會一直存在,而需要更新時widget就會重新創建。更新后的Element設置為當前Element的child。至于怎么更新,我們等下再講。

ComponentElement是ProxyElement、StatefulElement和StatelessElement的父類。但是只有StatefulElement覆寫了performRebuild。進一步來到StatefulElement的performRebuild實現:

void performRebuild() {if (_didChangeDependencies) {state.didChangeDependencies();_didChangeDependencies = false;}super.performRebuild();}

StatefulElement增加的任務是如果依賴發生了變化,要觸發state的didChangeDependencies方法。

3.3 updateChild

回到前文,我們再來看Element的updateChild實現:

Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {// 如果'newWidget'為null,而'child'不為null,那么我們刪除'child',返回null。if (newWidget == null) {if (child != null) {deactivateChild(child);}return null;}final Element newChild;if (child != null) {// 兩個widget相同,位置不同更新位置。先更新位置,然后返回child。這里比較的是hashCodeif (child.widget == newWidget) {if (child.slot != newSlot) {updateSlotForChild(child, newSlot);}newChild = child;} else if (Widget.canUpdate(child.widget, newWidget)) {//兩個widget不同,但是可以復用。位置不同則先更新位置。然后用新widget更新elementif (child.slot != newSlot) {updateSlotForChild(child, newSlot);}child.update(newWidget);newChild = child;} else {// 如果無法更新復用,那么刪除原來的child,然后創建一個新的Element并返回。deactivateChild(child);newChild = inflateWidget(newWidget, newSlot);}} else {// 如果是初次創建,那么創建一個新的Element并返回。newChild = inflateWidget(newWidget, newSlot);}return newChild;}

這里關鍵的兩個方法是update和inflateWidget。

3.4 update

對于不同類型的child的update方法是不一樣的。基類Element只是用新的替換舊的而已:

void update(covariant Widget newWidget) {_widget = newWidget;}

對于StatelessElement的update,就是直接更換widget:

 @overridevoid update(StatelessWidget newWidget) {//直接更換widgetsuper.update(newWidget);assert(widget == newWidget);rebuild(force: true);}

對于StatefulElement的update,除了更換widget,還要更換state指向的widget:

void update(StatefulWidget newWidget) {super.update(newWidget);final StatefulWidget oldWidget = state._widget!;state._widget = widget as StatefulWidget;final Object? debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;rebuild(force: true);}

最后都通過調用rebuild,標記自身dirty。

對于SingleChildRenderObjectElement就是對它的child調用updateChild,對于MultiChildRenderObjectElement就是對它的children調用updateChildren:

void update(SingleChildRenderObjectWidget newWidget) {super.update(newWidget);_child = updateChild(_child, (widget as SingleChildRenderObjectWidget).child, null);}void update(MultiChildRenderObjectWidget newWidget) {super.update(newWidget);final MultiChildRenderObjectWidget multiChildRenderObjectWidget = widget as MultiChildRenderObjectWidget;_children = updateChildren(_children, multiChildRenderObjectWidget.children, forgottenChildren: _forgottenChildren);_forgottenChildren.clear();}

而對于ProxyElement主要是更新widget和通知:

@overridevoid update(ProxyWidget newWidget) {final ProxyWidget oldWidget = widget as ProxyWidget;//使用新的widget更新持有的widgetsuper.update(newWidget);//通知其他關聯widget自己發生了變化updated(oldWidget);//標記dirtyrebuild(force: true);}@protectedvoid updated(covariant ProxyWidget oldWidget) {notifyClients(oldWidget);}

3.5 updateChildren

updateChild前面我們已經提到了,而對于updateChildren的實現:

List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element>? forgottenChildren, List<Object?>? slots }) {Element? replaceWithNullIfForgotten(Element child) {return forgottenChildren != null && forgottenChildren.contains(child) ? null : child;}Object? slotFor(int newChildIndex, Element? previousChild) {return slots != null? slots[newChildIndex]: IndexedSlot<Element?>(newChildIndex, previousChild);}// This attempts to diff the new child list (newWidgets) with// the old child list (oldChildren), and produce a new list of elements to// be the new list of child elements of this element. The called of this// method is expected to update this render object accordingly.// The cases it tries to optimize for are://  - the old list is empty//  - the lists are identical//  - there is an insertion or removal of one or more widgets in//    only one place in the list// If a widget with a key is in both lists, it will be synced.// Widgets without keys might be synced but there is no guarantee.// The general approach is to sync the entire new list backwards, as follows:// 1. Walk the lists from the top, syncing nodes, until you no longer have//    matching nodes.// 2. Walk the lists from the bottom, without syncing nodes, until you no//    longer have matching nodes. We'll sync these nodes at the end. We//    don't sync them now because we want to sync all the nodes in order//    from beginning to end.// At this point we narrowed the old and new lists to the point// where the nodes no longer match.// 3. Walk the narrowed part of the old list to get the list of//    keys and sync null with non-keyed items.// 4. Walk the narrowed part of the new list forwards://     * Sync non-keyed items with null//     * Sync keyed items with the source if it exists, else with null.// 5. Walk the bottom of the list again, syncing the nodes.// 6. Sync null with any items in the list of keys that are still//    mounted.int newChildrenTop = 0;int oldChildrenTop = 0;int newChildrenBottom = newWidgets.length - 1;int oldChildrenBottom = oldChildren.length - 1;final List<Element> newChildren = List<Element>.filled(newWidgets.length, _NullElement.instance);Element? previousChild;// 從前往后依次對比,相同的更新Element,記錄位置,直到不相等時跳出循環。.while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);final Widget newWidget = newWidgets[newChildrenTop];// 注意這里的canUpdate,本例中在沒有添加key時返回true。// 因此直接執行updateChild,本循環結束返回newChildren。后面因條件不滿足都在不執行。// 一旦添加key,這里返回false,不同之處就此開始。if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) {break;}final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;newChildren[newChildrenTop] = newChild;previousChild = newChild;newChildrenTop += 1;oldChildrenTop += 1;}// 從后往前依次對比,記錄位置,直到不相等時跳出循環。while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);final Widget newWidget = newWidgets[newChildrenBottom];if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) {break;}oldChildrenBottom -= 1;newChildrenBottom -= 1;}// 至此,就可以得到新舊List中不同Weiget的范圍。final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;Map<Key, Element>? oldKeyedChildren;
// 如果存在中間范圍,掃描舊children,獲取所有的key與Element保存至oldKeyedChildren。if (haveOldChildren) {oldKeyedChildren = <Key, Element>{};while (oldChildrenTop <= oldChildrenBottom) {final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);if (oldChild != null) {if (oldChild.widget.key != null) {oldKeyedChildren[oldChild.widget.key!] = oldChild;} else {deactivateChild(oldChild);}}oldChildrenTop += 1;}}// 更新中間不同的部分,如果新舊key相同就更新一下重新利用,否則新的widget就沒有舊的對應,是插入行為while (newChildrenTop <= newChildrenBottom) {Element? oldChild;final Widget newWidget = newWidgets[newChildrenTop];if (haveOldChildren) {final Key? key = newWidget.key;if (key != null) {// key不為null,通過key獲取對應的舊ElementoldChild = oldKeyedChildren![key];if (oldChild != null) {if (Widget.canUpdate(oldChild.widget, newWidget)) {// we found a match!// remove it from oldKeyedChildren so we don't unsync it lateroldKeyedChildren.remove(key);} else {// Not a match, let's pretend we didn't see it for now.oldChild = null;}}}}final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;newChildren[newChildrenTop] = newChild;previousChild = newChild;newChildrenTop += 1;}// We've scanned the whole list.// 重置newChildrenBottom = newWidgets.length - 1;oldChildrenBottom = oldChildren.length - 1;// 將后面相同的Element更新后添加到newChildren,至此形成新的完整的children。while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {final Element oldChild = oldChildren[oldChildrenTop];final Widget newWidget = newWidgets[newChildrenTop];final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;newChildren[newChildrenTop] = newChild;previousChild = newChild;newChildrenTop += 1;oldChildrenTop += 1;}// 清除舊列表中多余的帶key的Elementif (haveOldChildren && oldKeyedChildren!.isNotEmpty) {for (final Element oldChild in oldKeyedChildren.values) {if (forgottenChildren == null || !forgottenChildren.contains(oldChild)) {deactivateChild(oldChild);}}}return newChildren;}

dif算法相對比較復雜,可能理解起來比較困難。值得一提的是,無論 updateChild還是updateChildren都實現在基類element里。同層diff算法里使用key并不是出于性能考慮,沒有key能夠就地復用,使用key能夠指定復用對象。有時候就地復用會有一些問題,譬如某個widget自身有一些狀態,你如果就地復用其他widget,就會導致這些狀態的丟失。

3.6 inflateWidget

再來看看inflateWidget的實現,它主要是用來創建新的element,并且mount。如果widget有GlobalKey的話,則會嘗試獲取對應的element,然后更新后返回。

Element inflateWidget(Widget newWidget, Object? newSlot) {try {//如果widget帶key,并且是GlobalKey,則嘗試獲取一下對應的element,并用新的widget更新它然后返回final Key? key = newWidget.key;if (key is GlobalKey) {final Element? newChild = _retakeInactiveElement(key, newWidget);if (newChild != null) {newChild._activateWithParent(this, newSlot);final Element? updatedChild = updateChild(newChild, newWidget, newSlot);return updatedChild!;}}// 這里就調用到了createElement,重新創建了Elementfinal Element newChild = newWidget.createElement();newChild.mount(this, newSlot);return newChild;} }

3.7 mount

我們再來看看element基類的mount:

void mount(Element? parent, Object? newSlot) {_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;}final Key? key = widget.key;if (key is GlobalKey) {owner!._registerGlobalKey(key, this);}_updateInheritance();attachNotificationTree();}

mount就是將自身插入父element的某個slot中。我們發現Element在mount的時候,會將父element的ower設置給自己。如果widget帶有key,那么ower會將這個element注冊到自己的map里。

而對于組合式Element的mount有所差異,除了上述基類行為,還會調用_firstBuild:

@overridevoid mount(Element? parent, Object? newSlot) {super.mount(parent, newSlot);_firstBuild();}void _firstBuild() {// StatefulElement overrides this to also call state.didChangeDependencies.rebuild(); // This eventually calls performRebuild.}

對于StatelessElement,_firstBuild的實現只是單純rebuild一下。而對于StatefulElement:

@overridevoid _firstBuild() {final Object? debugCheckForReturnedFuture = state.initState() as dynamic;state.didChangeDependencies();super._firstBuild();}

我們發現_firstBuild里調用了state的initState方法,這里說明我們在state里實現的生命周期方法,其實會被StatefulElement根據自身的不同狀態而調用。因此其他方法我們不再贅述。

3.8 why?

在參考文章里有一個問題,我們來分析一下,增加我們對本文的理解程度。現在我們有如下一段代碼:

import 'dart:math';import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(title: 'Flutter Demo',theme: ThemeData(primarySwatch: Colors.blue,),home: MyHomePage(title: 'Home Page'),);}
}class MyHomePage extends StatefulWidget {MyHomePage({Key key, this.title}) : super(key: key);final String title;@override_MyHomePageState createState() => _MyHomePageState();
}class _MyHomePageState extends State<MyHomePage> {List<Widget> widgets;@overridevoid initState() {super.initState();widgets = [StatelessColorfulTile(),StatelessColorfulTile()];}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text(widget.title),),body: Row(children: widgets,),floatingActionButton: FloatingActionButton(child: Icon(Icons.refresh),onPressed: _swapTile,),);}_swapTile() {setState(() {widgets.insert(1, widgets.removeAt(0));});}
}class StatelessColorfulTile extends StatelessWidget {final Color _color = Utils.randomColor();@overrideWidget build(BuildContext context) {return Container(height: 150,width: 150,color: _color,);}
}class Utils {static Color randomColor() {var red = Random.secure().nextInt(255);var greed = Random.secure().nextInt(255);var blue = Random.secure().nextInt(255);return Color.fromARGB(255, red, greed, blue);}
}

代碼可以直接復制到DartPad中運行查看效果。 或者點擊這里直接運行。

效果很簡單,就是兩個彩色方塊,點擊右下角的按鈕后交換兩個方塊的位置。上面的方塊是StatelessWidget,那我們把它換成StatefulWidget呢?。

class StatefulColorfulTile extends StatefulWidget {StatefulColorfulTile({Key key}) : super(key: key);@overrideStatefulColorfulTileState createState() => StatefulColorfulTileState();
}class StatefulColorfulTileState extends State<StatefulColorfulTile> {final Color _color = Utils.randomColor();@overrideWidget build(BuildContext context) {return Container(height: 150,width: 150,color: _color,);}
}

?再次執行代碼,發現方塊沒有“交換”。這是為什么?結論是widget層面而言,兩個widget的確發生了交換,但是Element并沒有發生交換,原來位置的Element持有的state build出原來顏色的Container。

6 key

可以看參考,這里暫不展開

7 BuildOwner

buildOwner是framework這些代碼背后的大boss。我們來看看它做了哪些事情。每個element都指向一個Owner用來維護它的生命周期:

BuildOwner? get owner => _owner;
BuildOwner? _owner;

為什么我們能用globalKey找到對應的element,沒有什么神奇的,因為buildOwner有一個map維護著globalKey和element的對應關系:

  final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{};void _registerGlobalKey(GlobalKey key, Element element)
void _unregisterGlobalKey(GlobalKey key, Element element)

buildOwner另一個作用是維護著element的build列表:

  final List<Element> _dirtyElements = <Element>[];void scheduleBuildFor(Element element) {if (element._inDirtyList) {_dirtyElementsNeedsResorting = true;return;}if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {_scheduledFlushDirtyElements = true;onBuildScheduled!();}_dirtyElements.add(element);element._inDirtyList = true;}

WidgetsBinding會通過WidgetsBinding.drawFrame調用buildOwner的buildScope:

void drawFrame() {buildOwner!.buildScope(renderViewElement!); // 1.重新構建widgetsuper.drawFrame();//下面幾個是在super.drawFrame()執行的pipelineOwner.flushLayout();          // 2.更新布局pipelineOwner.flushCompositingBits();     //3.更新“層合成”信息pipelineOwner.flushPaint();               // 4.重繪if (sendFramesToEngine) {renderView.compositeFrame();            // 5. 上屏,將繪制出的bit數據發送給GPU}
}

buildScope對_dirtyElements里的element調用rebuild:

void buildScope(Element context, [ VoidCallback? callback ]) {if (callback == null && _dirtyElements.isEmpty) {return;}try {_scheduledFlushDirtyElements = true;if (callback != null) {_dirtyElementsNeedsResorting = false;try {callback();}}_dirtyElements.sort(Element._sort);_dirtyElementsNeedsResorting = false;int dirtyCount = _dirtyElements.length;int index = 0;while (index < dirtyCount) {final Element element = _dirtyElements[index];try {element.rebuild();} index += 1;if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting!) {_dirtyElements.sort(Element._sort);_dirtyElementsNeedsResorting = false;dirtyCount = _dirtyElements.length;while (index > 0 && _dirtyElements[index - 1].dirty) {// It is possible for previously dirty but inactive widgets to move right in the list.// We therefore have to move the index left in the list to account for this.// We don't know how many could have moved. However, we do know that the only possible// change to the list is that nodes that were previously to the left of the index have// now moved to be to the right of the right-most cleaned node, and we do know that// all the clean nodes were to the left of the index. So we move the index left// until just after the right-most clean node.index -= 1;}}}} finally {for (final Element element in _dirtyElements) {assert(element._inDirtyList);element._inDirtyList = false;}_dirtyElements.clear();_scheduledFlushDirtyElements = false;_dirtyElementsNeedsResorting = null;}}

后面的流程就回到了我們前面的performRebuild方法 。

8 沒有覆蓋的內容

本文沒有提及具體的布局邏輯,將在后面的文章里進行講述。

9 圖示

文中出現的一些關鍵類的繼承關系:

參考

1.說說Flutter中最熟悉的陌生人 —— Key_flutter globalkey 源碼_唯鹿的博客-CSDN博客

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/43575.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/43575.shtml
英文地址,請注明出處:http://en.pswp.cn/news/43575.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

NVIDIA GPU驅動和CUDA工具包 Linux CentOS 7 在線安裝指南

挑選指定系統和對應的GPU型號下載驅動和CUDA工具包: Linux CentOS安裝NVIDIA GPU驅動程序和NVIDIA CUDA工具包_centos安裝顯卡驅動和cuda_Entropy-Go的博客-CSDN博客 相比之下&#xff0c;本文是在線安裝NVIDIA GPU驅動和CUDA工具包方式&#xff0c;省去挑選對應正確安裝包的煩…

Uniapp Syntax Error: Error: Unbalanced delimiter found in string

報錯 in ./src/pages/user/components/tasks.vue?vue&typescript&langjs&Syntax Error: Error: Unbalanced delimiter found in string...這邊導致文件的原因&#xff1a;可能是條件編譯語法不小心刪了某個字符&#xff0c;導致不全&#xff0c;無法形成一對。 //…

GuLi商城-前端基礎Vue-生命周期和鉤子函數

下圖展示了實例的生命周期。你不需要立馬弄明白所有的東西&#xff0c;不過隨著你的不斷學習和使用&#xff0c;它 的參考價值會越來越高。 VUE 的生命周期指的是組件在創建、運行和銷毀過程中所經歷的一系列事件&#xff0c;通過這些事件可以 讓開發者在不同階段進行相應的…

vue3 + antv/x6 實現拖拽側邊欄節點到畫布

前篇&#xff1a;vue3ts使用antv/x6 自定義節點 前篇&#xff1a;vue3antv x6自定義節點樣式 1、創建側邊欄 用antd的menu來做側邊欄 npm i --save ant-design-vue4.x//入口文件main.js內 import Antd from ant-design-vue; import App from ./App; import ant-design-vue/…

安卓的代碼加固和其他安全問題

文章目錄 安卓加固apk文件結構dex加固過程 其它安全問題 安卓加固 從App的加固技術來看:主流分為dex加密和so加密,目前來看保護dex文件更為重要,因為dex反編譯后的java代碼可讀性更強。 android-ndk: Native Development Kit 官網解釋&#xff1a;這套工具使您能在 Android 應…

Kvm配置ovs網橋

環境&#xff1a;部署在kvm虛擬環境上&#xff08;讓虛擬機和宿主機都可以直接從路由器獲取到獨立ip&#xff09; 1、安裝ovs軟件安裝包并啟動服務&#xff08;一般采用源碼安裝&#xff0c;此處用yum安裝&#xff09; yum install openvswitch-2.9.0-3.el7.x86_64.rpm syste…

Git常見操作

一、全局配置命令 配置級別&#xff1a; –local&#xff08;默認&#xff0c;高級優先&#xff09;&#xff1a;只影響本地倉庫 –global(中優先級)&#xff1a;只影響所有當前用戶的git倉庫 –system&#xff08;低優先級&#xff09;&#xff1a;影響到全系統的git倉庫 1…

【力扣】739. 每日溫度 <單調棧>

【力扣】739. 每日溫度 給定一個整數數組 temperatures &#xff0c;表示每天的溫度&#xff0c;返回一個數組 answer &#xff0c;其中 answer[i] 是指對于第 i 天&#xff0c;下一個更高溫度出現在幾天后。如果氣溫在這之后都不會升高&#xff0c;請在該位置用 0 來代替。 …

劍指 Offer 40. 最小的k個數(C+實現)

劍指 Offer 40. 最小的k個數https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/ 法1&#xff1a;二叉堆 通過最小堆&#xff0c;直接篩選出最小的k個數 vector<int> getLeastNumbers(vector<int>& arr, int k) {priority_queue<int, vector<int>…

YOLOv8改進后效果

數據集 自建鐵路障礙數據集-包含路障&#xff0c;人等少數標簽。其中百分之八十作為訓練集&#xff0c;百分之二十作為測試集 第一次部署 版本&#xff1a;YOLOv5 訓練50epoch后精度可達0.94 mAP可達0.95.此時未包含任何改進操作 第二次部署 版本&#xff1a;YOLOv8改進版本 首…

WebRTC | ICE詳解

目錄 一、Candidate種類與優先級 二、ICE策略 1. iceServers 2. iceTransportPolicy 三、P2P連接 1.Nat類型 &#xff08;1&#xff09;完全錐型NAT &#xff08;2&#xff09;IP限制錐型NAT &#xff08;3&#xff09;端口限制錐型NAT &#xff08;4&#xff09;對稱…

iPhone 15受益:驍龍8 Gen 3可能缺席部分安卓旗艦機

明年一批領先的安卓手機的性能可能與今年的機型非常相似。硅成本的上漲可能是原因。 你可以想象&#xff0c;2024年許多最好的手機都會在Snapdragon 8 Gen 3上運行&#xff0c;這是高通公司針對移動設備的頂級芯片系統的更新&#xff0c;尚未宣布。然而&#xff0c;來自中國的…

centos上下載redis

1.redis 特點 Redis特性&#xff08;8個&#xff09; 1 速度快&#xff1a;10w ops&#xff08;每秒10w讀寫&#xff09;&#xff0c;數據存在內存中&#xff0c;c語言實現&#xff0c;單線程模型 2 持久化&#xff1a;rdb和aof 3 多種數據結構&#xff1a; 5大數據結構 …

Vue中實現分頁

1.構造分頁組件&#xff0c;并注冊為全局組件 <template><div class"pagination"><button v-if"startNumAndEndNum.start>1" click"$emit(getPageNo,pageNo-1)">上一頁</button><button v-if"startNumAndEn…

C#生產流程控制(串行,并行混合執行)

開源框架CsGo https://gitee.com/hamasm/CsGo?_fromgitee_search 文檔資料&#xff1a; https://blog.csdn.net/aa2528877987/article/details/132139337 實現效果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37…

Windows11 Docker Desktop 啟動 -wsl kernel version too low

系統環境&#xff1a;windows11 1&#xff1a;docker下載 Docker: Accelerated Container Application Development 下載后雙擊安裝即可 安裝后啟動Docker提示&#xff1a;Docker Desktop -wsl kernel version too low 處理起來也是非常方便 1:管理員身份啟動&#xff1a;…

C#程序隨系統啟動例子 - 開源研究系列文章

今天講講C#中應用程序隨系統啟動的例子。 我們知道&#xff0c;應用程序隨系統啟動&#xff0c;都是直接在操作系統注冊表中寫入程序的啟動參數&#xff0c;這樣操作系統在啟動的時候就根據啟動參數來啟動應用程序&#xff0c;而我們要做的就是將程序啟動參數寫入注冊表即可。此…

C語言慣用法之typedef結構體類型

以前曾問C語言熟手&#xff0c;為什么C語言里面充滿了typedef struct someType TSomeType&#xff1f; 當時&#xff0c;可能他以為我的問題太簡單了&#xff0c;也沒所有說出關鍵出來。 對于現在我的認識而言&#xff0c;這種聲明方式&#xff0c;更多的是為了簡寫&#xff0…

數據結構與算法基礎

一、基本概念和術語 &#xff08;一&#xff09;數據元素、數據結構、抽象數據類型等概念 &#xff08;二&#xff09;算法設計的基本要求 &#xff08;三&#xff09;語句的頻度和估算時間復雜度 二、線性表 &#xff08;一&#xff09;線性表的定義和基本操作 &#xff08…

【3Ds Max】車削命令的簡單使用(以制作花瓶為例)

簡介 在3ds Max中&#xff0c;"車削"&#xff08;Lathe&#xff09;是一種建模命令&#xff0c;用于創建圍繞軸線旋轉的幾何形狀。通過車削命令&#xff0c;您可以將一個閉合的平面或曲線幾何形狀旋轉&#xff0c;從而生成一個立體對象。這種方法常用于創建圓柱體、…