Flattern unter der Haube

Hallo alle zusammen! Mein Name ist Mikhail Zotiev, ich arbeite als Flutter-Entwickler bei Surf. Ich, wie wahrscheinlich die Mehrheit der anderen Entwickler, die mit Flutter arbeiten, mag vor allem, wie einfach es ist, mit seiner Hilfe schöne und bequeme Anwendungen zu erstellen. Es dauert sehr wenig Zeit, um in die Flutter-Entwicklung einzusteigen. Ich habe kürzlich in der Spieleentwicklung gearbeitet und bin jetzt vollständig auf die plattformübergreifende mobile Entwicklung von Flutter umgestiegen.

Was ist die Einfachheit? Mit einem Dutzend grundlegender Widgets können Sie recht anständige Benutzeroberflächen erstellen. Und im Laufe der Zeit, wenn das verwendete Gepäck recht anständig ist, ist es unwahrscheinlich, dass eine Aufgabe Sie zum Stillstand bringt: sei es ein ungewöhnliches Design oder eine raffinierte Animation. Und das Interessanteste - höchstwahrscheinlich können Sie es verwenden, ohne über die Frage nachzudenken: "Wie funktioniert es überhaupt?"

Da Flutter Open Source hat, habe ich beschlossen, herauszufinden, was sich unter der Haube befindet (auf der Dart-Seite der Macht) und es mit Ihnen zu teilen.



Widget


Wir alle haben den Satz vom Framework-Entwicklungsteam mehr als einmal gehört: "Alles in Flutter ist Widgets . " Mal sehen, ob das wirklich so ist. Dazu wenden wir uns der Widget- Klasse (im Folgenden - Widget) zu und beginnen, uns allmählich mit dem Inhalt vertraut zu machen.

Das erste, was wir in der Dokumentation für die Klasse lesen werden:
Beschreibt die Konfiguration für ein [Element].

Es stellt sich heraus, dass das Widget selbst nur eine Beschreibung eines Elements ist (im Folgenden - das Element).
Widgets sind die zentrale Klassenhierarchie im Flutter-Framework. Ein Widget ist eine unveränderliche Beschreibung eines Teils einer Benutzeroberfläche. Widgets können in Elemente aufgeblasen werden, die den zugrunde liegenden Renderbaum verwalten.
Zusammenfassend ist der Ausdruck "Alles in Flutter ist ein Widget" das Mindestmaß an Verständnis dafür, wie alles angeordnet ist, um Flutter zu verwenden. Das Widget ist die Mittelklasse in der Flutter-Hierarchie. Gleichzeitig gibt es viele zusätzliche Mechanismen, die dem Framework helfen, seine Aufgabe zu bewältigen.

Also haben wir noch ein paar Fakten gelernt:

  • Widget - eine unveränderliche Beschreibung eines Teils der Benutzeroberfläche;
  • Das Widget ist einer erweiterten Ansicht zugeordnet, die als Element bezeichnet wird.
  • Ein Element steuert eine Entität des Renderbaums.

Sie müssen eine seltsame Sache bemerkt haben. Die Benutzeroberfläche und die Unveränderlichkeit passen sehr schlecht zusammen, ich würde sogar sagen, dass dies völlig inkompatible Konzepte sind. Wir werden jedoch darauf zurückkommen, wenn ein vollständigeres Bild des Geräts der Flutter-Welt erstellt wird. Im Moment werden wir uns jedoch weiterhin mit der Dokumentation des Widgets vertraut machen.
Widgets selbst haben keinen veränderlichen Status (alle Felder müssen endgültig sein).
Wenn Sie einem Widget einen veränderlichen Status zuordnen möchten, sollten Sie ein [StatefulWidget] verwenden, das ein [State] -Objekt (über [StatefulWidget.createState]) erstellt, wenn es in ein Element aufgeblasen und in den Baum integriert wird.
Dieser Absatz ergänzt der erste Absatz etwas: wenn wir eine veränderbare Konfiguration benötigen, verwenden wir die spezielle staatliche Einheit ( im folgenden als Zustand bezeichnet), die den aktuellen Zustand dieses Widgets beschreibt. Der Status ist jedoch nicht dem Widget zugeordnet, sondern seiner elementaren Darstellung.
Ein bestimmtes Widget kann null oder mehrmals in den Baum aufgenommen werden. Insbesondere kann ein bestimmtes Widget mehrmals in den Baum eingefügt werden. Jedes Mal, wenn ein Widget in den Baum eingefügt wird, wird es in ein [Element] aufgeblasen. Dies bedeutet, dass ein Widget, das mehrmals in den Baum integriert wird, mehrmals aufgeblasen wird.
Das gleiche Widget kann viele Male oder gar nicht in den Widget-Baum aufgenommen werden. Jedes Mal, wenn ein Widget in den Widget-Baum aufgenommen wird, wird ihm ein Element zugeordnet.

In diesem Stadium sind die Widgets also fast fertig. Fassen wir zusammen:

  • Widget - die zentrale Klasse der Hierarchie;
  • Widget ist eine Konfiguration;
  • Widget - eine unveränderliche Beschreibung eines Teils der Benutzeroberfläche;
  • Das Widget ist einem Element zugeordnet, das das Rendern auf irgendeine Weise steuert.
  • Der sich ändernde Status des Widgets kann von einer Entität beschrieben werden, ist jedoch nicht mit dem Widget verbunden, sondern mit dem Element, das dieses Widget darstellt.

Element


Nach dem, was wir gelernt haben, stellt sich die Frage: "Was sind diese Elemente, die alles regieren?" Machen Sie dasselbe - öffnen Sie die Dokumentation für die Element-Klasse.
Eine Instanziierung eines [Widgets] an einer bestimmten Stelle im Baum.
Ein Element ist eine Darstellung eines Widgets an einer bestimmten Stelle in einem Baum.
Widgets beschreiben, wie ein Teilbaum konfiguriert wird. Mit demselben Widget können jedoch mehrere Teilbäume gleichzeitig konfiguriert werden, da Widgets unveränderlich sind. Ein [Element] repräsentiert die Verwendung eines Widgets zum Konfigurieren eines bestimmten Speicherorts in der Baumstruktur. Im Laufe der Zeit kann sich das einem bestimmten Element zugeordnete Widget ändern, wenn das übergeordnete Widget beispielsweise neu erstellt und ein neues Widget für diesen Speicherort erstellt.
Das Widget beschreibt die Konfiguration eines Teils der Benutzeroberfläche. Wie wir jedoch bereits wissen, kann dasselbe Widget an verschiedenen Stellen des Baums verwendet werden. Jeder dieser Orte wird durch ein entsprechendes Element dargestellt. Im Laufe der Zeit kann sich jedoch das dem Element zugeordnete Widget ändern. Dies bedeutet, dass die Elemente zäher sind und weiterhin verwendet werden, wobei nur ihre Verbindungen aktualisiert werden.

Dies ist eine ziemlich rationale Entscheidung. Wie wir oben bereits definiert haben, sind Widgets eine unveränderliche Konfiguration, die lediglich einen bestimmten Teil der Benutzeroberfläche beschreibt, was bedeutet, dass sie sehr leicht sein müssen. Und die Elemente, in deren Bereich die Kontrolle viel schwerer ist, werden aber nicht unnötig neu erstellt.

Um zu verstehen, wie dies geschieht, betrachten Sie den Lebenszyklus eines Elements:

  • Widget.createElement , .
  • mount . .
  • .
  • , (, ), . runtimeType key, . , , .
  • , , , , ( deactivate).
  • , . , , (unmount), .
  • Wenn Sie Elemente erneut in den Baum aufnehmen, z. B. wenn das Element oder seine Vorfahren einen globalen Schlüssel haben, wird dieser aus der Liste der inaktiven Elemente entfernt, die Aktivierungsmethode wird aufgerufen und das diesem Element zugeordnete gerenderte Objekt wird erneut in den Renderbaum eingebettet. Dies bedeutet, dass das Element erneut auf dem Bildschirm angezeigt werden soll.

In der Klassendeklaration sehen wir, dass das Element die BuildContext-Schnittstelle implementiert. Ein BuildContext steuert die Position eines Widgets in einem Widget-Baum, wie aus der Dokumentation hervorgeht. Entspricht fast genau der Artikelbeschreibung. Diese Schnittstelle wird verwendet, um eine direkte Manipulation des Elements zu vermeiden und gleichzeitig Zugriff auf die erforderlichen Kontextmethoden zu gewähren. Beispiel: findRenderObject, mit dem Sie das Renderbaumobjekt finden können, das diesem Element entspricht.

Renderderbject


Es bleibt der letzte Link dieser Triade - RenderObject . Wie der Name schon sagt, ist dies ein Objekt des Visualisierungsbaums. Es verfügt über ein übergeordnetes Objekt sowie ein Datenfeld, in dem das übergeordnete Objekt bestimmte Informationen zu diesem Objekt selbst speichert, z. B. seine Position. Dieses Objekt ist für die Implementierung grundlegender Rendering- und Layoutprotokolle verantwortlich.

RenderObject beschränkt das Modell der Verwendung untergeordneter Objekte nicht: Möglicherweise gibt es keine, eine oder mehrere. Das Positionierungssystem ist auch nicht beschränkt auf: das kartesische System, Polarkoordinaten, all dies und vieles mehr steht zur Verfügung. Es gibt keine Einschränkungen für die Verwendung von Standortprotokollen: Anpassen der Breite oder Höhe, Begrenzen der Größe, Angeben der Größe und des Standorts des übergeordneten Objekts oder ggf. Verwenden der Daten des übergeordneten Objekts.

Flattern Weltbild


Versuchen wir, ein Gesamtbild davon zu erstellen, wie alles zusammenarbeitet.

Wir haben oben bereits erwähnt, dass das Widget eine unveränderliche Beschreibung ist, aber die Benutzeroberfläche ist überhaupt nicht statisch. Diese Diskrepanz wird durch Aufteilung in 3 Objektebenen und Aufteilung der Verantwortungszonen beseitigt.

  • , .
  • , .
  • , — , .

Bild

Schauen

Bild

wir uns anhand eines einfachen Beispiels an, wie diese Bäume aussehen: In diesem Fall haben wir ein StatelessWidget in ein Padding-Widget eingeschlossen, das Text enthält.

Stellen wir uns an die Stelle von Flutter - wir haben diesen Widget-Baum erhalten.

Flutter: "Hey, Padding, ich brauche dein Element"
Padding: "Natürlich, halte SingleChildRenderObjectElement"

Bild

Flutter: "Element, hier ist dein Platz, beruhige dich"
SingleChildRenderObjectElement: " Leute , alles ist in Ordnung, aber ich brauche RenderObject"
Flutter: "Padding, wie um dich überhaupt zu zeichnen? "
Padding: "Hold it, RenderPadding"
SingleChildRenderObjectElement: "Großartig, mach dich an die Arbeit"

Bild

Flattern:"Also wer ist der nächste?" StatelessWidget, jetzt können Sie das Element lassen »
StatelessWidget: «Hier StatelessElement»
Flutter: «StatelessElement, du untertan SingleChildRenderObjectElement sein wird, hier ist der Ort, einsteigen»
StatelessElement: «OK»

Bild

Flutter: «Der Rich - Text, elementik Present, bitte»
Der Rich - Text gibt MultiChildRenderObjectElement
Flutter: "MultiChildRenderObjectElement, los geht's"
MultiChildRenderObjectElement: "Ich brauche ein Rendering für die Arbeit"
Flutter: "RichText, wir brauchen ein
Renderobjekt " RichText: "Hier ist ein RenderParagraph"
Flutter:"RenderParagraph erhalten Sie Anweisungen RenderPadding, und Sie steuern MultiChildRenderObjectElement"
MultiChildRenderObjectElement: "Jetzt ist alles in Ordnung, ich bin bereit"

Bild

Sicher werden Sie eine berechtigte Frage stellen: "Wo ist das Rendering-Objekt für StatelessWidget, warum ist es nicht da, wir haben oben entschieden, dass die Elemente Konfigurationen binden mit Display? " Achten wir auf die grundlegende Implementierung der Mount-Methode, die in diesem Abschnitt der Lebenszyklusbeschreibung erläutert wurde.

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;
    }());
}

Wir werden darin nicht die Erstellung eines Rendering-Objekts sehen. Das Element implementiert jedoch den BuildContext, der über eine Suchmethode für das Visualisierungsobjekt findRenderObject verfügt, die uns zum folgenden Getter führt:

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;
}

Im Basisfall erstellt ein Element möglicherweise kein Rendering-Objekt. Dazu sind nur RenderObjectElement und seine Nachkommen erforderlich. In diesem Fall muss ein Element auf einer Verschachtelungsebene ein untergeordnetes Element mit einem Rendering-Objekt haben.

Es scheint, warum all diese Schwierigkeiten. Bis zu 3 Bäume, verschiedene Verantwortungsbereiche usw. Die Antwort ist ganz einfach - hier wird die Leistung von Flutter aufgebaut. Widgets sind unveränderliche Konfigurationen, daher werden sie häufig neu erstellt, sind aber gleichzeitig recht leicht, was die Leistung nicht beeinträchtigt. Aber Flutter versucht, schwere Elemente so weit wie möglich wiederzuverwenden.

Betrachten Sie ein Beispiel.

Text in der Mitte des Bildschirms. Der Code in diesem Fall sieht ungefähr so ​​aus:

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

In diesem Fall sieht der Widget-Baum folgendermaßen aus:

Bild

Nachdem Flutter alle drei Bäume erstellt hat, wird das folgende Bild

Bild

angezeigt : Was passiert, wenn wir den anzuzeigenden Text ändern?

Bild

Wir haben jetzt einen neuen Widget-Baum. Oben haben wir über die maximal mögliche Wiederverwendung von Elementen gesprochen. Schauen Sie sich die Widget-Klassenmethode unter dem sprechenden Namen canUpdate an .

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

Wir überprüfen den Typ des vorherigen und des neuen Widgets sowie deren Schlüssel. Wenn sie gleich sind, muss der Artikel nicht geändert werden.

Vor dem Upgrade ist das erste Element also Center, nach dem Upgrade auch Center. Beide haben keine Schlüssel, ein völliger Zufall. Wir können den Elementlink auf ein neues Widget aktualisieren.

Bild

Zusätzlich zu Typ und Schlüssel ist das Widget eine Beschreibung und Konfiguration, und die Werte der Parameter, die für die Anzeige erforderlich sind, können sich ändern. Aus diesem Grund sollte das Element nach dem Aktualisieren des Links zum Widget Aktualisierungen des Rendering-Objekts initiieren. Im Fall von Center hat sich nichts geändert, und wir vergleichen weiter.

Der Typ und der Schlüssel sagen uns erneut, dass es keinen Sinn macht, das Element neu zu erstellen. Der Text ist ein Nachkomme von StatelessWidget und hat kein direktes Anzeigeobjekt.

Bild

Gehen Sie zu RichText. Das Widget hat auch seinen Typ nicht geändert, es gibt keine Unstimmigkeiten in den Schlüsseln. Das Element aktualisiert seine Zuordnung zum neuen Widget.

Bild

Die Verbindung wird aktualisiert, es bleiben nur die Eigenschaften zu aktualisieren. Infolgedessen zeigt RenderParagraph den neuen Textwert an.

Bild

Und sobald die Zeit für den nächsten Zeichenrahmen gekommen ist, werden wir das erwartete Ergebnis sehen.

Dank dieser Art von Arbeit erreicht Flutter eine so hohe Leistung.

Das obige Beispiel beschreibt den Fall, in dem sich die Widget-Struktur selbst nicht geändert hat. Aber was passiert, wenn sich die Struktur ändert? Flutter wird natürlich weiterhin versuchen, die Verwendung vorhandener Objekte zu maximieren, wie wir aus der Beschreibung des Lebenszyklus verstanden haben, aber für alle neuen Widgets werden neue Elemente erstellt und alte und unnötigere werden am Ende des Frames gelöscht.

Schauen wir uns einige Beispiele an. Um dies zu gewährleisten, verwenden wir das Android Studio-Tool - 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,
        ),
    ],
);

In diesem Fall ändert sich durch Klicken auf die Schaltfläche eines der Widgets. Mal sehen, was der Inspektor uns zeigt.

Bild

Bild

Wie wir sehen können, hat Flutter den Render nur für Padding neu erstellt, der Rest wurde nur wiederverwendet.

Betrachten Sie eine weitere Option, bei der sich die Struktur globaler ändert - wir ändern die Verschachtelungsebenen.

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

Bild

Bild

Trotz der Tatsache, dass sich der Baum visuell überhaupt nicht geändert hat, wurden die Elemente und Objekte des Rendering-Baums neu erstellt. Dies geschah, weil Flutter nach Ebenen vergleicht (in diesem Fall spielt es keine Rolle, dass sich der größte Teil des Baums nicht geändert hat). Das Sieben dieses Teils erfolgte zum Zeitpunkt des Vergleichs von Container und Zeile. Man kann jedoch aus dieser Situation herauskommen. Dies wird uns GlobalKey helfen. Fügen Sie einen solchen Schlüssel für Row hinzu.

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

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

Bild

Bild

Sobald wir Flutter sagten, dass das Teil wiederverwendet werden könne, nutzte er die Gelegenheit.

Fazit


Wir haben uns ein wenig mit Flutter-Magie vertraut gemacht und jetzt wissen wir, dass es sich nicht nur um Widgets handelt.

Flutter ist ein gut durchdachter, gut koordinierter Mechanismus mit einer eigenen Hierarchie und Verantwortungsbereichen, mit dem Sie nicht nur schöne, sondern auch produktive Anwendungen erstellen können. Natürlich haben wir nur einen kleinen, wenn auch ziemlich wichtigen Teil seines Geräts untersucht, daher werden wir in zukünftigen Artikeln weiterhin verschiedene Aspekte der internen Funktionsweise des Frameworks analysieren.

Ich hoffe, die Informationen in diesem Artikel sind hilfreich, um zu verstehen, wie Flutter intern funktioniert, und helfen Ihnen, während der Entwicklung elegante und produktive Lösungen zu finden.

Vielen Dank für Ihre Aufmerksamkeit!

Ressourcen


Flutter
"Wie Flutter Widgets rendert" von Andrew Fitz Gibbon, Matt Sullivan

All Articles