はじめに
React NativeとFlutterのレンダリングアーキテクチャについて紹介します。
React NativeとFlutterは、ともにモバイルアプリのクロスプラットフォーム開発フレームワークですが、JS/Dartコードとネイティブコード間の相互通信の方法、コアエンジンの違いなど、様々な違いが存在します。
本記事では、それらの内容について紹介したいと思います。
React Native
React Nativeでは、これまでのJavaScriptクロスプラットフォームフレームワークとは異なり、プラットフォームごとにネイティブウィジェットを呼び出しレンダリングを行います。
以下では、レンダリングが行われるまでの仕組みと、JavaScriptとネイティブコード間の通信について紹介します。
Thread
React Native ではすべての処理が以下のスレッドで実行されます。
- Main Thread
- Shadow Queue (Shadow Node Thread)
- Native Modules
- JS Thread
Main Thread
はUIのレンダリングを行うスレッドです。TouchやPressなどのインタラクションイベントの受け取りも行い、ブリッジを介してJS Thread
へ通知します。
Shadow Queue
はUIのレンダリングに必要なプロパティを受け取り、UIの位置を決定するための演算処理を行います。レンダリングをする準備が整うとMain Thread
に処理を引き渡します。
Native Modules
はネイティブAPIを使用した処理を行い、各Native Module
は独自のスレッドで動作します。iOSではパラレルGCDキューを使用し、Androidではスレッドプールを共有します。
JS Thread
はすべてのJavaScriptアプリケーションコードが実行されるスレッドです。JavaScriptイベントループに基づいているため、UIスレッドよりも遅く、アプリケーションで複雑な計算を行って多くのUIを変更すると、パフォーマンスが低下する可能性があります。
ブリッジ
JavaScriptとネイティブコード間のすべての通信およびメッセージはブリッジを介します。
ブリッジを介した情報は、Async Serialized Batched
により下記のJSON形式にシリアル化され、MessageQueue
で処理されます。
// type: 0=N->JS, 1=JS->N
type BridgeData = {
type: number,
module: ?string,
method: string | number,
args: any
}
ネイティブモジュール
ネイティブモジュールは、ネイティブAPIへのアクセスを提供します。
最初に、ネイティブモジュールまたはUIコンポーネントを構築する必要があるかどうかを選択する必要があります。ネイティブモジュールは、メソッドと定数をエクスポートするだけでUIをレンダリングは行いません。
UIコンポーネント
UIコンポーネントを構築するには、ViewManager を使用します。ViewManager は View を生成するファクトリで、ViewManager 自身のインスタンスがブリッジごとに作成されます。
ViewManagerとブリッジは以下の図の通りに働き、ネイティブビューをレンダリングします。
- ブリッジは、全てのネイティブモジュールの情報を保持します
- ネイティブコンポーネントを要求します (
requireNativeComponent
) - ViewManagerは、ブリッジのビューインスタンスへの参照を格納するビューを作成します
- ビューの参照を送信します
- 他のReact Componentと同様に
render
を呼び出し、最終的にネイティブビューをレンダリングします
Flutter
一方、フラッターではネイティブウィジェットのレンダリングを行いません。Dartフレームワークで管理されたウィジェットを呼び出し、レンダリングエンジンに依存して2Dウィジェット要素をペイントします。
主にC++で記述されたFlutterのレンダリングエンジンは、GoogleのSkia Graphics Libraryを使用して低レベルのレンダリングサポートを提供します。
参照: The-Engine-architecture - GitHub
Thread
Flutterエンジンは、独自のスレッドを作成または管理せず、Embedder
(各プラットフォーム)で作成・管理する必要があります。また、Embedder
はFlutterエンジンのスレッドで動作するTask Runnerを提供します。
The Flutter engine does not create or manage its own threads. Instead, it is the responsibility of the embedder to create and manage threads (and their message loops) for the Flutter engine. The embedder gives the Flutter engine task runners for the threads it manages. In addition to the threads managed by the embedder for the engine, the Dart VM also has its own thread pool. Neither the Flutter engine or the embedder have any access to the threads in this pool.
Task Runner
主なタスクランナーは次のとおりです。
Platform Task Runner
UI Task Runner
GPU Task Runner
IO Task Runner
Platform Task Runner
は、Embedder
がメインスレッドと見なすスレッドのタスクランナーです。
例えば、Androidではメインスレッド、iOSではFoundationによって参照されるメインスレッドです。
UI Task Runner
は、エンジンがRoot IsolateのすべてのDartコードを実行する場所です。
GPU Task Runner
は、デバイス上のGPUにアクセスする必要があるタスクを実行します。OpenGL
Vulkan
などのSkia用にセットアップされたバックエンドソフトウェアを使用してレンダリングを行うことができます。
IO Task Runner
は、主にアセットから圧縮画像を読み取り、画像データを処理し、共有のContextを通じてGPU Task Runner
に処理を引き渡すことができます。つまり、ディスクIOに関連するトランザクションが処理されます。
レンダリング結果の違い
React NativeとFlutterでレンダリングアーキテクチャが大きく異なる点として、React Nativeはネイティブコードが提供するモジュールをレンダリングし、FlutterはDartフレームワークに組み込まれたウィジェットを、Skia Graphics Libraryがレンダリングする点です。
これは、ウィジェットが最終的にレンダリングされる結果に違いを及ぼします。
上記の例として、マテリアルデザインが採用された Android 5.0 (APILevel 21) 以降と未満ではUIが大きく変わります。
実際にReact Nativeで作成したアプリでは、特別な処理やライブラリを入れない限り、ネイティブコードをレンダリングするため、上記の仕様が踏襲されるはずです。
一方、Flutterのレンダリングエンジンは、OSバージョンやAPILevelに関係なく、Dartフレームワークに組み込まれたマテリアルデザインウィジェット(Android)/クパチーノウィジェット(iOS)をレンダリングするため、バージョンの差分を吸収して同じ結果が出力されるはずです。
おわりに
これらの内容は、アプリのパフォーマンスチューニングを行う場合や、ネイティブAPIを使用する必要がある場合にとても役立つ情報となります。
また、コアレンダリングエンジンの違いは、技術選定を行う上で重要なポイントとなるのではないでしょうか。
この記事が少しでもモバイルアプリ開発者の参考になれば幸いです。