免責事項(a.k.a. 言い訳)
自分なりに正確に記したつもりですが間違ってたらごめんなさい&教えてください
モチベーション
Flutterのドキュメントなんかを読んでいると以下の三つのツリーが断片的に出てきます.
- Widgetツリー
- Elementツリー
- Renderツリー
この記事ではそれぞれのツリーのルートがどうやって生成されるのかをソースコードから追跡してみようと思います.
それぞれのツリーの役割や更新方法などは以下を参照されることをお勧めします.
runApp
runApp関数は受け取ったルート・ウィジットを"膨らませます". この際WidgetsFlutterBindingが色々やってくれるようです.
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
カスケード構文を使ってattachRootWidgetやscheduleWarmUpFrameはWidgetBindingを呼び出しています. これらはWidgetsBindingミックスインに定義されています.
attachRootWidget
attachRootWidgetはrootWidgetをrenderViewElement以下に取り付けます. ややこしいのはcontainerであるrenderViewに取り付けられるのはElementツリーらしいと言うことです.
The given container is the RenderObject that the Element tree should be inserted into.
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner, renderViewElement);
}
RenderObjectToWidgetAdapter
RenderObjectToWidgetAdapterと言うウィジットが初期化されています.
また引数としてrenderViewを取ります. この変数はどこで初期化されたのか分かりませんが, 名前からレンダー・ツリーのルート・オブジェクトを指すようです. 実態解明はひとまず置いておいて, attachToRenderTreeを見てみましょう.
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element.assignOwner(owner);
});
owner.buildScope(element, () {
element.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
RenderObjectToWidgetElement
ここでようやくcreateElementと言う関数が出てきました. しかしElementをコンテナであるRenderObjectに付けると言うような処理はないです
createElementはRenderObjectToWidgetElementのインスタンスを生成します.
RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
This element class is the instantiation of a RenderObjectToWidgetAdapter widget. It can be used only as the root of an Element tree.
うーん. コンテナがルートだと書いてあります. またRenderObjectToWidgetAdapterのインスタンスとも書いています. 引数もRenderObjectToWidgetElementインスタンスになってるようですが・・・全然分かりません.
ドキュメントの少し下の行に"RenderObjectToWidgetAdapter"のためにとあります. 実際attachToRenderTreeの処理はそのようになっています.
In typical usage, it will be instantiated for a RenderObjectToWidgetAdapter whose container is the RenderView...
次はmountをみてみましょう. シグネチャを見ると何をしているのかよく分かります.
void mount (Element parent, dynamic newSlot)
Elementツリーを構築するメソッドのようです. parentがnullなことからRenderObjectToWidgetElementがルート要素であることが分かります. 実際以下のような継承関係があります.
重要なのは上から2番目のRenderObjectElementのmountメソッドです.
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this); // (*)
assert(() {
_debugUpdateRenderObjectOwner();
return true;
}());
assert(_slot == newSlot);
attachRenderObject(newSlot);
_dirty = false;
}
ここでwidgetは何を指すでしょう. createElementはRenderObjectToWidgetElementをインスタンス化しているだけでした. そこでRenderObjectToWidgetElementのコンストラクタを見るとwidgetが渡されています. これが継承を辿ってRenderObjectElementにセットされたわけです. つまりwidgetはRenderObjectToWidgetAdapterなのが分かります.
ではRenderObjectToWidgetAdapterはどんなウィジットなのでしょうか. 継承をたどるとRenderObjectWidgetと言うウィジットにたどり着きます.
RenderObjectToWidgetAdapterのcreateRenderObjectを見るとコンテナが返されています.
@override
RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;
これはRenderツリーのルートでした. つまりElementツリーのルートがマウントされるときにRenderツリーが生成されるわけです.
ここまでの流れ
最初はルート・ウィジットであるRenderObjectToWidgetAdapterが生成されます. これがWidgetツリーのルートだと思われます. その生成プロセスでcreateElementが呼び出されてElementツリーのルート(RenderObjectToWidgetElement)が生成され, 更にそこからmountメソッド経由でcreateRenderObjectでRenderツリーのルートであるrenderObjectが生成されると言うのがここまでの流れです.
renderViewインスタンスの生成場所
解決していない問題としてRenderViewインスタンスの生成場所です. これがcontainerの正体でした.
最初にrenderViewが出現するのはattachRootWidget内部です. RenderObjectToWidgetAdapterをインスタンス化するときにrenderViewが突然引数として現れるのでした. 正体を突き止めるためにWidgetsFlutterBindingクラスをもう一度見直してみましょう.
実はattachRootWidgetメソッドはWidgetsBindingミックスインに定義されています. 更にこのミックスインの継承元の一つにRendererBindingと言うのがあります.
RendererBindingミックスインにinitRenderViewメソッドがあります. いかにもなメソッドが出てきました.
void initRenderView() {
assert(renderView == null);
renderView = RenderView(configuration: createViewConfiguration(), window: window);
renderView.prepareInitialFrame();
}
WidgetBindingに現れるrenderViewはRenderBindingからもたらされるものでした
まとめ
Widgetツリー, Elementツリー, そしてRenderツリーのルートが作成されることが分かりました. それぞれのツリーとルートとなるクラスを整理しておきます.
ツリー | ルート |
---|---|
Widgetツリー | RenderObjectToWidgetAdapter |
Elementツリー | RenderObjectToWidgetElement |
Renderツリー | RenderView |
WidgetsFlutterBindingの謎
runAppを実行すると最初に実行されるBindingが何をやってるのか分からなかったです.
そもそも何と何を結びつけるのか?
This is the glue that binds the framework to the Flutter engine.
フレームワークとFlutterエンジンを結びつける接着剤のようなものと言うことでしょうか. 以下はFlutterシステムのブロック・ダイアグラムです.
ドキュメントの言葉通りなら一番上と真ん中のレイヤーを結びつけるのがバインディングと言うことのようです.
しかしWidgetsFlutterBindingはいろんなバインディングのミックスインとして定義されています.
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding { ... }
いくつかのバインディングはフレームワーク・レイヤーに同じ名前のものが存在します. またAPI Docsのライブラリに共通の名前が並んでいます.
例えばGestureBindingを見てみましょう.
mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget { ... }
HitTestable, HitTestDispatcher, HitTestTargetはgestureライブラリに定義されています.
これらを辿ってみてもFlutterエンジンと関係ありそうなコードは出てきません. それもそのはずでFlutterエンジンはフレームワークとは別のプロジェクトとして独立しているからです.
ネイティブ・コードはどこにあるのか? あるいはどうやって結合されるのか?
分からないのでStackoverflowに質問しました. わかる人がいたら教えてください
Where or when does the WidgetsFlutterBinding actually bind the flutter framework to the engine?