Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
RenderObjectより下の世界のFlutter
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Fastriver
November 17, 2022
430
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
RenderObjectより下の世界のFlutter
FlutterKaigi 2022(
https://flutterkaigi.jp/2022/
)で発表した内容になります。
Fastriver
November 17, 2022
More Decks by Fastriver
See All by Fastriver
Flutter WebでもJamstackしたい
fastriver
1
2.4k
Featured
See All Featured
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
The State of eCommerce SEO: How to Win in Today's Products SERPs - #SEOweek
aleyda
2
11k
Conquering PDFs: document understanding beyond plain text
inesmontani
PRO
4
2.9k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
Embracing the Ebb and Flow
colly
88
5.1k
ラッコキーワード サービス紹介資料
rakko
1
3.9M
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
508
140k
Paper Plane
katiecoart
PRO
2
52k
[SF Ruby Conf 2025] Rails X
palkan
2
1.2k
Optimizing for Happiness
mojombo
378
71k
HTML-Aware ERB: The Path to Reactive Rendering @ RubyCon 2026, Rimini, Italy
marcoroth
2
330
Everyday Curiosity
cassininazir
0
250
Transcript
@fastriver_org RenderObjectより 下の世界のFlutter @fastriver_org
@fastriver_org 自己紹介 - fastriver (@fastriver_org) - 慶應義塾大学理工学部 - Flutterでミニゲームを制作 →
fastriver.dev 「Flatten me!」 Flutter Puzzle Hack提出作品 「蜿」 三田祭2020出展 「再実装Flutter (1)(2)」 技術書典13にて頒布 Zennでも販売中
@fastriver_org セッションの方針 - FlutterのRenderObject以下のコードを読み解き、主に描画に関する下回り部分が どのように作られているのかを考える - Flutterでアプリを作るための方法論などはあまり気にしない スライド中のソースコードについて - 視認性から一部を省略している
- 特にEngineは変更が速いので最新のコードとは異なる可能性がある
@fastriver_org Flutterによる画面の構築 void main() { runApp(ColoredBox( color: Colors.green, child: Center(
child: Column( mainAxisSize: MainAxisSize.min, children: [ RichText( text: const TextSpan(text: "Hello,"), textDirection: TextDirection.ltr, ), RichText( text: const TextSpan( text: "FlutterKaigi!", style: TextStyle(fontSize: 30)), textDirection: TextDirection.ltr, ), ], ), ), )); } Widgetを組み合わせて作る
@fastriver_org Flutterによる画面の構築 void main() { runApp(ColoredBox( color: Colors.green, child: Center(
child: Column( mainAxisSize: MainAxisSize.min, children: [ RichText( text: const TextSpan(text: "Hello,"), textDirection: TextDirection.ltr, ), RichText( text: const TextSpan( text: "FlutterKaigi!", style: TextStyle(fontSize: 30)), textDirection: TextDirection.ltr, ), ], ), ), )); } Widgetを組み合わせて作る ?
@fastriver_org Flutterの全体像 - Framework/Engine/Embedderの 3層からなる - Framework内でWidgetを並べ、 Engineで画像にして表示 - 画面への書き込みにはSkiaを利用
- 将来的には変更されるかも https://docs.flutter.dev/resources/architectural-overview https://skia.org/
@fastriver_org Widgetから画面までの流れの概要 1. Widgetツリーの構築 2. Widgetツリー → Renderツリー 3. Renderツリー
→ Layerツリー 4. Layerツリーのラスタライズ 5. 表示 Widget ツリー Render ツリー Layer ツリー 表示 開発者 構築 描画パイプライン Rasterize Layout Paint Engine Framework
@fastriver_org Widgetツリー → Renderツリー ColoredBox color=green Center Column RichText RichText
size=30 RenderObjectTo WidgetAdapter SingleChildElement MultiChildElement MultiChild Element MultiChild Element RenderObjectTo WidgetElement SingleChildElement _RenderColoredBox color=green RenderPositionedBox RenderFlex Render Paragraph Render Paragraph size=30 RenderView Widgetツリー Elementツリー Renderツリー
@fastriver_org Renderツリーとは - 描画面では3つのツリーの中で主役 - WidgetツリーはConfigurationの提供 - ElementツリーはLifecycleの管理 - RenderObjectで構成される
- レイアウトや描画計算などを担当 - 描画パイプラインで行われる - RenderツリーはWidgetツリーに対して少し小さい - RenderObjectWidgetを継承するWidgetはRenderObjectを生成 - InheritedWidgetやStatefulWidgetなどは生成しない https://youtu.be/zmbmrw07qBc より
@fastriver_org 描画パイプライン詳細 • 描画パイプライン(一般) • Flutterの描画パイプライン ◦ 描画パイプラインが呼ばれるタイミング ◦ レイアウト
◦ ペイント ◦ Engineへの送信 ◦ ラスタライズ ◦ 画面への描画
@fastriver_org 描画パイプライン詳細 • 描画パイプライン(一般) • Flutterの描画パイプライン ◦ 描画パイプラインが呼ばれるタイミング ◦ レイアウト
◦ ペイント ◦ Engineへの送信 ◦ ラスタライズ ◦ 画面への描画
@fastriver_org 描画パイプライン(一般) ツリー 構築 レイアウト ラスタライズ ペイント 表示 コードをパース して木構造にする
各ノードの大きさと位置 を決定する レイヤーに分けて canvasに命令を 書き込む 合成してひとつの画 像にする GPUに送信して 画面に表示
@fastriver_org 描画パイプライン詳細 • 描画パイプライン(一般) • Flutterの描画パイプライン ◦ 描画パイプラインが呼ばれるタイミング ◦ レイアウト
◦ ペイント ◦ Engineへの送信 ◦ ラスタライズ ◦ 画面への描画
@fastriver_org Flutterでの具体的な流れ 1. Widgetツリーを書く 2. Renderツリーの構築 3. レイアウト 4. ペイント
5. Layerツリーの構築 6. ラスタライズ 7. 画面への書き出し
@fastriver_org Flutter Frameworkでの描画パイプライン - PipelineOwnerが担当 - RendererBinding.drawFrame()で実行 void drawFrame() {
assert(renderView != null); pipelineOwner.flushLayout(); pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint(); if (sendFramesToEngine) { renderView.compositeFrame(); pipelineOwner.flushSemantics(); _firstFrameSent = true; } }
@fastriver_org 描画パイプライン詳細 • 描画パイプライン(一般) • Flutterの描画パイプライン ◦ 描画パイプラインが呼ばれるタイミング ◦ レイアウト
◦ ペイント ◦ Engineへの送信 ◦ ラスタライズ ◦ 画面への描画
@fastriver_org runApp()から描画パイプラインまで runApp() …→WidgetsBinding.attachRootWidget() →SchedulerBinding.ensureVisualUpdate() …→window.scheduleFrame() …→Engine::ScheduleFrame() →Animator::RequestFrame() →Animator::AwaitVSync() VSyncを待つ
void Animator::AwaitVSync() { waiter_->AsyncWaitForVsync( [self = weak_factory_.GetWeakPtr()]( std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder) { if (self) { if (self->CanReuseLastLayerTree()) { self->DrawLastLayerTree(std::move(frame_timings_recorder)); } else { self->BeginFrame(std::move(frame_timings_recorder)); } } }); }
@fastriver_org runApp()から描画パイプラインまで Animator::AwaitVSync()でVSyncを待つ →Animator::BeginFrame() …→Engine::BeginFrame() …→PlatformConfiguration::BeginFrame() …→SchedulerBinding.handleDrawFrame() …→RendererBinding.drawFrame() 描画パイプラインを実行 Framework
Engine 画面を 書き換えたい schedule() VSync beginFrame() drawFrame() 描画パイプライン 表示 ensure VisualUpdate()
@fastriver_org setState()から描画パイプラインまで State.setState() →Element.markNeedsBuild() →BuildOwner.scheduleBuildFor() →BuildOwner.onBuildScheduled() →WidgetsBinding._handleBuildScheduled() →SchedulerBinding.ensureVisualUpdate() …VSyncを待つ... …→RendererBinding.drawFrame()
描画パイプラインを実行 Framework Engine 画面を 書き換えたい schedule() VSync beginFrame() drawFrame() 描画パイプライン 表示 ensure VisualUpdate()
@fastriver_org 描画パイプライン詳細 • 描画パイプライン(一般) • Flutterの描画パイプライン ◦ 描画パイプラインが呼ばれるタイミング ◦ レイアウト
◦ ペイント ◦ Engineへの送信 ◦ ラスタライズ ◦ 画面への描画
@fastriver_org レイアウト - 各ノードのサイズ調整と位置調整を行う - 一度のツリー走査のみで全てのレイアウトを 決定 - 高速なレイアウトを実現 -
各ノードを矩形として扱う - 親は子に制約を渡し、子は制約内で 自身のサイズを決める - 親は自身に対する子の相対的な位置(offset)を 決める 制約をつけて サイズ要求 サイズを 返す 子のオフセット を計算
@fastriver_org PipelineOwner.flushLayout() - 登録されたノードの レイアウト処理を順に呼び出す - 起動時にRenderViewを登録 - Renderツリーの根ノード RenderView._layoutWithoutResize()
→RenderView.performLayout() void flushLayout() { try { while (_nodesNeedingLayout.isNotEmpty) { final List<RenderObject> dirtyNodes = _nodesNeedingLayout; _nodesNeedingLayout = <RenderObject>[]; dirtyNodes.sort((RenderObject a, RenderObject b) => a.depth - b.depth); for (int i = 0; i < dirtyNodes.length; i++) { final RenderObject node = dirtyNodes[i]; if (node._needsLayout && node.owner == this) { node._layoutWithoutResize(); } } } } }
@fastriver_org ツリー全体のレイアウト - RenderObject.performLayout()内で各個のレイアウトを決定 - 内部で子のlayout()を制約付きで呼び出し、ツリーの先端まで実行 - 自身のsizeと必要なら子のoffsetを決める - 子のoffsetは子のRenderObject.parentDataに格納
_layoutWithoutR esize() layout() performLayout() layout() flushLayout() performLayout() size parentData 制約 制約 親 親 子 子
@fastriver_org サイズの決定とレイアウト制約 - 子へのサイズ制約としてBoxConstraintsを渡 す - BoxConstraintsが持つのは 縦横それぞれの最大最小サイズの情報 - 子は制約内で大きさを決定
- 結果はsizeプロパティに格納 - 制約が解決できなければエラー max min
@fastriver_org 子のオフセットの決定 - 自身のサイズと子のサイズの決定後、それらを使って子の位置を計算 - 親の左上の位置からのずれで表現 - 結果は子のRenderObject.parentDataに格納 Centerの場合 Columnの場合
@fastriver_org 例: RenderPositionedBox PositionedやAlignのRenderObject 1. 子のlayout()を呼ぶ 2. 自身のサイズを子のサイズと 自身への制約から決定 3.
子のoffsetをalignmentと自身の サイズ、子のサイズから決定 void performLayout() { if (child != null) { child!.layout(constraints.loosen(), parentUsesSize: true); size = constraints.constrain(child!.size); final BoxParentData childParentData = child!.parentData! as BoxParentData; childParentData.offset = alignment!.alongOffset(size - child!.size as Offset); } else { ... } }
@fastriver_org 描画パイプライン詳細 • 描画パイプライン(一般) • Flutterの描画パイプライン ◦ 描画パイプラインが呼ばれるタイミング ◦ レイアウト
◦ ペイント ◦ Engineへの送信 ◦ ラスタライズ ◦ 画面への描画
@fastriver_org ペイント - 各ノードの情報を描画命令に変換 - 射影変換やフィルタが挟まる場合などには レイヤを追加しツリーを構成する - Layerツリー -
Pictureオブジェクトへ描画命令を順に 書き込み、Engineに渡す - この時点ではまだ画像 (ビット列)に変換しない Picture: 1. (0,0,304,441)に緑色の矩形を描画 2. (134,191)に”Hello,”とテキスト描画 3. (78,210)にsize=30で”FlutterKaigi!”と テキスト描画
@fastriver_org PipelineOwner.flushPaint() - 登録されたノードを順に処理 - PaintingContext.repaintComposited Child()の引数に渡す - 起動時にRenderViewを登録 -
Renderツリーの根ノード PaintingContextがLayerツリーの構築を担 当 void flushPaint() { try { final List<RenderObject> dirtyNodes = _nodesNeedingPaint; _nodesNeedingPaint = <RenderObject>[]; for (final RenderObject node in dirtyNodes..sort( (RenderObject a, RenderObject b) => b.depth - a.depth)) { PaintingContext.repaintCompositedChild(node); } } }
@fastriver_org PaintingContextによるLayerツリー構築 - レイアウトと異なりPaintingContext主体でツリーを走査 - PaintingContextは引数で渡される - RenderObject.paint()内でcanvasに命令を書き込む - PaintingContext.paintChild()に子を渡して続ける
PaintingContext.paintChild() _paintWithContext() paint() _paintWithContext() PaintingContext .repaintCompositedChild() 親 子 親 子
@fastriver_org 構築手順 1. 親レイヤを渡してPaintingContextを作成 ◦ PaintingContextの役割は子レイヤを作ること ◦ 最初のPaintingContextはrootLayerを親とする 2. 子を順に走査し、RenderObject.paint()内でcanvasに命令を書き込む
3. 全ての子孫ノードの走査が終わった場合 ◦ canvasを閉じてPictureLayerを作り、親レイヤの子に追加する 4. フィルタなどによりレイヤを追加する場合 ◦ 3と同様にPictureLayerを追加する ◦ その後新レイヤを子に追加し、新レイヤを親として PaintingContextを作り繰り返す
@fastriver_org 構築手順(単一のcanvas) TransformLayer PictureLayer root TransformLayer(root) PaintingContext canvas Renderツリー Layerツリー
@fastriver_org 構築手順(複数のcanvas) TransformLayer PictureLayer ClipRect root TransformLayer(root) PaintingContext canvas Renderツリー
Layerツリー ClipRectLayer PaintingContext canvas ClipRectLayer PictureLayer
@fastriver_org 例: _RenderColoredBox Container内部などで使われているColoredBoxのRenderObject 矩形を指定の色で塗る - sizeとoffsetを使い自身の専有部分を 矩形で塗りつぶし - 子を持つ場合は
PaintingContext.paintChild()に子と 現在のoffsetを渡し、子のペイントを 行う void paint(PaintingContext context, Offset offset) { if (size > Size.zero) { context.canvas.drawRect( offset & size, Paint()..color = color); } if (child != null) { context.paintChild(child!, offset); } }
@fastriver_org 例: RenderClipRect ClipRectのRenderObject - Layerを挿入するために PaintingContext.pushClipRect()を呼ぶ - 内部ではpushLayer()が呼ばれる -
現在のcanvasを止めてレイヤを子に追加 - 新しいcontextを作って続きのpaintを実行 void paint(PaintingContext context, Offset offset) { if (child != null) { if (clipBehavior != Clip.none) { _updateClip(); layer = context.pushClipRect( needsCompositing, offset, _clip!, super.paint, clipBehavior: clipBehavior, oldLayer: layer as ClipRectLayer?, ); } } } void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect? childPaintBounds }) { stopRecordingIfNeeded(); appendLayer(childLayer); final PaintingContext childContext = createChildContext(childLayer, childPaintBounds); painter(childContext, offset); childContext.stopRecordingIfNeeded(); }
@fastriver_org 描画パイプライン詳細 • 描画パイプライン(一般) • Flutterの描画パイプライン ◦ 描画パイプラインが呼ばれるタイミング ◦ レイアウト
◦ ペイント ◦ Engineへの送信 ◦ ラスタライズ ◦ 画面への描画
@fastriver_org LayerツリーをEngineへ送信 - RenderView.compositeFrame() - DartのLayerからC++のLayerをSceneBuilderを使って複製 - C++のLayerツリーの参照を持つSceneオブジェクトを作成 void compositeFrame()
{ try { final ui.SceneBuilder builder = ui.SceneBuilder(); final ui.Scene scene = layer!.buildScene(builder); _window.render(scene); scene.dispose(); } }
@fastriver_org SceneBuilderによるツリーの複製 TransformLayer PictureLayer ClipRectLayer PictureLayer TransformLayer DisplayListLayer ClipRectLayer DisplayListLayer
ContainerLayer Framework(Dart) Engine(C++)
@fastriver_org 描画パイプライン詳細 • 描画パイプライン(一般) • Flutterの描画パイプライン ◦ 描画パイプラインが呼ばれるタイミング ◦ レイアウト
◦ ペイント ◦ Engineへの送信 ◦ ラスタライズ ◦ 画面への描画
@fastriver_org ラスタライズ RenderView.compositeFrame()内で_window.render()が呼ばれている →FlutterView.render() …→Engine::Render() …→Rasterizer::DrawToSurface() …→ScopedFrame::Raster() - Preroll()とPaint()が順に呼ばれる -
Layerツリーを2回走査する RasterStatus CompositorContext::ScopedFrame::Raster( flutter::LayerTree& layer_tree) { bool root_needs_readback = layer_tree.Preroll(*this, ignore_raster_cache); //... layer_tree.Paint(*this, ignore_raster_cache); //... return RasterStatus::kSuccess; }
@fastriver_org Preroll() - 描画の準備をするためのパス - パス内で行うこと: - 描画範囲の計算(paintBounds) - PlatformViewの変形用の
射影変換行列の計算 - canvasのキャッシュ など void DisplayListLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { DisplayList* disp_list = display_list(); SkMatrix child_matrix = matrix; if (context->raster_cache) { child_matrix = RasterCacheUtil::GetIntegralTransCTM(child_matrix); } AutoCache cache = AutoCache(display_list_raster_cache_item_.get(), context, child_matrix); if (disp_list->can_apply_group_opacity()) { context->subtree_can_inherit_opacity = true; } set_paint_bounds(bounds_); }
@fastriver_org Paint() DisplayListLayerの持つ描画情報を画面用のcanvasに書き込むパス void DisplayListLayer::Paint(PaintContext& context) const { SkAutoCanvasRestore save(context.leaf_nodes_canvas,
true); context.leaf_nodes_canvas->translate(offset_.x(), offset_.y()); if (context.raster_cache) { context.leaf_nodes_canvas->setMatrix(RasterCacheUtil::GetIntegralTransCTM( context.leaf_nodes_canvas->getTotalMatrix())); } //... if (context.leaf_nodes_builder) { //... } else { display_list()->RenderTo(context.leaf_nodes_canvas, context.inherited_opacity); } }
@fastriver_org Layerによるフィルタの適用 ContainerLayer DisplayListLayer TransformLayer DisplayListLayer Hello Flutter 中央で反転 Hello
Flutter void TransformLayer::Paint(PaintContext& context) const { SkAutoCanvasRestore save(context.internal_nodes_canvas, true); context.internal_nodes_canvas->concat(transform_); PaintChildren(context); }
@fastriver_org まとめ - Flutterは、WidgetツリーからRenderツリーを構成し、それを入力として 描画パイプラインを動かすことで画面の表示を行う - 描画パイプラインはVSyncに合わせて実行される - Framework側でレイアウトとペイント、Engine側でラスタライズ Widget
ツリー Render ツリー Layer ツリー 表示 開発者 構築 描画パイプライン Rasterize Layout Paint Engine Framework
@fastriver_org 参考 - GitHub - flutter/flutter - GitHub - flutter/engine
- Flutter architectural overview - Rendering on screen of fluent rendering - 前端知识 - Exploration of the Flutter Rendering Mechanism from Architecture to Source Code - Alibaba Cloud Community - Flutter Engineについて解説 - C++のリファレンスにして勉強してみよう - Qiita - Flutterレンダリングパイプライン入門 | CyberAgent Developers Blog - Flutter の描画の仕組みを理解する