React NativeとFlutterのレンダリングアーキテクチャ


はじめに

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で処理されます。

example
// 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とブリッジは以下の図の通りに働き、ネイティブビューをレンダリングします。

  1. ブリッジは、全てのネイティブモジュールの情報を保持します
  2. ネイティブコンポーネントを要求します (requireNativeComponent)
  3. ViewManagerは、ブリッジのビューインスタンスへの参照を格納するビューを作成します
  4. ビューの参照を送信します
  5. 他の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を使用する必要がある場合にとても役立つ情報となります。
また、コアレンダリングエンジンの違いは、技術選定を行う上で重要なポイントとなるのではないでしょうか。
この記事が少しでもモバイルアプリ開発者の参考になれば幸いです。

参考URL