🛡️

FlutterのRaster Cacheを追ってみる

2022/07/09に公開1

Flutterの心臓部はWidgetです。Widgetの差分をいかに最低限必要な描画の差分へ変換していくか、というのがFlutterの基盤部分の役割の一つです。
この必要最低限の描画の差分を担保するための最適化がいくつか存在し、その中でも低レイヤーよりのRaster Cacheについてお話します。

描画の仕組み

Flutterは、FrameworkEngineの二つに大きく分かれています。
Flutterの描画は、Framework側で生成した描画内容を、Engine側で実際に画面に対して描画することで実現されています。
この描画内容をピクセル列に変換する処理を、Rasterizeと呼びます。

描画内容によっては高価な(=重たい)ものがあったり、複雑なUIの描画が必要なケースでは毎フレームRasterizeしていると遅延につながってしまいます。
そこで、Flutter内部ではヒューリスティックにRasterizeの結果をキャッシュする機構が存在し、 これがRaster Cacheと呼ばれます。

確認してみる

checkerboardRasterCacheImagesというフラグをONにすることで、実際どこがRaster Cacheされているのかを確認できます。

Flutter GalleryアプリでRaster Cacheの様子を確認

市松模様が表示されている部分は、Rasterizeの結果がキャッシュされており、その分描画が軽くなっています。
触ってみると分かりますが、初回描画時には市松模様がなくても数フレーム変化がないと市松模様が表示され、キャッシュされている様子が見えます。

キャッシュされないとどうなる?

例えばこのRaster Cacheがうまく効かないケースでは、下のIssueのようにパフォーマンスへ悪影響が出ます:
https://github.com/flutter/flutter/issues/24627#issuecomment-445402844

このIssueでは、Transform.rotateが異常に重たくなる理由が、Transform.rotateは他のTransformとは違いアンチエイリアスの結果が角度ごとに異なるため、Rasterizeの結果をキャッシュすることができず、結果として重たいShadowの描画が毎フレーム走ってしまうことが原因とコメントされています。

もちろん、逆にすべての描画結果をキャッシュすれば良い訳でもなく、Raster Cacheの生成はメモリ使用量の悪化にもつながります。

キャッシュ判定

パフォーマンスとメモリ使用量のトレードオフを考慮して、Flutter内部では描画回数が一定(3回)以上であることや描画内容のコストなどを考慮して、ヒューリスティックにキャッシュ判定を行っています。
最近ではRaster Cacheを行うかどうかのヒューリスティックアルゴリズムを、今までは描画コマンドが5個以上含まれているかどうかで行っていましたが、描画コマンド毎にコストのスコアを試算し判定するように変更されました(Flutter 3.0.0):
https://github.com/flutter/engine/pull/31417

before:
https://github.com/flutter/engine/blob/c0c31a182725dc361307c7216643b27afd728895/flow/raster_cache.cc#L88

after:
https://github.com/flutter/engine/blob/c0c31a182725dc361307c7216643b27afd728895/flow/raster_cache.cc#L115

これにより、パフォーマンスを劣化させることなく、メモリ使用量の削減に成功したそうです。

Using this as the raster cache admissions policy reduced memory usage without regressing performance in our benchmarks.
https://medium.com/flutter/whats-new-in-flutter-3-8c74a5bc32d0

このように、Flutter本体側でも日々改善が続いています。

アプリ開発者が意識できるところ

では、アプリ開発者はRaster Cacheのために何ができるでしょうか?

FlutterにはRepaintBoundaryという機能が存在し、これを使用すると他のUIと描画内容を分けることができます。
キャッシュ判定は描画内容の単位で行われるため、更新頻度の異なるUIの描画内容を適切に分けることによりRaster Cacheのキャッシュ判定を促すことができます。

例えば、Flutterのベンチマークを見ると、以下のようにRepaintBoundaryを使っています:

// https://github.com/flutter/flutter/blob/1704d4f5f9c2f53decd1d526a8a41dfa06d1e484/dev/benchmarks/macrobenchmarks/lib/src/color_filter_and_fade.dart#L63

final Widget fadeTransition = FadeTransition(
  opacity: _opacityAnimation,
  // This RepaintBoundary is necessary to not let the opacity change
  // invalidate the layer raster cache below. This is necessary with
  // or without the color filter.
  child: RepaintBoundary(
    child: column,
  ),
);

RepaintBoundaryの外はFadeTransitionによってopacityが頻繁に変更されますが、中身のコンテンツは描画内容が変わらないため、更新頻度に差があるUIを切り離しRaster Cacheを促すために使われています。

RepaintBoundaryはアプリ開発者が意識して挿入していく必要があります。

また、CustomPaintにはisComplex, willChangeというパフォーマンスヒントがあり、キャッシュ判定を促すことができます:
https://api.flutter.dev/flutter/widgets/CustomPaint/isComplex.html
https://api.flutter.dev/flutter/widgets/CustomPaint/willChange.html

RenderObjectでも同等のsetIsComplexHint, setWillChangeHintを利用できます:
https://api.flutter.dev/flutter/rendering/PaintingContext/setIsComplexHint.html
https://api.flutter.dev/flutter/rendering/PaintingContext/setWillChangeHint.html

willChangeは、キャッシュ判定をfalseに:
https://github.com/flutter/engine/blob/557655b7f552804da2e6f45d0c863737c6be8a82/flow/layers/display_list_raster_cache_item.cc#L38

isComplexは、キャッシュ判定を描画コストに関わらずtrueにできます:
https://github.com/flutter/engine/blob/557655b7f552804da2e6f45d0c863737c6be8a82/flow/layers/display_list_raster_cache_item.cc#L25

Discussion