ウィジェット、要素、BuildContext および RenderObject
ウィジェット
Widget
キークラスとそのサブクラスの継承関係を次の図に示します。
その中には、すべてのノードの基本クラスWidget
があります。サブクラスは主に 3 つのカテゴリに分類されます。Widget Tree
Widget
-
最初のカテゴリはのサブカテゴリで、具体的には(単一子ノード コンテナ)、(リーフ ノード)、(複数子ノード コンテナ)
RenderObjectWidget
に分かれており、共通の特徴は、すべて のサブカテゴリに対応していることです。 、などのロジックを実行できます。SingleChildRenderObjectWidget
LeafRenderObjectWidget
MultiChildRenderObjectWidget
RenderObject
Layout
Paint
-
2 番目のカテゴリは
StatelessWidget
と でStatefulWidget
、開発者によって最も一般的に使用されますWidget
。これらは自分自身を描画する機能はありません (つまり、対応していません) が、整理して構成Render Object
することができます。RenderObjectWidget
Widget
-
カテゴリ 3 は、具体的には と
ProxyWidget
に細分され、子ノードに追加のデータを提供することを特徴としています。ParentDataWidget
InheritedWidget
エレメント
Element
キー クラスとそのサブクラスの継承関係を次の図に示します。
図 5-2 から明らかな継承関係から、インターフェースをElement
実装していますが、図 5-2 は図 5-1 に対応しており、それぞれ対応するものがあります。2 つの直接サブクラスおよびがあり、そのうち2 つのサブクラスおよび はそれぞれおよびに対応します。BuildContext
Element
Widget
Element
ComponentElement
RenderObjectElement
ComponentElement
StatelessElement
StatefulElement
StatelessWidget
StatefulWidget
最終的な UI ツリーは実際には個々のノードで構成されていることがわかりますElement
。コンポーネントの最終的なレイアウトと描画はでRenderObject
行われ、作成から描画までの大まかな流れは、Widget
世代に応じElement
て対応するものを作成して属性RenderObject
と関連付け、最後に でレイアウト配置と描画が完了します。Element.renderObject
RenderObject
Element
これはWidget
UI ツリー内の特定の場所にインスタンス化されたオブジェクトであり、そのほとんどはElement
一意ですrenderObject
が、たとえば から継承されたクラスElement
など、複数の子ノードを持つものもあります。最終的には、それらすべてが 1 つのツリーを構成し、これを「レンダー ツリー」または「レンダー ツリー」と呼びます。RenderObjectElement
MultiChildRenderObjectElement
Element
RenderObject
要約すると、Flutter の UI システムには、ウィジェット ツリー、要素ツリー、レンダリング ツリーの3 つのツリーが含まれていると考えることができます。それらの依存関係は次のとおりです。図に示すように、要素ツリーはウィジェット ツリーに基づいて生成され、レンダリング ツリーは要素ツリーに依存します。
ここで、次のライフサイクルElement
に注目してみましょう。Element
-
フレームワークは、インスタンスを
Widget.createElement
作成するために呼び出します。Element
element
-
フレームワーク呼び出しでは
element.mount(parentElement,newSlot)
、mount
メソッド内で、まずelement
対応するWidget
メソッドを呼び出して、それに関連付けられたオブジェクトcreateRenderObject
を作成します。次に、そのメソッドを呼び出して、レンダリング ツリー内のスロットで指定された位置にオブジェクトを追加します (このステップは必要ありません。通常は、ツリー構造が変更されたときに再作成される) に追加) 。レンダリングツリーに挿入された後は「 」状態となり、「 」状態になった後は画面に表示可能(非表示可能)になります。element
RenderObject
element.attachRenderObject
element.renderObject
Element
element
active
active
-
親の構成データが
Widget
変更され、State.build
返されたWidget
構造が以前のものと異なる場合、対応するElement
ツリーを再構築する必要があります。再利用するためにElement
、Element
再構築する前に、古いツリー上の同じ位置を再利用しようとしますelement
。ノードはelement
、更新する前にWidget
対応するcanUpdate
メソッドを呼び出します。それが返された場合true
、古いものは再利用されElement
、古いものは次の方法で更新Element
されます。Widget
新しい構成データ。それ以外の場合は、新しい構成データが作成されますElement
。Widget.canUpdate
主なことは、と の合計が同時に等しいnewWidget
かどうかを判断し、同時に等しい場合は返し、そうでない場合は返します。この原則によれば、強制的に更新する必要がある場合、別の更新を指定することで多重化を回避できます。oldWidget
runtimeType
key
true
false
Widget
Key
-
祖先が
Element
削除を決定するとelement
(たとえば、Widget
ツリー構造が変更され、element
対応するものWidget
が削除された場合)、祖先はそれを削除するメソッドElement
を呼び出し、deactivateChild
削除後にelement.renderObject
レンダリング ツリーからも削除されます。element.deactivate
メソッドが呼び出され、element
ステータスが「inactive
」に変わります。 -
再度「
inactive
」状態はelement
画面に表示されなくなります。アニメーション実行中に特定のオブジェクトの作成と削除が繰り返されるのを避けるためelement
、現在のアニメーションの最後のフレームが終了するまで「inactive
」状態が保持されます。element
アニメーション実行終了後に「active
」状態に戻っていない場合は、 , フレームワーク 完全に削除するにはそのメソッドが呼び出されますが、unmount
この時点では二度とツリーに挿入されないelement
状態になります。defunct
-
ツリーの別の場所 (グローバル再利用要素の場合) の祖先など、ツリーの別の場所
element
に再挿入する場合、フレームワークはまず既存の場所からツリーを削除し、次にそのメソッドを呼び出して、レンダリングを再度開始します。木。Element
element
element
GlobalKey
element
activate
renderObject
attach
要約:
- Element オブジェクトは作成時に状態を初期化し
initial
、mount
メソッドを介して Element Tree に追加された後の状態になりactive
、ノードに対応する Widget が失敗した場合はメソッドを介して状態にdeactivate
入ります。inactive
現在のフレームのビルド プロセス中に他のElement
ノードがkey
そのノードを再利用した場合、このactivate
メソッドはノードを再びその状態にするために使用されますactive
。現在のフレームの終了後もノードがまだ要素ツリーにない場合は、メソッドが使用されます。unmount
メソッドを通じてアンインストールされ、defunct
後続のロジックが破棄されるのを待つ状態になります。
Element
ライフサイクルを読んだ後、開発者が要素ツリーを直接操作するのではないかと疑問に思う人もいるかもしれません。
実際、開発者にとっては、ほとんどの場合、ツリーに注目するだけで済み、Widget
Flutter フレームワークはウィジェット ツリー上の操作をElement
ツリーにマッピングしているため、複雑さが大幅に軽減され、開発効率が向上します。
ただし、Element
Flutter UI フレームワーク全体を理解するには、理解することが重要です。Flutter はElement
このリンクを通じて接続されWidget
、RenderObject
接続されます。要素レイヤーを理解することは、開発者が Flutter UI フレームワークを明確に理解するのに役立つだけでなく、開発者自身の抽象化機能も向上させることができます。そしてデザイン力。さらに、場合によっては、テーマ データの取得など、一部の操作を完了するために Element オブジェクトを直接使用する必要があります。
ビルドコンテキスト
StatelessWidget
と のメソッドStatefulWidget
がオブジェクトbuild
を渡すことはすでにわかっていますBuildContext
。
Widget build(BuildContext context) {
}
context
また、多くの場合、次のようなことを行うためにこれを使用する必要があることもわかっています。
Theme.of(context) // 获取主题
Navigator.push(context, route) // 入栈新路由
Localizations.of(context, type) // 获取Local
context.size // 获取上下文大小
context.findRenderObject() // 查找当前或最近的一个祖先RenderObject
それは何BuildContext
ですか? その定義を確認すると、それが抽象インターフェイス クラスであることがわかります。
abstract class BuildContext {
...
}
このオブジェクトcontext
に対応する実装クラスは誰ですか? つるをたどったところ、build
呼び出しが対応するStatelessWidget
メソッドStatefulWidget
内で発生していることがわかりました。たとえば、次のとおりです。StatelessElement
StatefulElement
build
StatelessElement
class StatelessElement extends ComponentElement {
...
Widget build() => widget.build(this);
...
}
以下にもありますStatefulElement
:
class StatefulElement extends ComponentElement {
...
Widget build() => state.build(this);
...
}
build
渡されたパラメータは明らかに であることがわかりますthis
。これは、またはそれ自体BuildContext
です。しかし、それ自体はインターフェイスを実装していませんでした。引き続きコードをトレースすると、コードがクラスから間接的に継承していることがわかりました。次に、クラス定義をチェックして、クラスが実際にインターフェイスを実装していることを確認します。StatelessElement
StatefulElement
StatelessElement
StatefulElement
BuildContext
Element
Element
Element
BuildContext
abstract class ComponentElement extends Element {
...}
abstract class Element extends DiagnosticableTree implements BuildContext {
...}
これまでのところ真実は明らかであり、BuildContext
にwidget
対応しているため、とのメソッドでオブジェクトに直接アクセスElement
できます。対象データを取得するコード内には、というメソッドがあります。context
StatelessWidget
StatefulWidget
build
Element
Theme.of(context)
Element
dependOnInheritedWidgetOfExactType()
概要:BuildContext
それはElement
神であり、BuildContext
のメソッド呼び出しは操作でありElement
、Widget
それはコートであり、Element
コートの下は裸体です。
BuildContext の別の意味
もう 1 つの意味BuildContext
は、それがツリー内の位置への参照でありWidget
Widget
Widget
Widget
、それ自体に関する情報ではなく、ツリー内の位置に関する情報がWidget
含まれていることです。
トピックの場合、それぞれにWidget
独自の があるためBuildContext
、ツリー内に複数のトピックが分散している場合、1 つのトピックを取得すると、別のトピックとは異なる結果がWidget
返される可能性があることを意味します。Widget
カウンタ アプリケーションのサンプル プログラムのトピックの特定のケース、または他のメソッドでは、ツリー内のそのタイプの最も近い親ノードをof
取得します。
高度
これが内部接続とElement
Flutter UI フレームワーク間のリンクであることがわかります。ほとんどの場合、開発者はレイヤーに注意を払うだけで済みますが、レイヤーが詳細を完全にシールドできない場合があるため、フレームワークはオブジェクトを次のオブジェクトに渡します。このようにして、開発者は必要に応じてオブジェクトを直接操作できます。widget
RenderObject
widget
widget
Element
StatelessWidget
StatefulWidget
build
Element
Element
そこで、次の 2 つの質問があります。
1. ウィジェットレイヤーがない場合、エレメントレイヤーだけで利用可能なUIフレームワークを構築できますか? もしそうなら、それはどのように見えるべきですか?
2. Flutter UI フレームワークは応答しない可能性がありますか?
質問 1 については、答えはもちろん「はい」です。widget
ツリーは単なるElement
ツリーのマッピングであり、UI ツリー、Widget
つまりコートを説明する構成情報のみを提供すると前述したためです。もちろん、人は恥をかいて生きることができます服を着なくても服を着たほうがまともな生活ができるし、服Widget
に頼らなくてもElement
UIフレームワークを完全に構築できる。
以下に例を示します。
純粋に関数をElement
シミュレートしますStatefulWidget
。ボタンのあるページがあるとします。ボタンのテキストは 9 桁の数字です。ボタンを 1 回クリックすると、9 つの数字がランダムに配置されます。コードは次のとおりです:
class HomeView extends ComponentElement{
HomeView(Widget widget) : super(widget);
String text = "123456789";
Widget build() {
Color primary = Theme.of(this).primaryColor; //1
return GestureDetector(
child: Center(
child: TextButton(
child: Text(text, style: TextStyle(color: primary),),
onPressed: () {
var t = text.split("")..shuffle();
text = t.join();
markNeedsBuild(); //点击后将该Element标记为dirty,Element将会rebuild
},
),
),
);
}
}
-
上記のメソッドは、およびの
build
メソッドとは異なり、パラメータを受け取りません。コード内で使用する必要がある箇所は、 に直接置き換えることができます。たとえば、コード コメント 1 のパラメータは、現在のオブジェクト自体がインスタンスであるため、直接渡すことができます。StatelessWidget
StatefulWidget
build(BuildContext)
BuildContext
this
Theme.of(this)
this
Element
-
text
変更が発生すると、markNeedsBuild()
メソッドを呼び出して現在のものElement
をマークしますdirty
。マークされたものは次のフレームで再構築されますdirty
。Element
実はState.setState()
内部的に呼ばれているメソッドでもありますmarkNeedsBuild()
。 -
上記のコードのメソッドは
build
依然として 1 を返します。これは、Flutter フレームワークにこの層がwidget
すでに存在し、コンポーネント ライブラリがすでにの形式で提供されているためです。Flutter フレームワークのすべてのコンポーネントが例の形式で提供されている場合の場合は、pure を使用してUI を構築できます。メソッドの戻り値の型は です。widget
widget
HomeView
Element
Element
HomeView
build
Element
widget
上記のコードを既存の Flutter フレームワークで実行する必要がある場合は、既存のフレームワークに統合される「アダプター」を提供する必要がありますHomeView
。以下はCustomHome
「アダプター」に相当します。
class CustomHome extends Widget {
Element createElement() {
return HomeView(this);
}
}
これでツリーCustomHome
に追加できるようになり、新しいルーティング ページに作成します。最終的な効果は次の図のようになります。widget
ボタンをクリックすると、ボタンのテキストがランダムに並べ替えられます。
質問 2 の答えは、もちろん「はい」です。Flutter エンジンが提供する API は独自で独立しています。これは、オペレーティング システムが提供する API に似ています。上位 UI フレームワークのデザインは完全にデザイナーに依存します。UI は、フレームワークは Android スタイルまたは iOS スタイルとして設計できますが、Google はもうそのようなことはしません。つまり、理論上は可能ですが、その必要はありません。これは、応答性という考え方自体が素晴らしいからです。この質問が提起される理由は、それを行うか行わないかは別のことであるからですそれは私たちの知識の理解度を反映している可能性があります。
レンダーオブジェクト
Element
それぞれが1 つに対応しRenderObject
、 を介して取得できると言いましたElement.renderObject
。また、RenderObject
主な役割はレイアウトと描画であり、これらすべてがRenderObject
レンダリング ツリー Render Tree を形成するとも言いました。以下ではRenderObject
役割に焦点を当てて説明します。
RenderObject
レンダリング ツリー内のオブジェクトであり、主な機能は、レイアウト、描画、レイヤー構成、オンスクリーンなど、レンダリング パイプライン(プロセスはによって実現されます)以外のイベント レスポンスbuild
と実行プロセスを実装することです。build
element
RenderObject
キー クラスとそのサブクラスを図 5-3 に示します。その各サブクラスはノードRenderObjectWidget
のタイプに対応しますWidget
。
RenderView
特別な、レンダー ツリー全体のルート ノードRenderObject
です。- もう 1 つの特別な点は、それが抽象クラスであること
RenderObject
です。はそのインターフェイスを実装し、間接的に を継承します。RenderAbstractViewport
RenderViewport
RenderBox
RenderBox
RenderSliver
Flutter で最も一般的なもので、RenderObject
行RenderBox
、列などの一般的なレイアウトを担当し、RenderSliver
list 内のそれぞれのレイアウトを担当しますItem
。
RenderObject
これには、レンダリング ツリー内の独自の親ノードを指す1parent
つと 1 つのparentData
属性がありますが、予約された変数です。親コンポーネントのレイアウト プロセス中に、そのすべての子コンポーネントのレイアウト情報 (位置情報など) を決定します。は、親コンポーネントに対する相対的なオフセットです)。これらのレイアウト情報は、後続の描画フェーズ (コンポーネントの描画位置を決定するため) で使用する必要があるため、レイアウト フェーズで保存する必要があります。属性の主な機能は、 layoutなどのレイアウト情報を保存することです。子要素のオフセット データは子要素に格納されます(詳細は実装を参照)。parent
parentData
parentData
Stack
RenderStack
parentData
Positioned
質問: があるのにRenderObject
、なぜ Flutter フレームワークはRenderBox
とRenderSliver
2 つのサブクラスを提供するのですか?
-
これは、
RenderObject
クラス自体が基本的なレイアウトおよび描画プロトコルのセットを実装しますが、子ノード モデル (たとえば、ノードが持つことができる子ノードの数など) や座標系 (たとえば、 、子ノードの位置決めはフルート カール座標または極座標で行われます) および特定のレイアウト プロトコル (幅と高さによるか、制約とサイズによるか)、または親ノードが子ノードのサイズと位置を設定する前または後に設定します。子ノードのレイアウトなど)。 -
この目的のために、Flutter フレームワークは
RenderBox
とRenderSliver
クラスを提供します。これらはすべて から継承されRenderObject
、レイアウト座標系はデカルト座標系を採用し、画面が(top, left)
原点となります。これら 2 つのクラスに基づいて、Flutter はRenderBox
に基づくボックス モデルレイアウトと、にSliver
基づくオンデマンド ローディング モデルを実装します。
プロセスの開始(ルートノード構築プロセス)
Flutter Engine は Dart オペレーティング環境、つまり Dart Runtime に基づいています。Dart Runtime を開始する主要なプロセスは次のとおりです。
DartVM
このうち、Dart Runtime はまず仮想マシンを作成して起動し、DartVM
起動後に仮想マシンを初期化してDartIsolate
起動し、起動プロセスの最後にDartIsolate
Dart アプリケーションのエントリmain
メソッドを実行します。つまり、私たちの日々の開発における「 」lib/main.dart
の機能は次のとおりですmain()
。
void main() => runApp(MyApp());
main()
この関数は 1 つのメソッドのみを呼び出していることがわかります。メソッドで何が行われるかをrunApp()
見てみましょう。runApp()
void runApp(Widget app) {
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
binding
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
..scheduleWarmUpFrame();
}
ここのパラメータはapp
one でwidget
、開発者によって Flutter フレームワークに渡される Widget です。これは、Flutter アプリケーションの開始後に表示される最初のコンポーネントであり、フレームワークとFlutter エンジンWidgetsFlutterBinding
をバインドするブリッジです。定義されています。widget
次のように:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding._instance == null) {
WidgetsFlutterBinding();
}
return WidgetsBinding.instance;
}
}
WidgetsFlutterBinding
まず の継承関係を見ると、多くのクラスWidgetsFlutterBinding
を継承して混在していることがわかります。そのため、起動すると、これらのクラスのコンストラクターが mixin の順序でトリガーされます。BindingBase
Binding
GestureBinding
: ジェスチャ処理を担当し、window.onPointerDataPacket
コールバックを提供し、Framework ジェスチャ サブシステムをバインドし、Framework イベント モデルと基礎となるイベントのバインディング エントリです。ServicesBinding
: プラットフォーム関連の機能を提供し、window.onPlatformMessage
プラットフォーム メッセージ チャネル (メッセージ チャネル) をバインドするためのコールバックを提供し、主にネイティブ通信と Flutter 通信を処理します。SchedulerBinding
: レンダリング プロセスでのさまざまなコールバックの管理、コールバックの提供window.onBeginFrame
、window.onDrawFrame
更新イベントの監視、フレームワーク描画スケジュール サブシステムのバインドを担当します。PaintingBinding
: 描画関連ロジック、描画ライブラリのバインディングを担当し、主に画像キャッシュの処理に使用されます。SemanticsBinding
: アクセシビリティの提供、セマンティック レイヤーと Flutter エンジン間のブリッジ、主に補助機能の基礎的なサポートを担当します。RendererBinding
: レンダー ツリーの最終レンダリングを担当し、PipelineOwner
オブジェクトを保持し、 などのコールバックを提供しwindow.onMetricsChanged
ますwindow.onTextScaleFactorChanged
。これは、レンダー ツリーと Flutter エンジンの間のブリッジです。WidgetsBinding
: Flutter の 3 つのツリーの管理を担当し、BuilderOwner
オブジェクトを保持し、 などのコールバックを提供しwindow.onLocaleChanged
ますonBuildScheduled
。これは、Flutter ウィジェット レイヤーとエンジンの間のブリッジです。
これらが混在する理由を理解する前に、Binding
まず紹介しましょうWindow
。Window
これは、Flutter Framework がホスト オペレーティング システムに接続するインターフェイスです。Window
クラス定義の一部を見てみましょう。
class Window {
// 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。
// DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5
double get devicePixelRatio => _devicePixelRatio;
// Flutter UI绘制区域的大小
Size get physicalSize => _physicalSize;
// 当前系统默认的语言Locale
Locale get locale;
// 当前系统字体缩放比例。
double get textScaleFactor => _textScaleFactor;
// 当绘制区域大小改变回调
VoidCallback get onMetricsChanged => _onMetricsChanged;
// Locale发生变化回调
VoidCallback get onLocaleChanged => _onLocaleChanged;
// 系统字体缩放变化回调
VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
// 绘制前回调,一般会受显示器的垂直同步信号VSync驱动,当屏幕刷新时就会被调用
FrameCallback get onBeginFrame => _onBeginFrame;
// 绘制回调
VoidCallback get onDrawFrame => _onDrawFrame;
// 点击或指针事件回调
PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
// 调度Frame,该方法执行后,onBeginFrame和onDrawFrame将紧接着会在合适时机被调用,
// 此方法会直接调用Flutter engine的Window_scheduleFrame方法
void scheduleFrame() native 'Window_scheduleFrame';
// 更新应用在GPU上的渲染,此方法会直接调用Flutter engine的Window_render方法
void render(Scene scene) native 'Window_render';
// 发送平台消息
void sendPlatformMessage(String name, ByteData data, PlatformMessageResponseCallback callback) ;
// 平台通道消息处理回调
PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;
... //其他属性及回调
}
Window
このクラスには、現在のデバイスとシステムに関する情報と、Flutter エンジンのコールバックがいくつか含まれていることがわかります。
さて、戻ってWidgetsFlutterBinding
さまざまなミックスを見てみましょうBinding
。これらのソース コードを見ると、基本的にオブジェクトのいくつかのイベントをリッスンして処理し、フレームワーク モデルに従ってこれらのイベントをパッケージ化し、抽象化し、配布していることがBinding
わかります。これは、Flutter Engine と上位フレームワークを結び付ける「接着剤」であることがわかります。の本質は 1 つであり、それ自体に特別なロジックはないため、これらのクラスを混合することで追加の機能が得られます。Binding
Window
WidgetsFlutterBinding
WidgetsFlutterBinding
WidgetsBinding
binding
このメソッドWidgetsFlutterBinding.ensureInitialized()
は主に、グローバル シングルトンを初期化し、シングルトン オブジェクトWidgetsBinding
を返す役割を果たしますが、他には何も行いません。WidgetsBinding
これはまた、それがすべての人の肩の上にある接着剤にすぎないことを示しています。
runApp
メソッドに戻ります。WidgetsBinding
シングルトン オブジェクトを取得した後、メソッドがすぐに呼び出されWidgetsBinding
、scheduleAttachRootWidget
その中でメソッドが呼び出されますattachRootWidget
。コードは次のとおりです。
void scheduleAttachRootWidget(Widget rootWidget) {
Timer.run(() {
attachRootWidget(rootWidget); }); // 注意,不是立即执行
}
void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = rootElement == null;
_readyToProduceFrames = true; // 开始生成 Element Tree
_rootElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView, // Render Tree的根节点
debugShortDescription: '[root]',
child: rootWidget, // 开发者通过runApp传入Widget Tree的根节点
).attachToRenderTree(buildOwner!, rootElement as RenderObjectToWidgetElement<RenderBox>?);
if (isBootstrapFrame) {
SchedulerBinding.instance.ensureVisualUpdate(); // 请求渲染
}
}
Element Tree
上記のロジックは、駆動と作成のエントリ ポイントです。すべてのロジックがメッセージ ループの管理下にあることを保証するために、startによって開始されることRender Tree
に注意してください。attachRootWidget
Timer.run
attachRootWidget
Widget
このメソッドは主にルートを先頭に追加する役割を果たしますRenderView
。コードには 2 つの変数があることに注意してください。1renderView
つrenderViewElement
はレンダリング ツリーのルートですがrenderView
対応するオブジェクトです。このメソッドは主に全体を完了することがわかります。ルートそしてルートプロセスへのからルート、RenderObject
renderViewElement
renderView
Element
widget
RenderObject
Element
attachToRenderTree
このメソッドはElement Tree
ビルドを実行し、そのルート ノードを返します。ソース コードの実装は次のとおりです。
RenderObjectToWidgetElement<T> attachToRenderTree(
BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
if (element == null) {
// 首帧构建,element参数为空
owner.lockState(() {
element = createElement(); // 创建Widget对应的Element
element!.assignOwner(owner); // 绑定BuildOwner
});
owner.buildScope(element!, () {
// 开始子节点的解析与挂载
element!.mount(null, null);
});
} else {
// 如热重载等场景
element._newWidget = this;
element.markNeedsBuild();
}
return element!;
}
このメソッドは、ルート (すなわち ) の作成を担当し、に関連付けられますelement
(すなわち、ツリーに対応するツリーを作成します)。がすでに作成されている場合は、ルート内の関連付けられた を新しいものに設定します。これにより、それが一度だけ作成され、後で再利用されることがわかります。最初のフレームのパラメータがなので、まずメソッドで作成が完了し、その後のインスタンスにバインドされますが、これは何ですか? 実際、これはフレームワークの管理クラスであり、再構築する必要があるものを追跡します。このオブジェクトにより、後で更新が行われます。RenderObjectToWidgetElement
element
widget
widget
element
element
element
widget
element
element
null
createElement
BuildOwner
BuildOwner
widget
widget
Element Tree
3 つのツリーの構築が完了すると、ロジックattachRootWidget
inがトリガーされますensureVisualUpdate
。
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle: // 闲置阶段,没有需要渲染的帧
// 计算注册到本次帧渲染的一次性高优先级回调,通常是与动画相关的计算
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
case SchedulerPhase.transientCallbacks: // 处理Dart中的微任务
// 计算待渲染帧的数据,包括Build、Layout、Paint等流程,这部分内容后面将详细介绍
case SchedulerPhase.midFrameMicrotasks:
// 帧渲染的逻辑结束,处理注册到本次帧渲染的一次性低优先级回调
case SchedulerPhase.persistentCallbacks:
return;
}
}
上記のロジックは、現在のステージに応じてフレームのレンダリングを開始する必要があるかどうかを判断し、各ステージの状態遷移を図 5-8 に示します。
図 5-8 では、まず、外部 (メソッドなどsetState
) と内部 (アニメーションのハートビート、画像読み込み完了のリスナーなど) が存在しない場合、フレームワークはデフォルトの状態になりますidle
。レンダリングのための新しいフレーム データ リクエストがある場合、フレームワークは、主にアニメーション計算などの優先度の高いワンタイム コールバックを処理するために、 Engine によって駆動handleBeginFrame
されるメソッドの状態に入ります。transientCallbacks
上記のロジックが完了すると、フレームワークはそのステータスを に更新しmidFrameMicrotasks
、特定のマイクロタスク処理がエンジンによって駆動されます。次に、エンジンはメソッドを呼び出しhandleDrawFrame
、フレームワークはこの時点で状態を更新して、persistentCallbacks
主にレンダリング パイプラインに関連する、フレームごとに実行する必要があるロジックを処理することを示します。フレームワーク内のレンダリング パイプラインに関連するロジックが完了すると、フレームワークは自身の状態を更新し、postFrameCallbacks
優先度の低いワンタイム コールバック (通常は開発者または上位レベルのロジックによって登録される) を処理します。最後に、フレームワークは状態を にリセットしますidle
。idle
フレームワークの最終状態であり、状態ループはフレームのレンダリングが必要な場合にのみ開始されます。
scheduleFrame
このメソッドのロジックは次のとおりです。platformDispatcher.scheduleFrame
次の信号が到着するVsync
と、インターフェースを介してリクエストを開始してレンダリングします。
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled) return;
ensureFrameCallbacksRegistered();
platformDispatcher.scheduleFrame();
_hasScheduledFrame = true;
}
実装の話に戻りますrunApp
が、コンポーネント ツリーが構築 (ビルド) された後、呼び出しattachRootWidget
が完了する最後のステップで に実装されているWidgetsFlutterBinding
インスタンスメソッドが呼び出され、呼び出された直後に描画されます。 、このメソッドはイベントの配布をロックします。つまり、描画が完了するまで Flutter はさまざまなイベントに応答しません。これにより、描画プロセス中に新たな再描画がトリガーされないようにすることができます。メソッドのコードは次のとおりです。scheduleWarmUpFrame()
SchedulerBinding
scheduleWarmUpFrame
// flutter/packages/flutter/lib/src/scheduler/binding.dart
void scheduleWarmUpFrame() {
if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle) return; // 已发送帧渲染请求
_warmUpFrame = true;
Timeline.startSync('Warm-up frame');
final bool hadScheduledFrame = _hasScheduledFrame;
Timer.run(() {
// 第1步,动画等相关逻辑
handleBeginFrame(null);
});
Timer.run(() {
// 第2步,立即渲染一帧(通常是首帧)
handleDrawFrame();
resetEpoch();
_warmUpFrame = false; // 首帧渲染完成
if (hadScheduledFrame) scheduleFrame();
});
lockEvents(() async {
// 第3步,首帧渲染前不消费手势
await endOfFrame;
Timeline.finishSync();
});
}
Timer.run
上記のロジックは主に 3 つのステップに分かれていますが、最初の 2 つのステップがメソッド内で開始されるため、3 番目のステップが最初に実行されることに注意してください。handleBeginFrame
このメソッドはアニメーション関連のロジックをトリガーし、handleDrawFrame
3 つのツリーの更新とLayoutやPaintRender Tree
などのレンダリング ロジックをトリガーします。通常、これら 2 つのロジックは、Vsync信号をリッスンしてエンジンによって駆動されます。ここで直接実行する理由は、Vsync信号がいつ到着しても、最初のフレームができるだけ早くレンダリングされるようにするためです。レンダリングされる。
要約する
レンダリングパイプライン
前の分析では、runApp
メソッドensureInitialized
による初期化処理が実行された後、2 つのメソッドがトリガーされ、前者は Render Tree の生成を担当し、後者は最初のフレームのレンダリングのトリガーを担当しますscheduleAttachRootWidget
。scheduleWarmUpFrame
1.フレーム
描画処理のことをフレーム(フレーム)と呼びます。Flutter が 60fps (フレーム/秒) を達成できると前述したのは、1 秒あたり最大 60 回の再描画をトリガーできることを意味し、FPS 値が大きいほど、インターフェイスはよりスムーズになります。ここで説明する必要があるのは、Flutter のフレーム概念は画面更新フレーム (フレーム) と同等ではないということです。これは、UI が変更されない場合、Flutter UI フレームワークのフレームは画面が更新されるたびにトリガーされるわけではないためです。画面が更新されるたびにレンダリング プロセスを実行する必要がないため、Flutter は最初のフレームがレンダリングされた後にアクティブなリクエスト フレームを取得し、レンダリング プロセスが再実行されることを認識します。 UIが変更される可能性がある場合のみ。
- Flutter はウィンドウに
onBeginFrame
とonDrawFrame
コールバックを登録し、onDrawFrame
最終的にコールバックで呼び出されますdrawFrame
。 - メソッドを呼び出した後、Flutter エンジン
window.scheduleFrame()
は適切なタイミングで と をonBeginFrame
呼び出します (Flutter エンジンの実装によっては、次の画面更新の前と考えることができます)onDrawFrame
。
scheduleFrame()
アクティブな呼び出しのみが実行されることがわかりますdrawFrame
。したがって、 Flutter で言及する場合frame
、特に指定がない限り、drawFrame()
画面のリフレッシュ レートではなく、 の呼び出しに対応します。
2.フラッタースケジューリング処理 SchedulerPhase
Flutter アプリケーションの実行プロセスは単純にidle
と のframe
2 つの状態に分けられます。idle
状態は処理がないことを意味します。アプリケーションの状態が変化して UI を更新する必要がある場合は、新しい をリクエストするframe
必要があります。それが到着すると、Flutter アプリケーションのライフサイクル全体は、 との間でます。scheduleFrame()
frame
frame
frame
idle
frame
フレーム処理の流れ
新しいタスクframe
が到着すると、具体的なプロセスは 4 つのタスク キューを順番に実行することです。4transientCallbacks、midFrameMicrotasks、persistentCallbacks、postFrameCallbacks
つのタスク キューが実行されると、現在のタスクframe
キューが終了します。要約すると、Flutter はライフサイクル全体を 5 つの状態に分割し、SchedulerPhase
それらを列挙クラスを通じて表現します。
enum SchedulerPhase {
/// 空闲状态,并没有 frame 在处理。这种状态代表页面未发生变化,并不需要重新渲染。
/// 如果页面发生变化,需要调用`scheduleFrame()`来请求 frame。
/// 注意,空闲状态只是指没有 frame 在处理,通常微任务、定时器回调或者用户事件回调都
/// 可能被执行,比如监听了tap事件,用户点击后我们 onTap 回调就是在idle阶段被执行的。
idle,
/// 执行”临时“回调任务,”临时“回调任务只能被执行一次,执行后会被移出”临时“任务队列。
/// 典型的代表就是动画回调会在该阶段执行。
transientCallbacks,
/// 在执行临时任务时可能会产生一些新的微任务,比如在执行第一个临时任务时创建了一个
/// Future,且这个 Future 在所有临时任务执行完毕前就已经 resolve 了,这中情况
/// Future 的回调将在[midFrameMicrotasks]阶段执行
midFrameMicrotasks,
/// 执行一些持久的任务(每一个frame都要执行的任务),比如渲染管线(构建、布局、绘制)
/// 就是在该任务队列中执行的.
persistentCallbacks,
/// 在当前 frame 在结束之前将会执行 postFrameCallbacks,通常进行一些清理工作和
/// 请求新的 frame。
postFrameCallbacks,
}
3. レンダリングパイプライン
新しいものがframe
到着すると、のメソッドWidgetsBinding
が呼び出されますdrawFrame()
。その実装を見てみましょう。
void drawFrame() {
...//省略无关代码
try {
buildOwner.buildScope(renderViewElement); // 先执行构建
super.drawFrame(); //然后调用父类的 drawFrame 方法
}
}
実際、主要なコードは 2 行だけです: 最初に再構築 ( build
)、次にdrawFrame
親クラスのメソッドを呼び出します。drawFrame
親クラスのメソッドを展開した後、次のようになります。
void drawFrame() {
buildOwner!.buildScope(renderViewElement!); // 1.重新构建widget树
//下面是 展开 super.drawFrame() 方法
pipelineOwner.flushLayout(); // 2.更新布局
pipelineOwner.flushCompositingBits(); //3.更新“层合成”信息
pipelineOwner.flushPaint(); // 4.重绘
if (sendFramesToEngine) {
renderView.compositeFrame(); // 5. 上屏,会将绘制出的bit数据发送给GPU
...
}
}
主に 5 つの処理が行われていることがわかります。
- ウィジェットツリーを再構築します。
- レイアウトを更新します。
- レイヤー構成情報を更新します。
- 描き直す。
- 上画面:描画した製品を画面上に表示します。
rendering pipeline
上記の 5 つのステップを中国語訳すると「レンダリング パイプライン」または「レンダリング パイプライン」と呼びます。
Web であっても Android であっても、UI フレームワークには独自のレンダリング パイプラインがあります。レンダリング パイプラインは UI フレームワークの中核であり、ユーザー入力の処理、UI 記述の生成、描画命令のラスタライズ、画面上の最終データを担当します。フラッターも例外ではありません。自己レンダリング方式のため、Flutter のレンダリング パイプラインはプラットフォームから独立しています。Android を例に挙げると、Flutter は単にEmbedder
それを取得するSurface
か、Texture
独自のレンダリング パイプラインの最終出力ターゲットとして機能します。
Flutter のレンダリング パイプラインは、システムからのVsync信号によって駆動される必要があります。UI を更新する必要がある場合、フレームワークはEngineに通知し、エンジンは次のVsync
信号が到着するまで待機し、フレームワークにアニメーション化、ビルド、レイアウト、ペイント、そして最後にlayer
Submit to Engineを生成します。以下の図に示すように、エンジンは結合しlayer
てテクスチャを生成し、最終的にOpen GLインターフェイスを介してデータをGPUに送信し、処理後にGPU がそれをモニターに表示します。
具体的には、Flutterのレンダリングパイプラインは以下の7つのステップに分かれています。
-
(1)ユーザー入力 (ユーザー入力) : マウス、キーボード、タッチ スクリーン、その他のデバイスを通じてユーザーが生成したジェスチャ動作に応答します。
-
(2)アニメーション (Animation) : タイマー (Timer) に基づいて現在のフレームのデータを更新します。
-
(3)ビルド (Build) : 3 つのツリーの作成、更新、破棄のフェーズ
StatelessWidget
と、 およびState
のメソッドがbuild
このフェーズで実行されます。 -
(4)レイアウト:
Render Tree
この段階で各ノードのサイズと位置の計算が完了します。 -
(5)ペイント:
Render Tree
各ノードをたどって生成するメソッドがこの段階で実行さLayer Tree
れ、一連の描画命令が生成されます。RenderObject
paint
-
(6)合成:ラスタライズの入力となるオブジェクト
Layer Tree
を生成する処理Scene
。 -
(7)ラスタライズ: 描画命令をGPUでスクリーニングできる生データに処理します。
setState
更新プロセス全体を理解するために、 の更新実行プロセスを例に挙げてみましょう。
setStateの実行フロー
電話がかかってきたときsetState
:
- まず、 current
element
メソッドを呼び出して、現在を としてmarkNeedsBuild
マークします。element
_dirty
true
- 後続の呼び出しでは、
scheduleBuildFor
現在のものが のリストにelement
追加されます。BuildOwner
_dirtyElements
- 同時に新しいものがリクエストされ、
frame
描画されますframe
。onBuildScheduled->ensureVisualUpdate->scheduleFrame()
以下はsetState
実行の大まかなフローチャートです。
そのロジックはupdateChild()
次のとおりです。
このうちonBuildScheduled
、メソッドは起動フェーズで初期化され、最終的に呼び出されensureVisualUpdate
、Vsync信号の監視がトリガーされます。新しいVsync信号が到着すると、buildScope
メソッドがトリガーされ、サブツリーが再構築され、同時にレンダリング パイプライン プロセスが実行されます。
void drawFrame() {
buildOwner!.buildScope(renderViewElement!); //重新构建widget树
pipelineOwner.flushLayout(); // 更新布局
pipelineOwner.flushCompositingBits(); //更新合成信息
pipelineOwner.flushPaint(); // 更新绘制
if (sendFramesToEngine) {
renderView.compositeFrame(); // 上屏,会将绘制出的bit数据发送给GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
}
-
widget
ツリーを再構築する:dirtyElements
リストが空でない場合は、リストを走査し、各element
メソッドを呼び出してrebuild
新しいwidget
(ツリー) を再構築します。新しいwidget
(ツリー) は新しい状態で構築されるため、widget
レイアウト情報 (占有スペースと位置)が発生する可能性があります。変更されると、そのメソッドが呼び出されrenderObject
、markNeedsLayout
ノードが見つかるまで現在のノードから親まで検索しrelayoutBoundary
、グローバル リストに追加しますnodesNeedingLayout
。ルート ノードが If not found の場合relayoutBoundary
、ルート ノードもがnodesNeedingLayout
リストに追加されます。 -
レイアウトの更新: 配列を走査し、
nodesNeedingLayout
それぞれの配列renderObject
を再レイアウトし (そのメソッドを呼び出し)layout
、新しいサイズとオフセットを決定します。layout
これはメソッドで呼び出されますmarkNeedsPaint()
。これはmarkNeedsLayout
メソッドの機能と同様です。また、isRepaintBoundary
プロパティが である親ノードが見つかるまで現在のノードから親まで検索しtrue
、それをグローバルnodesNeedingPaint
リストに追加します。ルートノード (RenderView
)isRepaintBoundary
は なのでtrue
、1 つ見つけなければなりません。検索プロセスが終了するとbuildOwner.requestVisualUpdate
、メソッドが呼び出されscheduleFrame()
、最終的にメソッドが呼び出されます。このメソッドでは、まず新しいものが要求されているかどうかを判断しframe
、そうでない場合は新しいものを要求しますframe
。 -
構成情報の更新: 今のところ無視します。
-
描画の更新:
nodesNeedingPaint
リストを走査し、各ノードのメソッドを呼び出してpaint
再描画すると、描画プロセスが生成されますLayer
。Layer
Flutter での描画結果は に保存される、つまり解放されない限り描画結果はキャッシュされるため、不必要な再描画のオーバーヘッドを避けるためにLayer
描画結果を横断的にキャッシュするLayer
ことができることを説明する必要があります。frame
Flutter フレームワークの描画プロセス中に、isRepaintBoundary
を持つノードが見つかるとtrue
、新しいノードが生成されますLayer
。Layer
との間には 1 対 1 の対応がなくrenderObject
、親子ノードを共有できることがわかります。これは後の実験で検証します。もちろん、カスタム コンポーネントの場合は、renderObject に任意の数のレイヤーを手動で追加できます。これは通常、一度だけ描画する必要があり、後で変更されない描画要素のシーンをキャッシュするために使用されます。この後のデモを説明するための例。 -
上画面: 描画が完了すると
Layer
ツリー。最後にLayer
ツリー内の描画情報を画面に表示する必要があります。Flutter が自己実装されたレンダリング エンジンであることはわかっているので、描画情報を Flutter エンジンに送信する必要があり、renderView.compositeFrame
この使命は達成されました。
上記はsetState
呼び出しからUI更新までの大まかな更新処理ですが、途中build
での再呼び出しが禁止されていたりsetState
、フレームワーク側でチェックが必要になるなど、実際の処理はさらに複雑になります。もう 1 つの例は、frame
アニメーションのスケジュール設定に関係しており、アニメーションが画面上にあるときに、すべてのアニメーションがLayer
シーン (Scene) オブジェクトに追加されてから、シーンがレンダリングされます。
setState実行タイミングの問題
setState
はトリガーされますbuild
が、build
実行フェーズでpersistentCallbacks
実行されるため、このフェーズで実行されない限り絶対に安全ですが、この種の粒度は、たとえばアプリケーションの状態が変化した場合、およびフェーズsetState
で粗すぎます。最善の方法は、新しいコンポーネントをリクエストするのではなく、コンポーネントにのみマークを付けることです。これは、現在のコンポーネントがまだ実行されていないため、後で実行された後、現在のフレーム レンダリング パイプラインで UI が更新されるためです。したがって、マーキングが完了した後、スケジューリング ステータスが最初に判断され、実行フェーズであるか実行フェーズにある場合にのみ、新しいスケジュール ステータスが要求されます。transientCallbacks
midFrameMicrotasks
dirty
frame
frame
persistentCallbacks
setState
dirty
idle
postFrameCallbacks
frame
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame(); // 请求新的frame
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks: // 注意这一行
return;
}
}
上記のコードはほとんどの場合問題ありませんが、build
ステージで再度呼び出すと、setState
依然として問題が発生します。なぜなら、build
ステージで再度呼び出すとsetState
、次のような問題が発生するbuild
ためです。これにより、循環呼び出しが発生するため、フラッターが発生します。フレームワークは、build
ステージが呼び出された場合に、setState
次のようなエラーが報告されることを検出します。
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, c) {
// build 阶段不能调用 setState, 会报错
setState(() {
++index;
});
return Text('xx');
},
);
}
実行後、エラーが報告され、コンソールに次のように出力されます。
==== Exception caught by widgets library ====
The following assertion was thrown building LayoutBuilder:
setState() or markNeedsBuild() called during build.
build
で直接呼び出す場合setState
、コードは次のようになります。
Widget build(BuildContext context) {
setState(() {
++index;
});
return Text('$index');
}
実行後にエラーは報告されません。これは、build
現在のコンポーネントdirty
( middle に相当element
) の状態が実行中でありtrue
、build
実行後にのみ設定されるためですfalse
。実行時にはsetState
まず現在dirty
値を判定し、 であればtrue
そのままリターンするためエラーは報告されません。
上では、build
フェーズ内での呼び出しがsetState
エラーを引き起こすことだけを説明しました。実際、構築、レイアウト、描画フェーズ全体で同期的に呼び出すことはできません。setState
これは、これらのフェーズでの呼び出しがsetState
新しいフェーズを要求する可能性がありframe
、循環呼び出しが発生する可能性があるためです。これらの段階でアプリケーションの状態を更新する場合、それを直接呼び出すことはできませんsetState
。
セキュリティアップデート
build
これで、フェーズ内で呼び出すことができないことがわかりましたsetState
。実際、コンポーネントのレイアウトフェーズと描画フェーズ中に、同期して再レイアウトまたは再描画を直接要求することはできません。理由は同じです。正しいものはどれですか?これらのフェーズで update メソッドを使用する場合、setState
例として次のメソッドを使用できます。
// 在build、布局、绘制阶段安全更新
void update(VoidCallback fn) {
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(fn);
});
}
update
この関数はframe
の実行時にのみ実行する必要がありpersistentCallbacks
、他のステージはsetState
直接呼び出すことができることに注意してください。idle
状態は特殊なケースになるため、状態idle
で呼び出される場合は、手動で呼び出して新しい状態をリクエストするupdate
必要があります。そうしないと、次の状態(他のコンポーネントによってリクエストされた)が到着するまで実行されないため、変更できます。それ:scheduleFrame()
frame
postFrameCallbacks
frame
frame
update
void update(VoidCallback fn) {
final schedulerPhase = SchedulerBinding.instance.schedulerPhase;
if (schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(fn);
});
} else {
setState(fn);
}
}
ここまでは、update
状態を安全に更新できる関数をカプセル化しました。
ここで、「カスタム コンポーネント: CustomCheckbox」セクションで、アニメーションを実行するために、描画完了後に次のコードを通じて再描画を要求していることを思い出してください。
SchedulerBinding.instance.addPostFrameCallback((_) {
...
markNeedsPaint();
});
直接呼び出すことはしませんがmarkNeedsPaint()
、その理由は上記の通りです。
要約する
なお、Build処理とLayout処理は交互に実行することも可能である。
参考: