引擎盖下飘动

大家好!我叫Mikhail Zotiev,我是Surf的Flutter开发人员。我和大多数使用Flutter的开发人员一样,大多数人都喜欢在它的帮助下创建美观,便捷的应用程序如此容易。只需很少的时间即可进入Flutter开发。我最近从事游戏开发,现在我已经完全转向Flutter上的跨平台移动开发。

简单是什么?借助许多基本的小部件,您可以构建相当不错的用户界面。随着时间的流逝,当所使用的行李相当不错时,某种任务不太可能使您处于停顿状态:是不寻常的设计还是精美的动画。最有趣的是-您甚至可能不用考虑以下问题就可以使用它:“它怎么工作?”

由于Flutter具有开源功能,因此我决定弄清楚幕后情况(在部队的Dart方面),并与您分享。



小部件


我们所有人都多次听到过框架开发团队的说法:“ Flutter中的一切都是小部件让我们看看是否真的是这样。为此,我们转到Widget(以下简称Widget),开始逐渐熟悉其内容。

我们将在该类文档中阅读的第一件事:
描述[元素]的配置。

事实证明,小部件本身只是对某些Element(以下简称-element)的描述。
小部件是Flutter框架中的中心类层次结构。小部件是用户界面一部分的不变描述。可以将小部件放大为元素,以管理基础渲染树。
总而言之,短语“ Flutter中的所有内容都是小部件”是了解如何使用Flutter安排所有内容的最低级别。小部件是Flutter层次结构中的中心类。同时,围绕它的许多其他机制可以帮助框架完成其任务。

因此,我们了解了更多事实:

  • 小部件-用户界面一部分的不变描述;
  • 小部件与称为元素的某些高级视图相关联;
  • 元素控制渲染树的某些实体。

您一定注意到了一件奇怪的事。用户界面和不变性很难很好地融合在一起,我什至可以说它们是完全不兼容的概念。但是,当出现有关Flutter世界的设备的更完整图片时,我们将返回到这一点,但是现在,我们将继续熟悉该小部件的文档。
窗口小部件本身没有可变状态(它们的所有字段都必须为final)。
如果希望将可变状态与窗口小部件相关联,请考虑使用[StatefulWidget],该状态会在膨胀为一个元素并合并到树中时创建一个[State]对象(通过[StatefulWidget.createState])。
本段对第一段进行了补充:如果我们需要可变的配置,则使用特殊的State实体(以下称为state),它描述了此小部件的当前状态。但是,状态不与窗口小部件关联,而是与其基本表示形式关联。
给定的小部件可以零次或多次包含在树中。特别地,给定的小部件可以多次放置在树中。每次将小部件放置在树中时,它都会膨胀为[Element],这意味着多次合并到树中的小部件将被多次膨胀。
相同的窗口小部件可以多次包含在窗口小部件树中,或者根本不包含。但是,每当窗口小部件树中包含窗口小部件时,就会将一个元素映射到该窗口小部件。

因此,在此阶段,小部件几乎完成了,让我们总结一下:

  • 小部件-层次结构的中心类;
  • 小部件是一些配置;
  • 小部件-用户界面一部分的不变描述;
  • 小部件与以某种方式控制渲染的元素相关联;
  • 小部件的更改状态可以由某些实体描述,但它与小部件无关,但与代表此小部件的元素有关。

元件


从我们学到的问题开始,问题开始了:“这些因素支配着一切?” 进行相同操作-打开Element类的文档。
树中特定位置的[Widget]的实例化。
元素是小部件在树中特定位置的某种表示形式。
窗口小部件描述了如何配置子树,但是由于窗口小部件是不可变的,因此可以使用同一窗口小部件同时配置多个子树。[Element]表示使用小部件来配置树中的特定位置。随着时间的流逝,与给定元素关联的窗口小部件可以更改,例如,如果父窗口小部件为该位置重建并创建了一个新窗口小部件。
该小部件描述了用户界面的某些部分的配置,但是我们已经知道,同一小部件​​可以在树的不同位置使用。每个这样的地方将由相应的元素表示。但是随着时间的流逝,与该项目关联的窗口小部件可能会更改。这意味着元素更加顽强并且继续使用,仅更新它们的连接。

这是一个相当合理的决定。正如我们在上面已经定义的那样,小部件是一个不变的配置,仅描述了接口的特定部分,这意味着它们必须非常轻巧。而且控制区域中的元素要沉重得多,但不必不必要地重新创建它们。

要了解如何完成此操作,请考虑元素的生命周期:

  • Widget.createElement , .
  • mount . .
  • .
  • , (, ), . runtimeType key, . , , .
  • , , , , ( deactivate).
  • , . , , (unmount), .
  • 例如,当您在树中重新包含元素时,如果该元素或其祖先具有全局键,则将从非活动元素列表中将其删除,将调用activate方法,并且与此元素关联的渲染对象将再次嵌入渲染树中。这意味着该项目应再次出现在屏幕上。

在类声明中,我们看到该元素实现了BuildContext接口。BuildContext是控制小部件在小部件树中的位置的工具,如其文档所述。几乎完全符合商品说明。该接口用于避免对元素的直接操作,但同时允许访问必要的上下文方法。例如,findRenderObject,它允许您查找与此元素对应的渲染树对象。

渲染对象


仍然需要处理此三元组的最后一个链接-RenderObject。顾名思义,这是可视化树的对象。它具有父对象,以及父对象用来存储有关该对象本身的特定信息(例如其位置)的数据字段。该对象负责基本渲染和布局协议的实现。

RenderObject并不限制使用子对象的模型:可能不存在,一个或多个。同样,定位系统不限于:笛卡尔系统,极坐标,所有这些以及更多可供使用。使用位置协议没有任何限制:调整宽度或高度,限制大小,设置父对象的大小和位置,或者在必要时使用父对象的数据。

颤抖的世界图片


让我们尝试对所有事物如何协同工作进行全面了解。

上面我们已经提到,小部件是一个不变的描述,但是用户界面并不是完全静态的。通过将对象划分为3个级别和划分责任区,可以消除这种差异。

  • , .
  • , .
  • , — , .

图片

让我们用一个简单的示例看一下这些树的外观:

图片

在这种情况下,我们将一些StatelessWidget包装在Padding小部件中并在其中包含文本。

让我们代替Flutter-我们得到了这个小部件树。

Flutter:“嘿,填充,我需要您的元素”
Padding:“当然,握住SingleChildRenderObjectElement”

图片

Flutter:“元素,这是您的位置,安顿下来”
SingleChildRenderObjectElement:“伙计,一切正常,但我需要RenderObject”
Flutter:“ Padding,喜欢完全吸引你?”
填充:“保留它,RenderPadding”
SingleChildRenderObjectElement:“很棒,开始工作”

图片

Flutter:“那么下一个是谁?” StatelessWidget,现在让元素»
StatelessWidget:«这里StatelessElement»
Flutter:«StatelessElement,您将服从SingleChildRenderObjectElement,在这里着手进行»
StatelessElement:«OK»

图片

Flutter:«RichText,elementik存在,请»
RichText给MultiChildRenderObjectElement
Flutter:“ MultiChildRenderObjectElement,开始,开始”
MultiChildRenderObjectElement:“我需要工作渲染”
Flutter:“ RichText,我们需要渲染对象”
RichText:“这里是RenderParagraph”
Flutter:“ RenderParagraph您将收到RenderPadding指令,而MultiChildRenderObjectElement将控制您”
MultiChildRenderObjectElement:“现在一切正常,我已经准备就绪”,

图片

您肯定会问一个逻辑问题:“ StatelessWidget的渲染对象在哪里,为什么不在那里,我们在上面决定了元素绑定配置显示?” 让我们注意mount方法的基本实现,该方法在生命周期描述的这一部分中进行了讨论。

void mount(Element parent, dynamic newSlot) {
    assert(_debugLifecycleState == _ElementLifecycle.initial);
    assert(widget != null);
    assert(_parent == null);
    assert(parent == null || parent._debugLifecycleState == _ElementLifecycle.active);
    assert(slot == null);
    assert(depth == null);
    assert(!_active);
    _parent = parent;
    _slot = newSlot;
    _depth = _parent != null ? _parent.depth + 1 : 1;
    _active = true;
    if (parent != null)
        _owner = parent.owner;
    if (widget.key is GlobalKey) {
        final GlobalKey key = widget.key;
        key._register(this);
    }
    _updateInheritance();
    assert(() {
        _debugLifecycleState = _ElementLifecycle.active;
        return true;
    }());
}

我们不会在其中看到渲染对象的创建。但是该元素实现了BuildContext,该上下文具有findRenderObject可视化对象搜索方法,该方法将导致我们进入以下获取方法:

RenderObject get renderObject {
    RenderObject result;
    void visit(Element element) {
        assert(result == null); 
        if (element is RenderObjectElement)
            result = element.renderObject;
        else
            element.visitChildren(visit);
    }
    visit(this);
    return result;
}

在基本情况下,元素可能不会创建渲染对象;仅需要RenderObjectElement及其后代才能执行此操作,但是,在这种情况下,处于某个嵌套级别的元素必须具有包含渲染对象的子元素。

看来为什么所有这些困难。多达3棵树,不同职责范围等答案很简单-这就是Flutter性能的基础。小部件是不可变的配置,因此,通常会重新创建它们,但是同时它们非常轻巧,这不会影响性能。但是Flutter正在尝试尽可能多地重用重元素。

考虑一个例子。

屏幕中间的文本。这种情况下的代码如下所示:

body: Center(
    child: Text(“Hello world!”)
),

在这种情况下,小部件树将如下所示:

图片

Flutter构建了所有3棵树之后,我们得到以下图片:

图片

如果更改将要显示的文本会发生什么?

图片

现在,我们有了一个新的小部件树。上面我们讨论了元素的最大可能重用。看一看Widget类的方法,它的话语为canUpdate

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key;
}

我们检查以前的小部件和新的小部件的类型,以及它们的键。如果它们相同,则无需更改项目。

因此,升级之前,第一个元素是“中心”,升级之后,也是“中心”。两者都没有键,完全是巧合。我们可以将项目链接更新为新的小部件。

图片

但是除了类型和键之外,小部件是描述和配置,显示所需的参数值可能会更改。这就是为什么元素在更新到小部件的链接之后,应该启动对渲染对象的更新。就Center而言,什么都没有改变,我们将继续进行比较。

再次,类型和键告诉我们重新创建元素没有意义。文本是StatelessWidget的后代;它没有直接的显示对象。

图片

转到RichText。该小部件也没有更改其类型;键中没有任何差异。该项目使用新的小部件更新其关联。

图片

连接已更新,仅保留以更新属性。结果,RenderParagraph将显示新的文本值。

图片

下一个图纸框架的时间到了,我们将看到我们期望的结果。

由于这种工作,Flutter的高性能得以实现。

上面的示例描述了微件结构本身未更改的情况。但是,如果结构发生变化会怎样?正如我们从生命周期描述中了解到的那样,Flutter当然将继续尝试最大限度地利用现有对象,但是将为所有新的小部件创建新元素,并在帧末尾删除旧的和更多不必要的小部件。

让我们看几个例子。为了确保以上所述,我们使用Android Studio工具-Flutter Inspector。

@override
Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: _isFirst ? first() : second(),
        ),
        floatingActionButton: FloatingActionButton(
            child: Text("Switch"),
            onPressed: () {
                setState(() {
                    _isFirst = !_isFirst;
                });
            },
        ),
    );
}

Widget first() => Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
        Text(
            "test",
            style: TextStyle(fontSize: 25),
        ),
        SizedBox(
            width: 5,
        ),
        Icon(
            Icons.error,
        ),
    ],
);

Widget second() => Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
        Text(
            "one more test",
            style: TextStyle(fontSize: 25),
        ),
        Padding(
            padding: EdgeInsets.only(left: 5),
        ),
        Icon(
            Icons.error,
        ),
    ],
);

在这种情况下,通过单击按钮,将更改一个小部件。让我们看看检查员向我们展示了什么。

图片

图片

如我们所见,Flutter仅为Padding重新创建了渲染,其余的仅被重用了。

再考虑1个选项,其中结构以更全局的方式更改-我们更改嵌套级别。

Widget second() => Container(child: first(),);

图片

图片

尽管该树根本没有视觉上的变化,但仍重新创建了渲染树的元素和对象。发生这种情况是因为Flutter会按级别进行比较(在这种情况下,大多数树没有变化都没关系),因此在比较Container和Row时进行了这部分的筛选。但是,可以摆脱这种情况。这将对我们帮助GlobalKey。为Row添加这样的键。

var _key = GlobalKey(debugLabel: "testLabel");

Widget first() => Row(
    key: _key,
    …
);

图片

图片

一旦我们告诉Flutter该零件可以重复使用,他便抓住了机会。

结论


我们更加熟悉Flutter魔术,现在我们知道它不仅在小部件中。

Flutter是一种协调良好的机制,具有自己的层次结构和职责范围,使用它不仅可以创建美观的应用程序,还可以创建高效的应用程序。当然,我们只检查了设备中很小但很小的一部分,因此我们将在以后的文章中继续分析框架内部工作的各个方面。

我希望本文中的信息有助于理解Flutter的内部工作方式,并帮助您在开发过程中找到优雅而有效的解决方案。

感谢您的关注!

资源资源


Flutter Flutter
如何渲染小部件”,作者:Andrew Fitz Gibbon,Matt Sullivan

All Articles