Open35

Flutter内部コードを探る

Flutterの実行ファイルを起動したとき何が起こるのか

Flutter for Windows のコードを読む

用語

  • {app}: Flutterアプリのルートディレクトリ
  • {flutter}: flutter/flutterのルートディレクトリ
  • {engine}: flutter/engineのルートディレクトリ

{app}/windows/runner/main.cppにmain関数であるwWinMain()が存在

  • FlutterWindowsView
    • WindowBindingHandler::SetView()にthisを入れる
      • これでWindowBindingHandlerから来たイベントをFlutterWindowsViewのメソッドで受け取れるようになる
  • DartProject
    • assets_path、icu_data_path、aot_library_pathを指定
    • Pathを保持しているだけのクラス
  • RendererBinding.drawFrame()
    • PipelineOwner.flushLayout()呼ぶ
      • 再計算の必要があるRenderObjectのレイアウトをすべて更新する
      • RenderObject.sizeに大きさ、RenderObject.parentData.offsetにオフセットを書き込む
      • _nodesNeedingLayoutにはrenderViewがいる
      • renderView._layoutWithoutResize()呼ぶ -> RenderObject._layoutWithoutResize()
        • RenderView.performLayout()呼ぶ
          • childのRenderObject.layout({画面サイズ}.tight)呼ぶ
        • RenderObject.markNeedsSemanticsUpdate()呼ぶ
        • RenderObject.markNeedsPaint()呼ぶ
    • PipelineOwner.flushCompositingBits()呼ぶ
      • RenderツリーのRenderObject.needsCompositingを更新
    • PipelineOwner.flushPaint()呼ぶ
      • RenderObject._layerHandle.layerにpaint
      • 描画結果はlayer.picture
      • _nodesNeedingPaintごとに処理する
      • renderView_nodesNeedingPaint内に存在
      • renderView._layerHandle.layer!.attachedはtrue
        • PaintingContext.repaintCompositedChild(renderView)呼ぶ
    • RenderView.compositeFrame()呼ぶ
      • builder = ui.SceneBuilder()でSceneBuilderを作成
      • scene = RenderView.layer.buildScene(builder)でsceneを作成
      • _window.render(scene)でsceneをengineに送る
    • PipelineOwner.flushSemantics()呼ぶ

コメントメモ:

  • Element.updateChild(): 子を新しい設定で更新するメソッド。このメソッドはWidgetシステムのコアである。

下の場合を考えよう。

runApp(Align(
  child: RichText(
    text: const TextSpan(
        children: [TextSpan(text: "Hello, "), TextSpan(text: "world!")]),
    textDirection: TextDirection.ltr,
  ),
));

ここまでで以下のツリーが作成されている(parent -> child)

  • RenderObjectToWidgetAdapter
    • Align
      • RichText
  • RenderObjectToWidgetElement
    • SingleChildRenderObjectElement
      • MultiChildRenderObjectElement
  • RenderView
    • RenderPositionedBox
      • RenderParagraph

続き

  • RenderView.performLayout()
    • RenderPositionedBox.layout({画面サイズ}.tight) -> RenderObject.layout
      • RenderObject._relayoutBoundaryを指定(自分)
      • RenderPositionedBox.sizedByParentはFalseのはず
      • RenderPositionedBox.performLayout()呼ぶ
        • child.layout({画面サイズ}.loosen, parentUsesSize: true)呼ぶ -> RenderObject.layout
          • RenderObject._relayoutBoundaryは親と同じやつ
          • visitChildren(_cleanChildRelayoutBoundary);も呼ぶんじゃないかなぁ
          • RenderBox.performResize()呼ぶ
            • RenderParagraph.computeDryLayout(constraints)呼ぶ
              • RenderParagraph._layoutChildren(constraints, dry: true)呼ぶ
                • childrenはないので空の配列が返る?
              • TextPainter.setPlaceholderDimensions([])呼ぶ
                • 空なのでなにもしない
              • RenderParagraph._layoutText()に幅の制約を与えて呼ぶ
                • TextPainter.layout()に幅の制約を与えて呼ぶ
          • RenderParagraph.performLayout()呼ぶ
            • RenderParagraph._layoutChildren(constraints)呼ぶ
            • RenderParagraph._layoutText()に幅の制約を与えて呼ぶ
            • _textPainter.sizeをsizeとする
            • オーバーフローしてたらその処理
          • RenderObject.markNeedsSemanticsUpdate()呼ぶ
          • RenderObject.markNeedsPaint()呼ぶ
        • shrinkWrapXはfalseなので縦横double.infinityを要求する→親の最大大きさになる
        • RenderAligningShiftedBox.alignChild()呼ぶ
          • RenderAligningShiftedBox._resolve()呼ぶ
            • alignmentとtextDirectionを解決する(Alignmentだとそのまま)
          • RenderPositionedBoxと子の左上の距離(offset)を計算してchild.parentData.offsetにセット
      • RenderObject.markNeedsSemanticsUpdate()呼ぶ
      • RenderObject.markNeedsPaint()呼ぶ
        • PipelineOwner.requestVisualUpdate()呼ぶ
          • SchedulerBinding.ensureVisualUpdate()呼ぶ
  • PaintingContext.repaintCompositedChild(renderView)
    • OffsetLayer? childLayer = renderView._layerHandle.layer == rootLayer
    • childLayer.removeAllChildren()呼ぶ
    • childContext = PaintingContext(childLayer, renderView.paintBounds)
      • paintBoundsはoffsetとsizeを合わせた矩形情報
    • renderView._paintWithContext(childContext, Offset.zero)呼ぶ
      • renderView.paint(childContext, Offset.zero)呼ぶ
        • childContext.paintChild(renderView.child, Offset.zero)呼ぶ
          • RenderPositionedBox._paintWithContext(childContext, Offset.zero)呼ぶ
            • RenderPositionedBox(=ShiftedBox).paint(childContext, Offset.zero)呼ぶ
              • childContext.paintChild(RenderPositionedBox.child, RenderPositionedBox.child.parentData.offset + Offset.zero)呼ぶ
                • RenderParagraph._paintWithContext(childContext, offset)呼ぶ
                  • RenderParagraph.paint(childContext, offset)呼ぶ
                    • RenderParagraph._layoutTextWithConstraints()呼ぶ
                      • RenderParagraph._layoutText()に幅の制約を与えて呼ぶ
                        • サイズ更新
                    • _textPainter.paint(childContext.canvas, offset)でcanvasに描画
                    • canvasを最初に使うときに_currentLayer=PictureLayer()を生成し、childLayerの子として追加
    • childContext.stopRecordingIfNeeded()呼ぶ
      • _currentLayer.pictureに描画結果を格納

ここまででLayerツリーは以下のようになっている

  • TransformLayer
    • dpiで変形する情報を持つ
    • PictureLayer
      • rootの大きさで文字がかかれたpictureを持つ
  • RenderView.isRepaintBoundary == true
  • RenderPositionedBox.isRepaintBoundary == false
  • RenderParagraph.isRepaintBoundary == false
  • RenderView.layer.buildScene(builder)
    • {engine}/SceneBuilderがスタックを使ってlayerツリーをengineに移す
    • updateSubtreeNeedsAddToScene()呼ぶ
      • 自身とサブツリーの`_needsAddToSceneを更新する
      • 自身ないしは子の_needsAddToSceneがtrueならtrue
      • TransformLayerPictureLayerは共にtrue
    • TransformLayer.addToScene(builder)呼ぶ
      • builder.pushTransform()でtransform情報をengineに積む
      • ContainerLayer.addChildrenToScene()呼ぶ
        • 子ごとにchild.addToScene(builder)呼ぶ
        • PictureLayer.addToScene()
          • builder.addPicture(picture)で描画をengineに追加
      • builder.pop()呼ぶ
    • return builder.build()
      • Sceneを作成
      • Sceneはlayerツリーの参照を持ってるだけだと思う

https://github.com/flutter/engine/blob/fa06eb819ff58affafcaa667cb5ed8b18f0f8dbe/lib/ui/compositing/scene_builder.cc
https://github.com/flutter/engine/blob/fa06eb819ff58affafcaa667cb5ed8b18f0f8dbe/lib/ui/compositing/scene.cc

ここまででSceneが持っているツリー

  • ContainerLayer (RootLayerとして追加された)
    • TransformLayer
      • PictureLayer
  • LayerTreeはあまり再利用しない
    • _needsPaintでマークされた場合子は再生成
      • isRepaintBoundary == trueかつ_needsPaint == falseなら、layerHandleの参照から再利用する
      • RenderTransformとかもpaint()内で再利用してるわね
  • SkPictureはRasterCacheにてPicture単位でキャッシュされる
    • 再利用できると判断されればキャッシュがDrawされる
    • RepaintBoundaryの説明にもそれっぽいことが書いてある

RepaintBoundary has the further side-effect of possibly hinting to the engine that it should further optimize animation performance if the render subtree behind the RepaintBoundary is sufficiently complex and is static while the surrounding tree changes frequently.

  • CacheはRasterCacheKey::Mapというstd::unordered_mapに保存される
  • キーはRasterCacheKeyで、id・type・matrixを持つ
  • 値はRasterCache::Entryで、このフレームで使われたか・アクセス回数・RasterCacheResultを持つ
  • RasterCacheResultはSkImageとSkRectを持つ
  • RasterCache::Prepare()を呼んだときにCacheを新規作成
    • PictureLayerだとPreroll内で呼んでいる
  • RenderObject._relayoutBoundary
    • https://juejin.cn/post/6981368741653119013
    • ツリー中で再レイアウトする部分を決定
    • _relayoutBoundary != this の場合親を探索
    • _relayoutBoundary == this の場合自身で再レイアウトの伝搬を停止
      • 親はrelayoutされない
  • sizedByParent == trueの場合
    • layout()内でperformResize()が呼ばれ、ここでサイズを決定する
    • performLayout()内ではサイズを変更してはいけない
  • sizedByParent == falseの場合
    • performResize()は呼ばれない
    • performLayout()内でサイズを適宜変更する
  • performResize()はRenderBoxではcomputeDryLayout()で更新される

HitTest詳細

  • hitTestではRenderObjectの左上からのローカルな座標が渡される
    • 子のhitTestを呼ぶときにOffset分を引いて流している
  • pointerの位置から見た場合、Offsetの適用はその分を引くこと
    • HitTestResultのStackには-offsetを積む
  • RenderTransformなどはそのままtransformをStackに積む
  • Stackに積まれた変換行列はそこまでの累積をHitTestEntryの追加時に渡す
    • 根を右に置いて左から掛け合わせることで、順に変換したのと同等の変換行列になっている
  • EntryのtransformはGestureBinding.dispatchEventで使われる
  • abstract class PointerEvent with Diagnosticable
  • mixin _PointerEventDescription on PointerEvent
  • abstract class _AbstractPointerEvent implements PointerEvent
  • abstract class _TransformedPointerEvent extends _AbstractPointerEvent with Diagnosticable, _PointerEventDescription
  • mixin _CopyPointerXxxEvent on PointerEvent
  • class PointerXxxEvent extends PointerEvent with _PointerEventDescription, _CopyPainterXxxEvent
  • class _TransformedPointerXxxEvent extends _TransformedPointerEvent with _CopyPointerXxxEvent implements PointerXxxEvent

Layerについて

  • RasterではPaint前にPrerollを呼ぶ
    • サイズ確定のため?
    • PictureLayer -> 持っているpictureからrectを計算
    • MutatorsStackはPlatformViewの変形用
    • TransformLayer -> context.cull_rectに逆行列を埋めて子のrectを計算
      • 子のrectを変換して自分のrectとする
    • read_backはよくわからないけどフラグでオン・オフできるからなくてもよいのかな
  • GLFWKeyCallback()
    • TextInputPlugin::KeyboardHook()
    • KeyEventHandler::KeyboardHook()
      • BasicMessageChannel::Send()
        • BinaryMessenger::Send()
          • BinaryMessengerはPluginRegistrarのBinaryMessengerImpl
            • FlutterDesktopMessengerSend()
              • FlutterDesktopMessengerSendWithReply()
                • FlutterEngineSendPlatformMessage()
                  • EmbedderEngine::SendPlatformMessage()
                    • PlatformView::DispatchPlatformMessage()
                      • Shell::OnPlatformViewDispatchPlatformMessage()
                        • (in UI TaskRunner) Engine::DispatchPlatformMessage()
                          • RuntimeController::DispatchPlatformMessage()
                            • PlatformConfiguration::DispatchPlatformMessage()
                              • (hooks.dart) _dispatchPlatformMessage()
                                • PlatformDispatcher._dispatchPlatformMessage()
                                  • ChannelBuffers.push()
                                    • _Channel.push()
                                      • "flutter/keyevent"のcallbackが設定されていれば呼び出し
                                        • 後述
                                      • 設定されていなければqueueに保存
                                        • これは設定されたときにdrain()で全て呼び出される
  • ServicesBinding.initInstances()
    • ServicesBinding._initKeyboard()
      • window.onKeyData = _keyEventManager.handleKeyData
        • PlatformDispatcher.setOnKeyData
          • ChannelBuffers.setListener()
            • _Channel.setListener()
              • "flutter/keyData"にcallbackを登録
      • SystemChannels.keyEvent.setMessageHandlerにKeyEventManager.handleRawKeyMessageを指定
        • _DefaultBinaryMessenger.setMessageHandler()
          • ui.channelBuffers.setListener()
            • "flutter/keyEvent"にcallbackを追加
  • つまりcallbackはKeyEventManager.handleRawKeyMessage()

needsAddToSceneの調査

  • trueになる時
    • Layerの生成時
    • markNeedsAddToScene()が呼ばれた時
    • updateSubtreeNeedsAddToScene()でalwaysNeedsAddToSceneがtrue
    • updateSubtreeNeddsAddToScene()で子のneedsAddToSceneがtrue
  • falseになる時
    • ContainerLayer.buildScene()が呼ばれた後
    • Layer._addToSceneWithRetainedRendering()が呼ばれた後
      • ContainerLayer.addChildrenToScene()から呼ばれる
        • これは各addToScene()から呼ばれる
      • どちらもaddToScene()が呼ばれた後である
  • markNeedsAddToScene()の呼ばれるタイミング
    • engineLayerが設定された時
    • layerのプロパティが変更された時
  • needsAddToSceneの使われるタイミング
    • Layer._addToSceneWithRetainedRendering()のみ
      • falseかつengineLayerがある場合addRetainedを呼び処理を省略
      • ContainerLayer.addChildrenToScene()からのみ呼ばれる

SceneBuilderのoldLayerの役割

  • pushXxxを呼ぶとき一緒にoldLayerを渡すことができる
  • oldLayerはインスタンスが使い回されるわけではない
    • 毎回新たにLayerが生成される
  • 新しいLayerはoldLayerからoriginal_layer_idを継承する
  • 同じidを持つかどうかはLayer.IsReplacing()で確認される
    • ContainerLayer::DiffChildren()で利用
      • ContainerLayer::Diff()で利用
        • ここで使い回せるかどうかを判定している?
  • addRetained()を使うとインスタンスを使い回すことができる
    • needsAddToSceneがfalseの場合
作成者以外のコメントは許可されていません