Flutter Notes | Flutter コア原則 (2) 主要なクラスと起動プロセス

ウィジェット、要素、BuildContext および RenderObject

ウィジェット

Widgetキークラスとそのサブクラスの継承関係を次の図に示します。

ここに画像の説明を挿入

その中には、すべてのノードの基本クラスWidgetがあります。サブクラスは主に 3 つのカテゴリに分類されます。Widget TreeWidget

  • 最初のカテゴリはのサブカテゴリで、具体的には(単一子ノード コンテナ)、(リーフ ノード)、(複数子ノード コンテナ)RenderObjectWidgetに分かれており、共通の特徴は、すべて のサブカテゴリに対応していることです。 などのロジックを実行できます。SingleChildRenderObjectWidgetLeafRenderObjectWidgetMultiChildRenderObjectWidgetRenderObjectLayoutPaint

  • 2 番目のカテゴリはStatelessWidgetと でStatefulWidget、開発者によって最も一般的に使用されますWidget。これらは自分自身を描画する機能はありません (つまり、対応していません) が、整理して構成Render ObjectすることができますRenderObjectWidgetWidget

  • カテゴリ 3 は、具体的には とProxyWidgetに細分され、子ノードに追加のデータを提供することを特徴としています。ParentDataWidgetInheritedWidget

エレメント

Elementキー クラスとそのサブクラスの継承関係を次の図に示します。

ここに画像の説明を挿入
図 5-2 から明らかな継承関係から、インターフェースをElement実装していますが、図 5-2 は図 5-1 に対応しており、それぞれ対応するものがあります2 つの直接サブクラスおよびがあり、そのうち2 つのサブクラスおよび はそれぞれおよびに対応しますBuildContextElementWidgetElementComponentElementRenderObjectElementComponentElementStatelessElementStatefulElementStatelessWidgetStatefulWidget

最終的な UI ツリーは実際には個々のノードで構成されていることがわかりますElementコンポーネントの最終的なレイアウト描画はRenderObject行われ、作成から描画までの大まかな流れは、Widget世代に応じElementて対応するものを作成して属性RenderObjectと関連付け、最後に でレイアウト配置と描画が完了します。Element.renderObjectRenderObject

ElementこれはWidgetUI ツリー内の特定の場所にインスタンス化されたオブジェクトであり、そのほとんどはElement一意ですrenderObjectが、たとえば から継承されたクラスElementなど、複数の子ノードを持つものもあります最終的には、それらすべてが 1 つのツリーを構成し、これを「レンダー ツリー」または「レンダー ツリー」と呼びますRenderObjectElementMultiChildRenderObjectElementElementRenderObject

要約すると、Flutter の UI システムには、ウィジェット ツリー要素ツリーレンダリング ツリーの3 つのツリーが含まれていると考えることができます。それらの依存関係は次のとおりです。図に示すように、要素ツリーはウィジェット ツリーに基づいて生成され、レンダリング ツリーは要素ツリーに依存します。

ここに画像の説明を挿入
ここで、次のライフサイクルElement注目してみましょう。Element

  1. フレームワークは、インスタンスをWidget.createElement作成するために呼び出します。Elementelement

  2. フレームワーク呼び出しではelement.mount(parentElement,newSlot)mountメソッド内で、まずelement対応するWidgetメソッドを呼び出して、それに関連付けられたオブジェクトcreateRenderObjectを作成します。次に、そのメソッドを呼び出して、レンダリング ツリー内のスロットで指定された位置にオブジェクトを追加します (このステップは必要ありません。通常は、ツリー構造が変更されたときに再作成される) に追加) レンダリングツリーに挿入された後は「 」状態となり「 」状態になった後は画面に表示可能(非表示可能)になります。elementRenderObjectelement.attachRenderObjectelement.renderObjectElementelementactiveactive

  3. 親の構成データがWidget変更され、State.build返されたWidget構造が以前のものと異なる場合、対応するElementツリーを再構築する必要があります。再利用するためにElementElement再構築する前に、古いツリー上の同じ位置を再利用しようとしますelement。ノードはelement、更新する前にWidget対応するcanUpdateメソッドを呼び出します。それが返された場合true、古いものは再利用されElement、古いものは次の方法で更新Elementされます。Widget新しい構成データ。それ以外の場合は、新しい構成データが作成されますElement

    Widget.canUpdate主なことは、と の合計が同時に等しいnewWidgetかどうかを判断し、同時に等しい場合は返し、そうでない場合は返しますこの原則によれば、強制的に更新する必要がある場合、別の更新を指定することで多重化を回避できます。oldWidgetruntimeTypekeytruefalseWidgetKey

  4. 祖先がElement削除を決定するとelement(たとえば、Widgetツリー構造が変更され、element対応するものWidgetが削除された場合)、祖先はそれを削除するメソッドElementを呼び出し、deactivateChild削除後にelement.renderObjectレンダリング ツリーからも削除されます。element.deactivateメソッドが呼び出され、elementステータスが「inactive」に変わります。

  5. 再度「inactive」状態はelement画面に表示されなくなります。アニメーション実行中に特定のオブジェクトの作成と削除が繰り返されるのを避けるためelement、現在のアニメーションの最後のフレームが終了するまで「inactive」状態が保持されます。elementアニメーション実行終了後に「 active」状態に戻っていない場合は、 , フレームワーク 完全に削除するにはそのメソッドが呼び出されますが、unmountこの時点では二度とツリーに挿入されないelement状態になります。defunct

  6. ツリーの別の場所 (グローバル再利用要素の場合) の祖先など、ツリーのの場所elementに再挿入する場合、フレームワークはまず既存の場所からツリーを削除し、次にそのメソッドを呼び出して、レンダリングを再度開始します。木。ElementelementelementGlobalKeyelementactivaterenderObjectattach

ここに画像の説明を挿入

要約:

  • Element オブジェクトは作成時に状態を初期化しinitialmountメソッドを介して Element Tree に追加された後の状態になりactive、ノードに対応する Widget が失敗した場合はメソッドを介して状態にdeactivate入ります。inactive現在のフレームのビルド プロセス中に他のElementノードがkeyそのノードを再利用した場合、このactivateメソッドはノードを再びその状態にするために使用されますactive。現在のフレームの終了後もノードがまだ要素ツリーにない場合は、メソッドが使用されます。unmountメソッドを通じてアンインストールされ、defunct後続のロジックが破棄されるのを待つ状態になります

Elementライフサイクルを読んだ後、開発者が要素ツリーを直接操作するのではないかと疑問に思う人もいるかもしれません。

実際、開発者にとっては、ほとんどの場合、ツリーに注目するだけで済み、WidgetFlutter フレームワークはウィジェット ツリー上の操作をElementツリーにマッピングしているため、複雑さが大幅に軽減され、開発効率が向上します。

ただし、ElementFlutter UI フレームワーク全体を理解するには、理解することが重要です。Flutter はElementこのリンクを通じて接続されWidgetRenderObject接続されます。要素レイヤーを理解することは、開発者が 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内で発生していることがわかりましたたとえば、次のとおりですStatelessElementStatefulElementbuildStatelessElement

class StatelessElement extends ComponentElement {
    
    
  ...
  
  Widget build() => widget.build(this);
  ...
}

以下にもありますStatefulElement:

class StatefulElement extends ComponentElement {
    
    
  ...	
  
  Widget build() => state.build(this);
  ...
}

build渡されたパラメータは明らかに であることがわかりますthisこれは、またはそれ自体BuildContextですしかしそれ自体はインターフェイスを実装していませんでした。引き続きコードをトレースすると、コードがクラスから間接的に継承していることがわかりました。次に、クラス定義をチェックして、クラスが実際にインターフェイスを実装していることを確認します。StatelessElementStatefulElementStatelessElementStatefulElementBuildContextElementElementElementBuildContext

abstract class ComponentElement extends Element {
    
    ...}
abstract class Element extends DiagnosticableTree implements BuildContext {
    
    ...}

これまでのところ真実は明らかであり、BuildContextwidget対応しているため、メソッドでオブジェクトに直接アクセスElementできます対象データを取得するコード内には、というメソッドがありますcontextStatelessWidgetStatefulWidgetbuildElementTheme.of(context)ElementdependOnInheritedWidgetOfExactType()

概要:BuildContextそれはElement神であり、BuildContextのメソッド呼び出しは操作でありElementWidgetそれはコートであり、Elementコートの下は裸体です。

BuildContext の別の意味

もう 1 つの意味BuildContextは、それがツリー内の位置への参照でありWidgetWidgetWidgetWidget、それ自体に関する情報ではなくツリー内の位置に関する情報がWidget含まれていることです。

トピックの場合、それぞれにWidget独自の があるためBuildContext、ツリー内に複数のトピックが分散している場合、1 つのトピックを取得すると、別のトピックとは異なる結果がWidget返される可能性があることを意味します。Widgetカウンタ アプリケーションのサンプル プログラムのトピックの特定のケース、または他のメソッドでは、ツリー内のそのタイプの最も近い親ノードをof取得します

ここに画像の説明を挿入

高度

これが内部接続とElementFlutter UI フレームワーク間のリンクであることがわかります。ほとんどの場合、開発者はレイヤーに注意を払うだけで済みますが、レイヤーが詳細を完全にシールドできない場合があるため、フレームワークはオブジェクトを次のオブジェクト渡します。このようにして、開発者は必要に応じてオブジェクトを直接操作できますwidgetRenderObjectwidgetwidgetElementStatelessWidgetStatefulWidgetbuildElementElement

そこで、次の 2 つの質問があります。

1. ウィジェットレイヤーがない場合、エレメントレイヤーだけで利用可能なUIフレームワークを構築できますか? もしそうなら、それはどのように見えるべきですか?
2. Flutter UI フレームワークは応答しない可能性がありますか?

質問 1 については、答えはもちろん「はい」です。widgetツリーは単なるElementツリーのマッピングであり、UI ツリー、Widgetつまりコートを説明する構成情報のみを提供すると前述したためです。もちろん、人は恥をかいて生きることができます服を着なくても服を着たほうがまともな生活ができるし、服Widgetに頼らなくてもElementUIフレームワークを完全に構築できる。

以下に例を示します。

純粋に関数を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 のパラメータは、現在のオブジェクト自体がインスタンスであるため、直接渡すことができます。StatelessWidgetStatefulWidgetbuild(BuildContext)BuildContextthisTheme.of(this)thisElement

  • text変更が発生すると、markNeedsBuild()メソッドを呼び出して現在のものElementをマークしますdirty。マークされたものは次のフレームで再構築されますdirtyElement実はState.setState()内部的に呼ばれているメソッドでもありますmarkNeedsBuild()

  • 上記のコードのメソッドはbuild依然として 1 を返します。これは、Flutter フレームワークにこの層がwidgetすでに存在し、コンポーネント ライブラリがすでにの形式で提供されているためです。Flutter フレームワークのすべてのコンポーネントが例の形式提供されている場合の場合は、pure を使用してUI を構築できます。メソッドの戻り値の型は ですwidgetwidgetHomeViewElementElementHomeViewbuildElement

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と実行プロセスを実装することですbuildelement

ここに画像の説明を挿入

RenderObjectキー クラスとそのサブクラスを図 5-3 に示します。その各サブクラスはノードRenderObjectWidgetのタイプに対応しますWidget

  • RenderView特別な、レンダー ツリー全体のルート ノードRenderObjectです
  • もう 1 つの特別な点は、それが抽象クラスであることRenderObjectです。はそのインターフェイスを実装し、間接的に を継承しますRenderAbstractViewportRenderViewportRenderBox
  • RenderBoxRenderSliverFlutter で最も一般的なものでRenderObjectRenderBox、列などの一般的なレイアウトを担当し、RenderSliverlist 内のそれぞれのレイアウトを担当しますItem

RenderObjectこれには、レンダリング ツリー内の独自の親ノードを指す1parentつと 1 つのparentData属性がありますが、予約された変数です。親コンポーネントのレイアウト プロセス中に、そのすべての子コンポーネントのレイアウト情報 (位置情報など) を決定します。は、親コンポーネントに対する相対的なオフセットです)。これらのレイアウト情報は、後続の描画フェーズ (コンポーネントの描画位置を決定するため) で使用する必要があるため、レイアウト フェーズで保存する必要があります。属性の主な機能は、 layoutなどのレイアウト情報を保存することです子要素のオフセット データは子要素に格納されます(詳細は実装を参照)。parentparentDataparentDataStackRenderStackparentDataPositioned

質問: があるのにRenderObject、なぜ Flutter フレームワークはRenderBoxRenderSliver2 つのサブクラスを提供するのですか?

  • これは、RenderObjectクラス自体が基本的なレイアウトおよび描画プロトコルのセットを実装しますが、子ノード モデル (たとえば、ノードが持つことができる子ノードの数など) や座標系 (たとえば、 、子ノードの位置決めはフルート カール座標または極座標で行われます) および特定のレイアウト プロトコル (幅と高さによるか、制約とサイズによるか)、または親ノードが子ノードのサイズと位置を設定する前または後に設定します。子ノードのレイアウトなど)。

  • この目的のために、Flutter フレームワークはRenderBoxRenderSliverクラスを提供します。これらはすべて から継承されRenderObject、レイアウト座標系はデカルト座標系を採用し、画面が(top, left)原点となります。これら 2 つのクラスに基づいて、Flutter はRenderBoxに基づくボックス モデルレイアウトと、にSliver基づくオンデマンド ローディング モデルを実装します。

プロセスの開始(ルートノード構築プロセス)

Flutter Engine は Dart オペレーティング環境、つまり Dart Runtime に基づいています。Dart Runtime を開始する主要なプロセスは次のとおりです。

ここに画像の説明を挿入

DartVMこのうち、Dart Runtime はまず仮想マシンを作成して起動し、DartVM起動後に仮想マシンを初期化してDartIsolate起動し、起動プロセスの最後にDartIsolateDart アプリケーションのエントリ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();
}

ここのパラメータはappone で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 の順序でトリガーされます。BindingBaseBinding

ここに画像の説明を挿入

  • GestureBinding: ジェスチャ処理を担当し、window.onPointerDataPacketコールバックを提供し、Framework ジェスチャ サブシステムをバインドし、Framework イベント モデルと基礎となるイベントのバインディング エントリです。
  • ServicesBinding: プラットフォーム関連の機能を提供し、window.onPlatformMessageプラットフォーム メッセージ チャネル (メッセージ チャネル) をバインドするためのコールバックを提供し、主にネイティブ通信と Flutter 通信を処理します。
  • SchedulerBinding: レンダリング プロセスでのさまざまなコールバックの管理、コールバックの提供window.onBeginFramewindow.onDrawFrame更新イベントの監視、フレームワーク描画スケジュール サブシステムのバインドを担当します。
  • PaintingBinding: 描画関連ロジック、描画ライブラリのバインディングを担当し、主に画像キャッシュの処理に使用されます。
  • SemanticsBinding: アクセシビリティの提供、セマンティック レイヤーと Flutter エンジン間のブリッジ、主に補助機能の基礎的なサポートを担当します。
  • RendererBinding: レンダー ツリーの最終レンダリングを担当し、PipelineOwnerオブジェクトを保持し、 などのコールバックを提供しwindow.onMetricsChangedますwindow.onTextScaleFactorChangedこれは、レンダー ツリーと Flutter エンジンの間のブリッジです。
  • WidgetsBinding: Flutter の 3 つのツリーの管理を担当し、BuilderOwnerオブジェクトを保持し、 などのコールバックを提供しwindow.onLocaleChangedますonBuildScheduledこれは、Flutter ウィジェット レイヤーとエンジンの間のブリッジです。

これらが混在する理由を理解する前に、Bindingまず紹介しましょうWindowWindowこれは、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 つであり、それ自体に特別なロジックはないため、これらのクラスを混合することで追加の機能が得られます。BindingWindowWidgetsFlutterBindingWidgetsFlutterBindingWidgetsBindingbinding

このメソッドWidgetsFlutterBinding.ensureInitialized()は主に、グローバル シングルトンを初期化し、シングルトン オブジェクトWidgetsBindingを返す役割を果たしますが、他には何も行いません。WidgetsBindingこれはまた、それがすべての人の肩の上にある接着剤にすぎないことを示しています。

runAppメソッドに戻ります。WidgetsBindingシングルトン オブジェクトを取得した後、メソッドがすぐに呼び出されWidgetsBindingscheduleAttachRootWidgetその中でメソッドが呼び出されます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に注意してください。attachRootWidgetTimer.run

attachRootWidgetWidgetこのメソッドは主にルートを先頭に追加する役割を果たしますRenderView。コードには 2 つの変数があることに注意してください。1renderViewrenderViewElementレンダリング ツリーのルートですrenderView対応するオブジェクトです。このメソッドは主に全体を完了することがわかります。ルートそしてルートプロセスへのからルート、RenderObjectrenderViewElementrenderViewElementwidgetRenderObjectElement

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(すなわち、ツリーに対応するツリーを作成します)。がすでに作成されている場合は、ルート内の関連付けられた を新しいものに設定します。これにより、それが一度だけ作成され、後で再利用されることがわかります。最初のフレームのパラメータがなので、まずメソッドで作成が完了し、その後のインスタンスにバインドされますが、これは何ですか? 実際、これはフレームワークの管理クラスであり、再構築する必要があるものを追跡します。このオブジェクトにより、後で更新が行われます。RenderObjectToWidgetElementelementwidgetwidgetelementelementelementwidgetelementelementnullcreateElementBuildOwnerBuildOwnerwidgetwidgetElement Tree

3 つのツリーの構築が完了すると、ロジックattachRootWidgetinがトリガーされます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優先度の低いワンタイム コールバック (通常は開発者または上位レベルのロジックによって登録される) を処理します。最後に、フレームワークは状態を にリセットしますidleidleフレームワークの最終状態であり、状態ループはフレームのレンダリングが必要な場合にのみ開始されます。

scheduleFrameこのメソッドのロジックは次のとおりです。platformDispatcher.scheduleFrame次の信号が到着するVsyncと、インターフェースを介してリクエストを開始してレンダリングします。

void scheduleFrame() {
    
    
  if (_hasScheduledFrame || !framesEnabled) return;
  ensureFrameCallbacksRegistered();  
  platformDispatcher.scheduleFrame();
  _hasScheduledFrame = true;
}

実装の話に戻りますrunAppが、コンポーネント ツリーが構築 (ビルド) された後、呼び出しattachRootWidgetが完了する最後のステップで に実装されているWidgetsFlutterBindingインスタンスメソッドが呼び出され、呼び出された直後に描画されます。 、このメソッドはイベントの配布をロックします。つまり、描画が完了するまで Flutter はさまざまなイベントに応答しません。これにより、描画プロセス中に新たな再描画がトリガーされないようにすることができます。メソッドのコードは次のとおりです。scheduleWarmUpFrame()SchedulerBindingscheduleWarmUpFrame

// 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このメソッドはアニメーション関連のロジックをトリガーし、handleDrawFrame3 つのツリーの更新とLayoutPaintRender Treeなどのレンダリング ロジックをトリガーします。通常、これら 2 つのロジックは、Vsync信号をリッスンしてエンジンによって駆動されます。ここで直接実行する理由は、Vsync信号がいつ到着しても、最初のフレームができるだけ早くレンダリングされるようにするためです。レンダリングされる。

要約する

ここに画像の説明を挿入

レンダリングパイプライン

前の分析では、runAppメソッドensureInitializedによる初期化処理が実行された後、2 つのメソッドがトリガーされ、前者は Render Tree の生成を担当し、後者は最初のフレームのレンダリングのトリガーを担当しますscheduleAttachRootWidgetscheduleWarmUpFrame

1.フレーム

描画処理のことをフレーム(フレーム)と呼びます。Flutter が 60fps (フレーム/秒) を達成できると前述したのは、1 秒あたり最大 60 回の再描画をトリガーできることを意味し、FPS 値が大きいほど、インターフェイスはよりスムーズになります。ここで説明する必要があるのは、Flutter のフレーム概念は画面更新フレーム (フレーム) と同等ではないということです。これは、UI が変更されない場合、Flutter UI フレームワークのフレームは画面が更新されるたびにトリガーされるわけではないためです。画面が更新されるたびにレンダリング プロセスを実行する必要がないため、Flutter は最初のフレームがレンダリングされた後にアクティブなリクエスト フレームを取得し、レンダリング プロセスが再実行されることを認識します。 UIが変更される可能性がある場合のみ。

  1. Flutter はウィンドウにonBeginFrameonDrawFrameコールバックを登録し、onDrawFrame最終的にコールバックで呼び出されますdrawFrame
  2. メソッドを呼び出した後、Flutter エンジンwindow.scheduleFrame()は適切なタイミングで と をonBeginFrame呼び出します (Flutter エンジンの実装によっては、次の画面更新の前と考えることができます) onDrawFrame

scheduleFrame()アクティブな呼び出しのみが実行されることがわかりますdrawFrameしたがって、 Flutter で言及する場合frame、特に指定がない限り、drawFrame()画面のリフレッシュ レートではなく、 の呼び出しに対応します。

2.フラッタースケジューリング処理 SchedulerPhase

Flutter アプリケーションの実行プロセスは単純にidleと のframe2 つの状態に分けられます。idle状態は処理がないことを意味します。アプリケーションの状態が変化して UI を更新する必要がある場合は、新しい をリクエストするframe必要がありますそれが到着すると、Flutter アプリケーションのライフサイクル全体は、 との間でますscheduleFrame()frameframeframeidleframe

フレーム処理の流れ

新しいタスク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 つの処理が行われていることがわかります。

  1. ウィジェットツリーを再構築します。
  2. レイアウトを更新します。
  3. レイヤー構成情報を更新します。
  4. 描き直す。
  5. 上画面:描画した製品を画面上に表示します。

rendering pipeline上記の 5 つのステップを中国語訳すると「レンダリング パイプライン」または「レンダリング パイプライン」と呼びます。

Web であっても Android であっても、UI フレームワークには独自のレンダリング パイプラインがあります。レンダリング パイプラインは UI フレームワークの中核であり、ユーザー入力の処理、UI 記述の生成、描画命令のラスタライズ、画面上の最終データを担当します。フラッターも例外ではありません。自己レンダリング方式のため、Flutter のレンダリング パイプラインはプラットフォームから独立しています。Android を例に挙げると、Flutter は単にEmbedderそれを取得するSurfaceか、Texture独自のレンダリング パイプラインの最終出力ターゲットとして機能します。

ここに画像の説明を挿入

Flutter のレンダリング パイプラインは、システムからのVsync信号によって駆動される必要があります。UI を更新する必要がある場合、フレームワークはEngineに通知しエンジンは次のVsync信号が到着するまで待機し、フレームワークアニメーション化、ビルド、レイアウト、ペイント、そして最後にlayerSubmit 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、一連の描画命令が生成されます。RenderObjectpaint

  • (6)合成:ラスタライズの入力となるオブジェクトLayer Treeを生成する処理Scene

  • (7)ラスタライズ: 描画命令をGPUでスクリーニングできる生データに処理します。

setState更新プロセス全体を理解するために、 の更新実行プロセスを例に挙げてみましょう。

setStateの実行フロー

電話がかかってきたときsetState:

  • まず、 currentelementメソッドを呼び出して、現在を としてmarkNeedsBuildマークしますelement_dirtytrue
  • 後続の呼び出しでは、scheduleBuildFor現在のものが のリストにelement追加されますBuildOwner_dirtyElements
  • 同時に新しいものがリクエストされframe描画されますframeonBuildScheduled->ensureVisualUpdate->scheduleFrame()

以下はsetState実行の大まかなフローチャートです。

ここに画像の説明を挿入

そのロジックはupdateChild()次のとおりです。

ここに画像の説明を挿入

このうちonBuildScheduled、メソッドは起動フェーズで初期化され、最終的に呼び出されensureVisualUpdateVsync信号の監視がトリガーされます。新しい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;
  }
}
  1. widgetツリーを再構築する:dirtyElementsリストが空でない場合は、リストを走査し、各elementメソッドを呼び出してrebuild新しいwidget(ツリー) を再構築します。新しいwidget(ツリー) は新しい状態で構築されるため、widgetレイアウト情報 (占有スペースと位置)が発生する可能性があります。変更されると、そのメソッドが呼び出されrenderObjectmarkNeedsLayoutノードが見つかるまで現在のノードから親まで検索しrelayoutBoundary、グローバル リストに追加しますnodesNeedingLayout。ルート ノードが If not found の場合relayoutBoundary、ルート ノードもがnodesNeedingLayoutリストに追加されます。

  2. レイアウトの更新: 配列を走査し、nodesNeedingLayoutそれぞれの配列renderObjectを再レイアウトし (そのメソッドを呼び出し) layout、新しいサイズとオフセットを決定します。layoutこれはメソッドで呼び出されますmarkNeedsPaint()。これはmarkNeedsLayoutメソッドの機能と同様です。また、isRepaintBoundaryプロパティが である親ノードが見つかるまで現在のノードから親まで検索しtrue、それをグローバルnodesNeedingPaintリストに追加します。ルートノード ( RenderView)isRepaintBoundaryは なのでtrue、1 つ見つけなければなりません。検索プロセスが終了するとbuildOwner.requestVisualUpdate、メソッドが呼び出されscheduleFrame()、最終的にメソッドが呼び出されます。このメソッドでは、まず新しいものが要求されているかどうかを判断しframe、そうでない場合は新しいものを要求しますframe

  3. 構成情報の更新: 今のところ無視します。

  4. 描画の更新:nodesNeedingPaintリストを走査し、各ノードのメソッドを呼び出してpaint再描画すると、描画プロセスが生成されますLayerLayerFlutter での描画結果は に保存される、つまり解放されない限り描画結果はキャッシュされるため、不必要な再描画のオーバーヘッドを避けるためにLayer描画結果を横断的にキャッシュするLayerことができることを説明する必要があります。frameFlutter フレームワークの描画プロセス中に、isRepaintBoundaryを持つノードが見つかるとtrue、新しいノードが生成されますLayerLayerとの間には 1 対 1 の対応がなくrenderObject、親子ノードを共有できることがわかります。これは後の実験で検証します。もちろん、カスタム コンポーネントの場合は、renderObject に任意の数のレイヤーを手動で追加できます。これは通常、一度だけ描画する必要があり、後で変更されない描画要素のシーンをキャッシュするために使用されます。この後のデモを説明するための例。

  5. 上画面: 描画が完了するとLayerツリー。最後にLayerツリー内の描画情報を画面に表示する必要があります。Flutter が自己実装されたレンダリング エンジンであることはわかっているので、描画情報を Flutter エンジンに送信する必要があり、renderView.compositeFrameこの使命は達成されました。

上記はsetState呼び出しからUI更新までの大まかな更新処理ですが、途中buildでの再呼び出しが禁止されていたりsetState、フレームワーク側でチェックが必要になるなど、実際の処理はさらに複雑になります。もう 1 つの例は、frameアニメーションのスケジュール設定に関係しており、アニメーションが画面上にあるときに、すべてのアニメーションがLayerシーン (Scene) オブジェクトに追加されてから、シーンがレンダリングされます。

setState実行タイミングの問題

setStateはトリガーされますbuildが、build実行フェーズでpersistentCallbacks実行されるため、このフェーズで実行されない限り絶対に安全ですが、この種の粒度は、たとえばアプリケーションの状態が変化した場合、およびフェーズsetStateで粗すぎます。最善の方法は、新しいコンポーネントをリクエストするのではなく、コンポーネントにのみマークを付けることです。これは、現在のコンポーネントがまだ実行されていないため、後で実行された後、現在のフレーム レンダリング パイプラインで UI が更新されるためです。したがって、マーキングが完了した後、スケジューリング ステータスが最初に判断され、実行フェーズであるか実行フェーズにある場合にのみ、新しいスケジュール ステータスが要求されますtransientCallbacksmidFrameMicrotasksdirtyframeframepersistentCallbackssetStatedirtyidlepostFrameCallbacksframe

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) の状態が実行中でありtruebuild実行後にのみ設定されるためです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()framepostFrameCallbacksframeframeupdate

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処理は交互に実行することも可能である。


参考:

おすすめ

転載: blog.csdn.net/lyabc123456/article/details/130931167