2024年は8月くらいまではずっとFlutter書いてました。その中で、AndroidのViewをFlutterのUIに組み込むPlatform Viewを使ったのですが、これどうやって画面に描画されてるのだろう?という疑問がありました。
Platform Viewについて調べると、自分の前提知識が足りないせいで「なんか分かった気になってるだけな気がするんだよなぁ」という気持ちになってました。
というわけで、今回はFlutterのPlatformViewを使うと何が起きてるのかを調べてみた日記です。(Andoirdだけです、iOSには触れません)
前回の日記(AndroidでOpenGL ES - bati11 の 日記)の内容は、今回につながっています。
Platform Viewを使ってない場合
まずは、Platform View関係なくFlutterアプリがどういう仕組みになっているか。
この日記で特に書くことはなく、Flutterの公式ページを読む。
以下のような図がある。
レイアウトして描画するのは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
の実装では、FlutterView
はFrameLayout
を継承していて、FlutterSurfaceView
というSurfaceView
を継承したviewをFlutterView自身にaddView()
している。
FlutterのWidgetツリー、というよりRenderObjectツリーは、Engineによってレイアウトされ、FlutterSurfaceView
のSurface
に対して描画されるのだろう。
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
の実装を読むとMethodChannel
でJavaのコードが実行されることが分かる。ここで利用するMethodChannel
はSystemChannelsというクラスがあって SystemChannels.platform_views
で用意されてる。Platform ViewだけでなくFlutterフレームワークが他の用途でJavaとやりとりするためのMethodChannel
がいくつか用意されてるのが分かる。今回は 'flutter/platform_views'
に注目。
static const MethodChannel platform_views = MethodChannel( 'flutter/platform_views', );
_AndroidViewControllerInternals#sendCreateMessage()
で MethodChannel
でcreate
をコールしている。
return SystemChannels.platform_views.invokeMethod<dynamic>('create', args);
ここから先はJavaで書かれたコードの話。
MethodChannel
のhandlerを登録しているのはPlatformViewsChannel.javaのここ↓
public PlatformViewsChannel(@NonNull DartExecutor dartExecutor) { channel = new MethodChannel(dartExecutor, "flutter/platform_views", StandardMethodCodec.INSTANCE); channel.setMethodCallHandler(parsingHandler); }
そして、上記の parsingHandler
の create
コールバックが実行され、 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);
3つのモードがあると言いつつ、公式のPlatformViewのページを見ると、HCとTLHCの説明しか書いてない。VDの弱点はTLHCで補うことができるからVDはもうほとんど場合で不要、ということかな。
推奨されるTLHCを見ていく。
TLHC (Texture Layer Hybrid Composition)
さっきのフォールバックの分岐処理の部分で、フォールバックせずに、TLHCが選ばれたら configureForTextureLayerComposition()を呼ぶ。そこから先はざっと以下のような流れ。
- まず、
PlatformViewRenderTarget
オブジェクトをmakePlatformViewRenderTarget()で生成する- Android APIレベル29 (Android 10) 以上であれば FlutterRenderer#createSurfaceProducer()で
SurfaceProducerPlatformViewRenderTarget
が生成される- Androidフレームワークの android.media.ImageReaderを生成していて、
ImageReader
がSurface
を保持している
- Androidフレームワークの android.media.ImageReaderを生成していて、
- そうでなければ FlutterRenderer#createSurfaceTexture()で
SurfaceTexturePlatformViewRenderTarget
が生成される。- Androidフレームワークの android.graphics.SurfaceTextureを生成していて、
SurfaceTexture
がSurface
を保持している
- Androidフレームワークの android.graphics.SurfaceTextureを生成していて、
- Android APIレベル29 (Android 10) 以上であれば FlutterRenderer#createSurfaceProducer()で
- 次に、PlatformViewWrapperを生成する。このとき、コンストラクタの引数に上で生成した
PlatformViewRenderTarget
を指定するPlatformViewWrapper
はAndroidのFrameLayout
のサブクラスである
PlatformViewWrapper
のサイズや配置場所を決める- Platform Viewとして埋め込みたい自分で作ったAndroidのView(PlatformViewを継承して実装する)を
PlatformViewWrapper
の子ツリーにするfinal View embeddedView = platformView.getView(); ... viewWrapper.addView(embeddedView);
FlutterView
にPlatformViewWapper
を追加する
ここまででAndroidアプリのViewツリーは以下のように、FlutterView
の子にPlatformViewWrapper
が、PlatformViewWrapper
の子に自分で作ったAndroidのView(Flutterアプリに組み込みたいAndroidのViewツリー)がぶら下がる。
さて、FlutterとしてのWidgetツリーの方ではAndroidView
の部分に Texture が差し込まれる。
前回の記事を思い返すと、AndroidのSurface
にはプロデューサーとコンシューマーがいると書いた。Java側で生成したPlatformViewRenderTarget
が保持するSurface
がプロデューサーとなりバッファに描画をして、Flutter側のTexture
がコンシューマーとなりバッファを読み取る、という形で連携する。
AndroidアプリのViewツリーの描画先はアプリのSurfaceView
になるはずだが、どうやって描画先をPlatformViewRenderTarget
が保持するSurface
にしているのか。
PlatformViewWrapper
の実装を見ると分かる。Androidアプリでは、Viewツリーのルートから順に各Viewのdraw(canvas)
を辿り、各Viewが引数のcanvas|に対して自分自身を描画することで、結果的にアプリ全体を描画する。
PlatformViewWrapperは
draw(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); }
このようにPlatformViewWrapper
はAndroidアプリとしての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 を手にいれる。PlatformView
をVirtualDisplay
に対して描画すると、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が描画されるSurface
もPlatformView
の下と上に分割されると。これらがSurfaceFlinger
で合成されるのだろう。PlatformView
なしの場合とだいぶ違うし、パフォーマンスへの影響が大きい可能性があると。
以下のスライド見るとイメージしやすいです。
Androidで不安定なPlatform Viewsとの闘い - Speaker Deck
おしまい
最初よりはだいぶ分かった気がするぞー。