FlutterのPlatformView (Android)

2024年は8月くらいまではずっとFlutter書いてました。その中で、AndroidのViewをFlutterのUIに組み込むPlatform Viewを使ったのですが、これどうやって画面に描画されてるのだろう?という疑問がありました。

docs.flutter.dev

Platform Viewについて調べると、自分の前提知識が足りないせいで「なんか分かった気になってるだけな気がするんだよなぁ」という気持ちになってました。

というわけで、今回はFlutterのPlatformViewを使うと何が起きてるのかを調べてみた日記です。(Andoirdだけです、iOSには触れません)

前回の日記(AndroidでOpenGL ES - bati11 の 日記)の内容は、今回につながっています。

Platform Viewを使ってない場合

まずは、Platform View関係なくFlutterアプリがどういう仕組みになっているか。

この日記で特に書くことはなく、Flutterの公式ページを読む。

docs.flutter.dev

以下のような図がある。

レイアウトして描画するのはEngineの仕事。上記の公式ページの「Rendering and layout」に書いてある。詳しくは追加で以下のスライドを読む、講演を聴く。とても勉強になりました。

プラットフォーム固有の描画先を用意するのはEmbbederの仕事。上記の公式ページの「Platform embedding」に書いてある。

Androidの場合について以下のように書いてある。FlutterViewが重要。

On Android, Flutter is, by default, loaded into the embedder as an Activity. The view is controlled by a FlutterView, which renders Flutter content either as a view or a texture, depending on the composition and z-ordering requirements of the Flutter content.

Flutterのバージョン3.24.5時点でのFlutterViewの実装では、FlutterViewFrameLayoutを継承していて、FlutterSurfaceViewというSurfaceViewを継承したviewをFlutterView自身にaddView()している。

FlutterのWidgetツリー、というよりRenderObjectツリーは、Engineによってレイアウトされ、FlutterSurfaceViewSurfaceに対して描画されるのだろう。

Platform Viewを使ってる場合

WidgetツリーにAndroidViewがある場合、FlutterのWidgetとは別世界の、AndroidのViewツリーについてもアプリのUIとして描画されるわけだけど、これはどういう仕組みになっているのかな。

flutter/docs/platforms/android/Android-Platform-Views.md at 3.24.5 · flutter/flutter · GitHub

AndroidにおいてPlatform Viewには3つのモードがある。

  • VD (Virtual Display)
  • HC (Hybrid Composition)
  • TLHC (Texture Layer Hybrid Composition)

実際のソースコードを見ていくことにする。Flutterのバージョンは3.24.5です。

Flutterアプリに組み込みたいAndroidのViewは、io.flutter.plugin.platform.PlatformViewというインタフェースを実装する必要がある。FlutterのWidgetツリーにはAndroidViewを書く(TLHCの場合)。

ここら辺の記述の仕方は公式ページの通り実装すればいい。

AndroidViewの実装を読むとMethodChannelJavaのコードが実行されることが分かる。ここで利用するMethodChannelSystemChannelsというクラスがあって SystemChannels.platform_views で用意されてる。Platform ViewだけでなくFlutterフレームワークが他の用途でJavaとやりとりするためのMethodChannelがいくつか用意されてるのが分かる。今回は 'flutter/platform_views' に注目。

  static const MethodChannel platform_views = MethodChannel(
    'flutter/platform_views',
  );

https://github.com/flutter/flutter/blob/3.24.5/packages/flutter/lib/src/services/system_channels.dart#L381-L383

_AndroidViewControllerInternals#sendCreateMessage()MethodChannelcreateをコールしている。

return SystemChannels.platform_views.invokeMethod<dynamic>('create', args);

https://github.com/flutter/flutter/blob/3.24.5/packages/flutter/lib/src/services/platform_views.dart#L1240

ここから先はJavaで書かれたコードの話。

MethodChannelのhandlerを登録しているのはPlatformViewsChannel.javaのここ↓

public PlatformViewsChannel(@NonNull DartExecutor dartExecutor) {
  channel =
      new MethodChannel(dartExecutor, "flutter/platform_views", StandardMethodCodec.INSTANCE);
  channel.setMethodCallHandler(parsingHandler);
}

https://github.com/flutter/engine/blob/3.24.5/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java#L267-L271

そして、上記の parsingHandlercreate コールバックが実行され、 PlatformViewsHandler#createForTextureLayer() が呼ばれる。このメソッドが重要そう。

PlatformViewsHandler#createForTextureLayer() に分岐がある。ドキュメント等を読んでいる感じ、AndroidのPlatform Viewは VD→TC→TLHC と改善されてきている模様。ソースコードでもTLHCが使えるなら使う、使えない場合はVDもしくはHCにフォールバックするようになってる。

// Fall back to Hybrid Composition or Virtual Display when necessary, depending on which
// fallback mode is requested.
if (!supportsTextureLayerMode) {
  if (request.displayMode
      == PlatformViewsChannel.PlatformViewCreationRequest.RequestedDisplayMode
          .TEXTURE_WITH_HYBRID_FALLBACK) {
    configureForHybridComposition(platformView, request);
    return PlatformViewsChannel.PlatformViewsHandler.NON_TEXTURE_FALLBACK;
  } else if (!usesSoftwareRendering) { // Virtual Display doesn't support software mode.
    return configureForVirtualDisplay(platformView, request);
  }
  // TODO(stuartmorgan): Consider throwing a specific exception here as a breaking change.
  // For now, preserve the 3.0 behavior of falling through to Texture Layer mode even
  // though it won't work correctly.
}
return configureForTextureLayerComposition(platformView, request);

https://github.com/flutter/engine/blob/3.24.5/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L217-L232

3つのモードがあると言いつつ、公式のPlatformViewのページを見ると、HCとTLHCの説明しか書いてない。VDの弱点はTLHCで補うことができるからVDはもうほとんど場合で不要、ということかな。

推奨されるTLHCを見ていく。

TLHC (Texture Layer Hybrid Composition)

flutter/docs/platforms/android/Texture-Layer-Hybrid-Composition.md at 3.24.5 · flutter/flutter · GitHub

さっきのフォールバックの分岐処理の部分で、フォールバックせずに、TLHCが選ばれたら configureForTextureLayerComposition()を呼ぶ。そこから先はざっと以下のような流れ。

  1. まず、PlatformViewRenderTargetオブジェクトをmakePlatformViewRenderTarget()で生成する
  2. 次に、PlatformViewWrapperを生成する。このとき、コンストラクタの引数に上で生成したPlatformViewRenderTargetを指定する
    • PlatformViewWrapperAndroidFrameLayoutのサブクラスである
  3. PlatformViewWrapperのサイズや配置場所を決める
  4. Platform Viewとして埋め込みたい自分で作ったAndroidのView(PlatformViewを継承して実装する)をPlatformViewWrapperの子ツリーにする
    • final View embeddedView = platformView.getView();
      ...
      viewWrapper.addView(embeddedView);
      
  5. FlutterViewPlatformViewWapperを追加する

ここまででAndroidアプリのViewツリーは以下のように、FlutterViewの子にPlatformViewWrapperが、PlatformViewWrapperの子に自分で作ったAndroidのView(Flutterアプリに組み込みたいAndroidのViewツリー)がぶら下がる。

さて、FlutterとしてのWidgetツリーの方ではAndroidViewの部分に Texture が差し込まれる。

前回の記事を思い返すと、AndroidSurfaceにはプロデューサーとコンシューマーがいると書いた。Java側で生成したPlatformViewRenderTargetが保持するSurfaceがプロデューサーとなりバッファに描画をして、Flutter側のTextureがコンシューマーとなりバッファを読み取る、という形で連携する。

AndroidアプリのViewツリーの描画先はアプリのSurfaceViewになるはずだが、どうやって描画先をPlatformViewRenderTargetが保持するSurfaceにしているのか。

PlatformViewWrapperの実装を見ると分かる。Androidアプリでは、Viewツリーのルートから順に各Viewのdraw(canvas)を辿り、各Viewが引数のcanvas|に対して自分自身を描画することで、結果的にアプリ全体を描画する。PlatformViewWrapperdraw(canvas)をoverrideして引数のcanvasではなく、自分自身が保持するSurfaceからcanvas`を取得して、そこに描画するように差し替える。

final Canvas targetCanvas = targetSurface.lockHardwareCanvas();
...
try {
  // Fill the render target with transparent pixels. This is needed for platform views that
  // expect a transparent background.
  targetCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
  // Override the canvas that this subtree of views will use to draw.
  super.draw(targetCanvas);
} finally {
  renderTarget.scheduleFrame();
  targetSurface.unlockCanvasAndPost(targetCanvas);
}

https://github.com/flutter/engine/blob/3.24.5/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java#L176-L185

このようにPlatformViewWrapperAndroidアプリとしてのcanvasではなく、自分自身が保持するcanvas、つまりSurfaceに描画する。このSurfaceのコンシューマーがFlutterのTexutureということ。

Androidアプリとしては、PlatformViewWrapper部分のViewツリーについて、アプリのSurfaceに描画はしないがViewツリーとしては保持する、という状態になる。AndroidアプリはViewツリーとしては保持しているため、VDにおけるテキスト入力やアクセシビリティの問題を回避できる。なるほどなー。

VD (Virtual Display)

flutter/docs/platforms/android/Virtual-Display.md at 3.24.5 · flutter/flutter · GitHub

TLHCが使えない場合のフォールバックとしてVDを使う場合もPlatformViewRenderTargetオブジェクトをmakePlatformViewRenderTarget()で生成するのは同じ。

PlatformViewRenderTargetが保持するSurfaceを、AndroidフレームワークDisplayManager#createVirtualDisplay() の引数に指定して android.hardware.display.VirtualDisplay を手にいれる。PlatformViewVirtualDisplayに対して描画すると、SurfaceのコンシューマーであるFlutter WidgetツリーのTextureで利用される。

描画はTLHCと同じような流れだけど、AndroidアプリとしてのViewツリーにPlatformViewが含まれないことが大きな違い。これがVDの問題の原因となっている。

HC (Hybrid Composition)

flutter/docs/platforms/Hybrid-Composition.md at 3.24.5 · flutter/flutter · GitHub

HCはスタミナ切れでコード読んでない。ドキュメント読んでる感じだとTLHCやVDと全然違うみたい。公式PlatformViewのページの文章にはこう書いてある。

Platform Views are rendered as they are normally. Flutter content is rendered into a texture. SurfaceFlinger composes the Flutter content and the platform views.

Android platform-views | Flutter

SurfaceFlingerによって合成されると書いてある。Androidアプリとしては複数のSurfaceを持っている状態でそれぞれ描画されて、最終的にAndroidシステムサービスのSurfaceFlingerによって合成される。

HC個別のmarkdownより、Platform全般のmarkdownの方が説明がわかりやすい。

flutter/docs/platforms/android/Android-Platform-Views.md at 3.24.5 · flutter/flutter · GitHub

The Flutter widget tree is divided into two different Android Views, one below the platform view and one above it.

FlutterのWidgetが描画されるSurfacePlatformViewの下と上に分割されると。これらがSurfaceFlingerで合成されるのだろう。PlatformViewなしの場合とだいぶ違うし、パフォーマンスへの影響が大きい可能性があると。

以下のスライド見るとイメージしやすいです。

Androidで不安定なPlatform Viewsとの闘い - Speaker Deck

おしまい

最初よりはだいぶ分かった気がするぞー。