[Flutter] Platform Viewsの仕組みとパフォーマンス影響を理解して利用する

## 1. はじめに

FlutterでネイティブアプリのView (画面) をFlutter側に表示する仕組みである[Platform Views](https://flutter.dev/docs/development/platform-integration/platform-views)についてまとめました。Platform Viewsは万能ではないのと、パフォーマンスにも多少なりとも影響を与えるので正しく理解して利用しましょうというのが本記事の目的です。ただし、筆者がiOSには詳しくないということもあり、AndroidとFlutter desktop (Linux, Windows, macOS) の内容を中心に解説します。

## 2. Platform Viewsとは？

通常、FlutterはFlutterのフレームワークで用意されているウィジェットを利用して目的のUIを作成します。

しかし、例えば動画再生アプリでは、ネイティブ側のMediaPlayerのような動画デコーダを利用してそのデコード画面をFlutter側の画面に表示する必要があります。他にも、インターネット上のページを表示したい (いわゆるWebView) こともあります。

このユースケースを実現する仕組みが`Platform Views`で、各プラットフォームネイティブの目的のViewをFlutterのUIの中に取り込むための機能です。

## 3. Android

Flutterは、Android環境向けには`Virtual Display`と`Hybrid Composition`の2種類のAPIを提供します。それぞれトレードオフがあるので、適切に選択する必要があります。

最初に、ざっくり使い分けを言うと、メディアプレイヤなどのネイティブ側のViewを表示するだけであれば`Virtual Display`、WebViewのようにユーザイベント（タッチ操作など）を伴うものであれば`Hybrid Composition`と言う感じでしょうか。

|種類|Pros|Cons|
|---|---|---|
|[Virtual Display](https://developer.android.com/reference/android/hardware/display/VirtualDisplay)|Hybrid Compositionより高速|ユーザのアクセシビリティに課題あり|
|Hybrid Composition|Virtual Displayのアクセシビリティの課題が解決|パフォーマンスに懸念あり|

### Virtual Displayの解説

[Virtual Display](https://developer.android.com/reference/android/hardware/display/VirtualDisplay)はAndroid OSの用語で、日本語だとそのまま仮想ディスプレイです。物理的なディスプレイへの描画の代わりにオフスクリーンレンダリングできるように、内部にサーフェスを持つFBO (バッファ) のイメージです。Androidでのユースケースだと、スクリーンキャプチャ系が多そうです。

仕組みを以下に示します。Androidネイティブの環境で用意されたサーフェス（SurfaeView）に対して、基本的にFlutterはUIをグラフィックスとして描画します。一方、Platform ViewsとしてFlutterに追加したいネイティブのUIは、VirtualDiplayで用意された空間にAndroidの目的のViewを追加し、そのサーフェスをFlutter側から操作して目的の位置・レイヤに合成することで、最終的なUI画面を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/d7b43c58-4c7f-ee12-2d85-94b547da3961.png)

このように、サーフェスを複数用意して最後にGPUで合成しているだけのため、通常のグラフィックス処理と同じでパフォーマンス的には特に問題ないと思います（パフォーマンス測定した訳ではないのと、グラフィックスの使用メモリや処理は増えるから使わないケースに比べるとパフォーマンスは落ちるのは当然だけど、体感でわかるレベルで落ちることはないかと。Android Frameworkの実装にも寄りますが…）。

ここで注意しなければならないのが、Virtual DisplayはAndroidのActivityにマッピングされている訳ではないという点です。通常のViewへの追加であれば、ユーザイベント系をViewを経由して拾うことが出来ますが、AndroidのVirtualDisplayを利用する場合は通常はタッチやキーボードなどのイベントを検出出来ません。しかし、Flutterでは、[AccessibilityNodeProvider](https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeProvider)というアクセシビリティの機能を利用し、タッチ等のイベントを検出できるようにされてはいますが、Flutter公式サイトにも以下のように少し弱気なコメントが残されており、何らか問題が出る可能性があります（バーチャルキーボードのハンドリングなどすでにバグあり）。

> Virtual displays renders the android.view.View instance to a texture, so it’s not embedded within the Android Activity’s view hierachy. Certain platform interactions such as keyboard handling, and accessibility features might not work.

#### サンプルコード

[[Flutter] Platform Viewsを利用した簡易WebViewプラグインの作り方](https://qiita.com/kurun_pan/items/7c9c8fba3346ab4c7c5f)を参考にして下さい。

### Hybrid Compositionの解説

[Hybrid composition](https://github.com/flutter/flutter/wiki/Hybrid-Composition)は、Flutter 1.22以降で利用できる新しい手法です。Virtual Displayのアクセシビリティの問題を改善するための代替機能ですが、仕組み上はパフォーマンスがVirtual Displayに劣ります。

Virtual Displayの場合はAndroidのViewとして直接アタッチ （addView) しない（出来ない）ので、アクセシビリティの課題がありました。では、正確にユーザイベントを検出するために実際にaddViewしているのがHybrid Compositionです。なので本当にAndroidのActivityにaddViewします。

しかし、単純にAndroidネイティブのActivityにaddViewすると、Flutter側からUI画面のレイヤの優先度やPlatformViewsの表示位置, サイズ, 最終的なViewの合成などの制御が難しくなるので、Flutter側からはどうしてもサーフェスという単位でグラフィックスとして扱える必要があります。

そこで登場するキーとなるのが[FlutterImageView](https://github.com/flutter/engine/blob/master/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java)と、この中で利用している[ImageReader](https://developer.android.com/reference/android/media/ImageReader)です。ImageReaderは、Camera用途で利用されることが多い様子で、カメラからキャプチャしたイメージバッファを直接そのままGPUで加工したり、UIに表示するユースケースを想定しているのか、サーフェスを取得やイメージデータのバッファを直接操作できるという点が特徴です。

Virtual Displayの場合、Flutter側で全てのサーフェスを合成レンダリングして最終的なUI画面を作成していましたが、Hybrid compositionの場合、Flutter側で作成するUI画面とAndroidネイティブ側で作成するUI画面を同時に描画することで、見た目上の最終的な画面を作っています。そのような意味で`Hybrid`と言っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/0893c076-274f-8257-efe4-575e4f9f48dc.png)

[FlutterImageView](https://github.com/flutter/engine/blob/master/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java)のソースコードを見てもらえれば分かりますが、イメージデータのコピーが毎フレームされているのが分かります。

## 4. iOS

iOSの場合、`UIView`の1種類のAPIしか提供されていません。仕組み的には、Hybrid compositionとのことですが、iOSは全然詳しくないですし、Flutter EngineはObjective-Cで書かれており雰囲気でしか読めません。

iOSは以上。

## 5. Desktop (Linux, Windows, macOS)

Linux, Windows, macOSのデスクトップ環境のPlatform Viewsは2020/12/22時点でまだ未実装です。

デスクトップ向けの場合、Android/iOSと少し状況（実装）が異なり、Flutter Engineは[Embedder](https://github.com/flutter/flutter/wiki/Custom-Flutter-Engine-Embedders)というプラットフォーム向けのポーティングレイヤ（API）を提供してくれています（Android/iOSは直接はこのポーティングレイヤを利用していない。おそらく、デスクトップ向けにインタフェースを共通化できるように途中から作成したのではないかと予想しています）。そして、このEmbedderの[API](https://github.com/flutter/engine/blob/master/shell/platform/embedder/embedder.h)の中にはPlatform Viewsを使うためのAPIが定義されています。

仕組み的には、macOSはiOS同様、`UIView`を利用したHybrid Composition方式になりそうですが、それ以外の環境では、AndroidのVirtual Displayのように、ネイティブ側で作成したFBOにネイティブアプリが描画し、そのサーフェスをFlutter側で適切な場所にレンダリングするというアプローチになります（Android/iOSと違って、システムとしてのアプリケーションフレームワークはないため、Hybrid Composition的なことは出来ません）。

ということで、デスクトップ環境向けにはEmbedderのAPIを利用した形でいつか実装がされると思います（来年？）。

## 6. Platform Viewsのパフォーマンス影響

そもそもPlatform Viewsを利用するとFlutter UIのレンダリングのパフォーマンスが低下する可能性があります（Virtual DisplayやHybrid Compositionどちらかの選択, 利用するOSに依らず）。パフォーマンスはPlatform Viewsで扱うイメージサイズにも依存、一般的にはサイズが大きくなればなるほどパフォーマンスは落ちます。

Flutterは、各プラットフォーム上で基本的に以下の4スレッドで動作します。

|スレッド (タスクランナー) 名|用途|
|---|---|
|Platform Task Runner|いわゆるメインスレッドで、ユーザ操作やネイティブからのメッセージをハンドリングし、その他のTask Runnerとの受け渡しをするスレッド|
|UI Task Runner|Dart VMが実行されるスレッド。Flutter Engineに渡すLayer Treeを生成するまでを担当する、Dartのコードが動くスレッド|
|GPU Task Runner|GPU処理に関わるつまり、Skiaを利用してOpenGLやVulkanなどでGPUを利用して最終的に描画する処理に関わるスレッド|
|IO Task Runner|画像ファイルのデコードなど、I/Oアクセスを伴う時間がかかる処理を行う専用のスレッド|

Platform Viewsに関する処理は、基本的に`Platform Task Runner`で行われるため、これが重たくなるともともとPlatform Task Runnerが行っていた処理が滞るようになり、結果的にそれはユーザ操作に対するのレスポンス低下や、UI Task Runnerの処理をブロッキングするなどの原因となる可能性があるためです。

実際、どこまでパフォーマンス的に影響あるのかは時間が出来たら見てみたいと思います。