Flutter をすばやく学び、使用する 24 レクチャー - 22 セルフレンダリング モード: Flutter のレンダリング原理からパフォーマンス最適化戦略について詳しく学びます

セルフ レンダリング モードでは、3 つのツリーをフラッターすることが重要な知識ポイントになります。このレッスンでは、Flutter の自己レンダリング モードの 3 つのツリーについて学び、次に 3 つのツリーの描画プロセスから、Flutter がパフォーマンスを最適化する方法と、Flutter アプリのパフォーマンスを向上させる方法を学びます。

3本の木

Flutter には、Widget、Element、RenderObject という 3 つのツリーがあります。

  • ウィジェットは UI インターフェイスを記述するために使用され、主にいくつかの基本的な UI レンダリング構成情報が含まれます。

  • 要素は、Widget と RenderObject の間のフロントエンドの仮想 Dom に似ています。

  • RenderObject は実際にレンダリングする必要があるツリーであり、レンダリング エンジンは RenderObject に基づいてインターフェイスをレンダリングします。

Flutter での一連の処理の後、図 1 に示すような構成情報が生成されます (このレンダリング ツリーの構造情報を取得するには、デバッグ モードを使用できます)。

図面 0.png

図1 レンダリングツリー構造

図 1 では、次の 3 つの属性がより重要です。

  • _widget はウィジェット ツリーと呼ばれるものです。

  • _chilid は要素ツリーと呼ばれるものです。

  • _renderObject は RenderObject ツリーです。

上記のレンダリング ツリー構造は、次のようなウィジェットの非常に単純な構成です。

void main() {
  runApp(MaterialApp(
    title: 'Navigation Basics',
    home: FirstRoute(),
  ));
}
class FirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('flutter test'),
    );
  }
}

上記のコードは単純なページ コンポーネントを記述していますが、この単純なページ コンポーネントの背後には非常に複雑なツリー構造があります。図 2 に示すように、レンダリングされた要素ツリーがどのように見えるかを見てください。

図面 1.png

図2 要素ツリー構造

非常に単純な Widget であるにもかかわらず、実際に Flutter で生成される要素ツリー構造図は非常に複雑であることに気づいたことはありませんか。ツリーの一番下に、使用するコンポーネント FirstRoute->Center->Text->RichText があることに気づきましたか (図 2 の赤い部分)。3 つのツリーの構造を理解した後、3 つのツリーがどのように変形されるかを見てみましょう。

3本の木の対応関係

Flutterでは、WidgetとElementツリーの間には1対1の対応がありますが、RenderObjectとは1対1の対応はありません。一部のウィジェットはレンダリングする必要がないため、たとえば、上記のテスト コードの FirstRoute はレンダリングする必要のないウィジェットです。最終的には、RenderObjectWidget に関連するウィジェットのみが RenderObject に変換され、このタイプのみをレンダリングする必要があります。表 1 に示す 3 つのツリー部分タイプ間の対応関係がわかります。

図面 2.png

表 1 ウィジェット、要素、レンダーオブジェクトの対応

次に、この 3 つがどのように変化するかを見てみましょう。

Threetrees の変換プロセス

Flutter の操作におけるコア ロジックの一部は、これら 3 つのツリーの変換を処理することであり、すべてのインターフェイスの対話とイベント処理は、最終的にこれら 3 つのツリーに対する操作の結果に反映されます。通常の状況では、これが Flutter プロジェクトの実行方法です。

void main() {
  runApp(MaterialApp(
    title: 'Navigation Basics',
    home: FirstRoute(),
  ));
}

MaterialApp は説明した Widget であり、Flutter は、scheduleAttachRootWidget、attachRootWidget、attachToRenderTree を通じて RenderObjectToWidgetElement のマウント メソッドを呼び出します。このプロセスには非常に多くのソース コード関数が関係しますが、ここではさらに重要な関数をいくつか選択して紹介します。

重要な機能の説明

機能を紹介する前に、図 3 に示す全体的なアーキテクチャのフローチャートを見てみましょう。

図面 3.png

図3 フラッターツリー変換図

上の図は比較的複雑なので、最初に簡単に理解してください。後で詳しく説明します。まず、これらの主要な機能の機能を見てみましょう。

  • ScheduleAttachRootWidget、ルート ウィジェットを作成し、ルート ウィジェットから子ノードまで要素 Element を再帰的に作成し、子ノードが RenderObjectWidget であるウィジェットの RenderObject ツリー ノードを作成し、それによって View のレンダリング ツリーを作成します。ここのソースコードではタスクが使用されていますが、その目的はマイクロタスクの実行への影響を避けることです。
void scheduleAttachRootWidget(Widget rootWidget) {
  Timer.run(() {
    attachRootWidget(rootWidget);
  });
}

  • AttachRootWidget は、scheduleAttachRootWidget と同じ機能を持ち、最初にルート ノードを作成し、その後、attachToRenderTree を呼び出してループで子ノードを作成します。
void attachRootWidget(Widget rootWidget) {
  _readyToProduceFrames = true;
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget,
  ).attachToRenderTree(buildOwner, renderViewElement as RenderObjectToWidgetElement<RenderBox>);
}

  • attachToRenderTree、このメソッドには 2 つの重要な呼び出しがあります。コア コード部分の例のみを示します。BuildScope が最初に実行されますが、2 番目のパラメーター (コールバック関数、つまり element.mount) が buildScope で最初に呼び出されます。 、マウントはループ内に子ノードを作成し、作成プロセス中に更新する必要があるデータをダーティとしてマークします。
owner.buildScope(element, () {
  element.mount(null, null);
});

  • buildScope の場合、最初のレンダリング ダーティが空のリストであるため、最初のレンダリングにこの関数の実行フローがない場合、この関数のコアは、2 番目のレンダリングまたは setState の後にダーティとマークされた要素がある場合にのみ有効になります。この関数の目的は、ダーティ配列を循環することでもあります。要素に子がある場合、再帰的に子要素を決定し、子要素を構築して、新しい要素を作成するか、要素を変更するか、RenderObject を作成します。

  • updateChild、このメソッドは非常に重要です。すべての子ノードはこの関数を通じて処理されます。この関数では、Flutter が Element と RenderObject の間の変換ロジックを処理し、Element ツリーの中間状態を通じて RenderObject ツリーへの影響を軽減します。パフォーマンスの向上。この関数の具体的なコードロジックを分解して分析してみましょう。この関数の入力パラメータには、Element child、Widget newWidget、および Dynamic newSlot の 3 つのパラメータが含まれます。child は現在のノードの要素情報、newWidget はウィジェット ツリーの新しいノード、newSlot はノードの新しい位置です。パラメーターを理解したら、コア ロジックを見てみましょう。まず、新しい Widget ノードがあるかどうかを判断します。

if (newWidget == null) {
  if (child != null)
    deactivateChild(child);
  return null;
}

存在しない場合、現在のノードの要素は直接破棄されます。ノードがウィジェットに存在し、ノードも要素にも存在する場合は、最初に 2 つのノードが一貫しているかどうかを判断します (例:コードが一致していても位置が異なる場合は、位置を更新するだけです。それ以外の場合は、子ノードが更新できるかどうかを判断し、更新できる場合は更新し、そうでない場合は、元の Element 子ノードを破棄して新しいノードを作成します。

if (hasSameSuperclass && child.widget == newWidget) {
  if (child.slot != newSlot)
    updateSlotForChild(child, newSlot);
  newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
  if (child.slot != newSlot)
    updateSlotForChild(child, newSlot);
  child.update(newWidget);
  assert(child.widget == newWidget);
  assert(() {
    child.owner._debugElementWasRebuilt(child);
    return true;
  }());
  newChild = child;
} else {
  deactivateChild(child);
  assert(child._parent == null);
  newChild = inflateWidget(newWidget, newSlot);
}

上記のコードの 8 行目は非常に重要です。child.update 関数のロジックでは、現在のノードのタイプに応じてさまざまな更新が呼び出されます。図 3 の更新中のプロセスを参照できます。各プロセスは、子ノードを再帰的に実行し、updateChild にループバックします。次の 3 つのコア関数、つまり、performRebuild、inflateWidget、markNeedsBuild が updateChild プロセスに再度入ります。次に、これら 3 つの関数の具体的な関数を見てみましょう。

  • PerformRebuild は非常に重要なコード部分です。この部分はコンポーネントに記述するビルド ロジック関数です。StatelessWidget と StatefulWidget のビルド関数はここで実行されます。実行が完了すると、ノードの子ノードとして使用されます。そして、updateChild 再帰関数を入力します。

  • inflateWidget は新しいノードを作成しますが、作成完了後は現在の Element タイプに応じて RenderObjectElement か ComponentElement かを判断します。2 つのタイプの違いに応じて、現在のノードにマウントするために異なるマウントが呼び出されます。2 つのタイプのマウントでは、子ノードが循環され、updateChild が呼び出されて子ノードの更新プロセスに戻ります。ここでもう一つポイントがあり、RenderObjectElementの場合はRenderObjectが作成されます。

  • markNeedsBuild をダーティとしてマークし、scheduleBuildFor を呼び出して次の buildScope 操作を待ちます。

上記は重要な機能の一部ですが、その他の機能については、公式 Web サイトのドキュメントをご自身で確認してください。次に、図 3 のフローチャートと合わせて、最初のビルド処理と setState 処理の 2 つの処理を組み合わせて説明します。

最初のビルド

ページ コンポーネントを初めてロードするときは、すべてのノードが存在しないため、このときのプロセスのほとんどは、図 4 に示すように新しいノードを作成することです。

図面 4.png

図 4 最初のビルド プロセス

runAppからRenderObjectToWidgetElement(mount)までのロジックは同じで、_rebuildではupdateChildを呼び出してノードを更新しますが、ノードが存在しないのでこのときinflateWidgetを呼び出してElementを作成します。

Element が Component の場合、Component.mount が呼び出されます。Element は Component.mount に作成され、現在のノードにマウントされます。次に、_firstBuild が呼び出されてサブコンポーネントがビルドされます。ビルドが完了すると、ビルドされたコンポーネントが使用されますコンポーネントとして、updateChild のサブコンポーネント更新を入力します。

ElementがRenderObjectElementの場合、RenderObjectElement.mountが呼び出され、RenderObjectElement.mount内でRenderObjectElementが作成され、createRenderObjectが呼び出されてRenderObjectが作成され、RenderObjectとRenderObjectElementが要素ツリーとRenderObjectツリーにマウントされます。最後に、同じ呼び出しが updateChild されて再帰的に子ノードが作成されます。

上記は最初のビルドのロジックで、単体で見ると非常にわかりやすいですが、次に setState のロジックを見てみましょう。

setState

setState を呼び出すと、実際にはコンポーネントの markNeedsBuild を呼び出します。この関数は、コンポーネントをダーティに設定し、次の buildScope のロジックを追加し、次の再構築サイクルを待つために上で導入されました。このプロセスを図 5 に示します。buildScope は、rebuild を呼び出してからビルド操作に入り、それによって updateChild ループ システムに入ります。

図面 6.png

図5 setStateプロセス

図 5 では、Flutter では、親ノードが更新された場合、つまり、setState 呼び出しにより、ビルド ロジックを決定するために子ノードの再帰ループが確実に発生しますが、RenderObject ツリーが必ずしも作成されるわけではないことがわかります (子ノードが作成されない可能性があるため)、ノードは変更されていないため、変更はありません)、ただし、パフォーマンスへの一定の影響は依然としてあります。

上記は Threetrees の変換処理であり、コア処理以外の機能は省略していますが、興味のある方はFlutter 公式サイトの Githubで学ぶことができます。全体的なプロセスをマスターしたら、次にこの変換プロセスから Flutter APP のパフォーマンスを向上させることができる重要なポイントを抽出します。

パフォーマンス向上のポイント

図 3 のプロセス全体では、updateChild 関数に特別な注意を払う必要があります。これは、ウィジェットから要素、レンダーオブジェクトに至るまでの Flutter のパフォーマンス向上の重要なポイントでもあります。この関数の機能は上で紹介しましたが、WidgetからElementへ変換し、ElementからRenderObjectへ変換する過程で、細かい判断と最適化を行うのがポイントで、その詳細な処理とは以下の5点です。

  • 新しいノードを削除する場合は、Element ノードを直接削除します。

  • ノードが存在し、コンポーネントの種類が同じで、コンポーネントが等しい場合、ノードの位置が更新されます。

  • ノードが存在する場合、コンポーネントの種類は同じですが、コンポーネントが異なり、コンポーネントを更新できる場合、コンポーネントが更新されます。現在のコンポーネントが更新されるため、現在のコンポーネントの子ノードも更新する必要があるため、 update は、子ノード リストを更新するために呼び出されます。このプロセスでは、ノードの RenderObject の子ノードも更新されます。

  • ノードが存在する場合、コンポーネントの種類は同じですが、コンポーネントが異なり、第二に、コンポーネントを更新できず、ノードが作成され、その作成プロセス中に RenderObject であるかどうかが判断されます。 RenderObject が作成され、子ノードがループで判断されます。

  • ノードが存在しない場合でも、作成プロセスは同じになります。

このようにして、RenderObject に対するウィジェットの影響を軽減でき、作成および更新する必要があるノードのみが RenderObject ツリーに反映されます。このツリー ノードの変換プロセスから、APP のパフォーマンスを向上させるための次の 4 つの重要なポイントを抽出できます。

定数

上で述べたように、親コンポーネントの更新により、子コンポーネントの再構築が必要になります。1 つの方法は、ステートフル コンポーネントの下の子コンポーネントの数を減らすことです。もう 1 つの方法は、できる限り多くの const コンポーネントを使用することです。親コンポーネントが更新されても、子コンポーネントは更新されず、再構築操作が再度実行されます。上記の判定ロジックと、ノードが存在し、コンポーネントの種類が同じで、コンポーネントが等しい場合の処理​​ロジックです。

また、プロジェクトのソース コードには、実用的な最適化ポイントがいくつかあります。特に、一般的なエラー報告コンポーネントや一般的な読み込みコンポーネントなど、長期間変更されない一部のコンポーネントは、変数のないコンポーネントに対してのみ返されます。 、コードの次の部分など。

if (error) {
    return CommonError(action: this.setFirstPage);
  }
  if (contentList == null) {
    return const Loading();
  }
}

コードの 2 行目に変数アクションがあるため、const に設定できません。次の Loading は変数を持たないため、const に設定できます。他のコードも同じ方法で変更でき、特にコンポーネントの設計が不合理な場合には、パフォーマンスの向上に役立ちます。

更新できる

上記のupdateChildの処理にはcanUpdateという実行関数があり、これもパフォーマンス向上のポイントであり、特に複数の要素を調整する必要がある場合には、具体的なロジックの実装を確認することができます。

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

主な目的は、実行時のクラスが同じかどうかを判断すると同時に、キーが同じかどうかを判断することであり、同じであれば、コンポーネントの要素の位置を直接更新してパフォーマンスを向上させることができます。コンポーネントでは、コンポーネントのキーの変更を最小限に抑えるようにしてください。コンポーネントのキーはデフォルトで空に設定できます。

次に、コンポーネントを頻繁に並べ替え、削除、または追加する必要がある場合は、コンポーネントにキーを追加してパフォーマンスを向上させることが最善です。ここで注意していただきたいのは、StatefulWidget の状態は Element に保存されるため、同じクラス名 (runtimeType) を持つ 2 つの Widget を区別したい場合は、異なるキーを持たないと新旧の Widget の変更を区別できなくなり、特にリストでは、データ、各リストはステートフル クラスです。リスト内の項目のリストを切り替える必要がある場合は、キーを設定する必要があります。設定しないと、シーケンスの切り替えが失敗します。これについて詳しくは、この英語の記事を参照してください。

ウィジェットを膨張させる

updateChild の inflateWidget 実行関数も重要なパフォーマンス向上ポイントです。この関数は、作成前にキーが GlobalKey であるかどうかをチェックします。そうであれば、要素が存在することを意味し、この時点で直接有効にすることができます。存在しないため、再作成する必要があります。これはコンポーネントのキャッシュに似ています。コンポーネントのビルド コストを削減することしかできません。コードの次の部分を見てください。

final Key key = newWidget.key;
if (key is GlobalKey) {
  final Element newChild = _retakeInactiveElement(key, newWidget);
  if (newChild != null) {
    assert(newChild._parent == null);
    assert(() {
      _debugCheckForCycles(newChild);
      return true;
    }());
    newChild._activateWithParent(this, newSlot);
    final Element updatedChild = updateChild(newChild, newWidget, newSlot);
    assert(newChild == updatedChild);
    return updatedChild;
  }
}
final Element newChild = newWidget.createElement();

ただし、この部分はコンポーネントをメモリにキャッシュし、不要なコンテンツがリサイクルされないため、非常にメモリを消費します。そのため、GlobalKey を使用する場合は十分に注意し、再利用性が高く、ビルドが複雑なコンポーネントに適用するようにしてください。仕事。

setState

図 3 の setState がトリガーされると、現在のコンポーネントは再構築操作を実行します。現在のコンポーネントのビルドにより、現在のコンポーネントの下にあるすべてのサブコンポーネントの再構築動作が発生するため、設計時には、ステートフルのステートレス性を最小限に抑えるようにしてください。これにより、不要なビルド ロジックが削減されます。これらは、前に説明したコンポーネント設計のポイントの一部でもあり、Flutter はウィジェットと要素を比較的高速に構築できますが、パフォーマンスの観点から、この部分での不必要な損失を削減するよう努めています。次に、特にステートフル コンポーネントの場合、Flutter でのビルドが頻繁にトリガーされるため、ビルド内のビジネス ロジックを減らすことに注意してください。

要約する

このクラスでは、Flutter の 3 ツリーの概念から 3 ツリーの対応まで、Flutter の自己レンダリングにおける 3 つのツリーの知識を中心に、3 ツリーの変換プロセスに焦点を当て、そのプロセスにおけるパフォーマンス最適化の重要なポイントをまとめます。注意点。

このレッスンを完了した後は、Flutter の 3 ツリーの概念をマスターし、3 ツリー変換プロセスを明確に理解する必要があります。変換プロセス中のパフォーマンス最適化の知識を学ぶことで、コーディング中に非常に優れたコードを開発できるようになります。プロセス、コーディングの習慣。

このレッスンのソース コードを表示するには、リンクをクリックしてください

おすすめ

転載: blog.csdn.net/g_z_q_/article/details/129719002