LoginSignup
68
49

More than 3 years have passed since last update.

Flutter Engineについて解説 - C++のリファレンスにして勉強してみよう

Last updated at Posted at 2020-03-01

はじめに

Flutterのアーキテクチャは以下の通り、大きく分けて3つのコンポーネント (Framework, Engine, Embedder) から構成されています。今回はその3つのコンポーネントのうち、Flutter Framework (Dart) 以外の部分について詳しく解説します。
image.png
Flutter Engineは割と良く出来ているかつコード規模も小さく、スマートフォンなどの組み込み機器向けやデスクトップ環境でも動くように設計されています。そのため、Flutter Engineを参考にしてC++や設計ノウハウを習得できる良い題材になるはずです。ソースコードはちょっとトリッキーな書き方してたりしますが。。

前半はFlutter Engineのアーキテクチャについての解説がメインですが、後半はFlutter Engineのビルド方法とFlutterアプリケーションの実行方法を説明していますので、実際にソースコードをカスタマイズして動作させることも可能です。

なお、大きく分けて3つのコンポーネントと説明しましたが、githubのリポジトリはFlutter FrameworkとFlutter Engineの2つに分かれていることと、Flutter EngineとFlutter Embedderはまとめて1つのコンポーネントとして扱ってもいいのかなとは個人的には思っていますので、ここでは混ぜて解説します。

Flutter Webについては、Dart言語をJavaScript言語に変換してブラウザ上で実行しているため、Flutter Engineは利用しておらず、全く仕組みが異なります。

Flutter Engineの役割

詳細は後から説明しますが、Flutter Engineの役割は簡単に説明すると以下の通りです。

  • Flutterアプリケーションを実行するmain関数になる部分
  • 2Dグラフィックスのレンダリング処理 (Skiaライブラリを利用)
  • Dart VMの実行および管理
  • 各種プラットフォーム (Android, iOS, Linux etc) との中継
  • C++で開発されたソースコード

ちなみに、Embedder部分の役割は各種プラットフォーム上で動作させるための繋ぎ込みのAPIが定義&提供です。この部分をプラットフォーム毎にポーティングします。

ソースコード

https://github.com/flutter/engine
注意: Dart VMやSkia等のFlutter Engine管轄外の3rd-partyのOSSは含まれていません。ビルド時にダウンロードします。

Flutter Engineアーキテクチャ

Flutter Engineは4スレッドで動作します。Flutterの公式ドキュメントの中では、Flutter Engineのレイヤではスレッドという表現を使わずに、Task Runnerという表現を使っています。実際のスレッド自体の作成や管理は下のレイヤのEmbedder (後述のプラットフォーム依存部分) で対応しており、そこから提供される枠組みの中でTask Runnerを動かしています。

そして、Engineには以下の4つのTask Runnerが存在し、Flutterアプリケーションが動作する土台の機能をshell(Linuxなどのコマンドのshellではなく、グラフィック機能を提供するシェル)として提供します。

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アクセスを伴う時間がかかる処理を行う専用のスレッド

図示すると以下の様になります。
スクリーンショット 2021-01-07 23.12.28.png

DartからはFlutter Engineのlibs (Dart→C++のI/Fブリッジ) を経由して、runtimeの機能を触ります。runtimeの主な役割は、DartVMの管理や起動処理、各種サービスの提供です。runtimeは下位レイヤのshellで定義されいているAPIを利用し、各種機能を実現します。

Dart VM

各Task Runnerの説明に入る前に、Dart VMについて少しだけ解説をしておきます。

DartはJavaScript同様に、基本的にはシングルスレッドのプログラミング言語です。が、JavaScriptのWeb Workers同様のマルチスレッドの機能も提供されます。それがIsolate (アイソレート) です。Dartではスレッドではなく、Isolateと呼びますが、仕組み的にはスレッドよりもプロセスの方が近いです。

Isolateはメインスレッドやその他Isolateとは完全にメモリ空間が別になるため、非同期で実行は出来るものの、プロセスが完全に別物になるイメージで例えばグローバル変数等の参照は出来ません。必要なデータはIPC(プロセス間通信)のメッセージの交換でやり取りすることで対応します。そのやり取りするデータ自体は、共有メモリ (共有ファイル) の方式を採用している様子です。
スクリーンショット 2020-03-01 8.26.02.png

Flutter EngineはDart VM起動直後にRoot Isolateを作成し、基本的にFlutterアプリケーションはそこで動作する事になります。Dartから意図的にIsolateを利用すると新規にIsolateが作成されますが、それがなければ基本的にRoot Isolateのみで動作します。

Platform Task Runner

Platform Task RunnerはいわゆるMainスレッドです。Platform Viewsに関わる処理やDartからのメッセージ受信(PlatformChannels)などのイベントを処理し、UI Task Runner と GPU Task Runner の中継役的なこともやったりします。

一例として、Dart VMの起動処理を実行している部分のソースコードを追ってみます。下記はDart VMの初期化設定やFlutter Engineのshellをインスタンスしている部分です。

flutter/shell/common/shell.cc
std::unique_ptr<Shell> Shell::Create(
    TaskRunners task_runners,
    const WindowData window_data,
    Settings settings,
    fml::RefPtr<const DartSnapshot> isolate_snapshot,
    const Shell::CreateCallback<PlatformView>& on_create_platform_view,
    const Shell::CreateCallback<Rasterizer>& on_create_rasterizer,
    DartVMRef vm) {
  PerformInitializationTasks(settings);
  PersistentCache::SetCacheSkSL(settings.cache_sksl);

  TRACE_EVENT0("flutter", "Shell::CreateWithSnapshots");

  if (!task_runners.IsValid() || !on_create_platform_view ||
      !on_create_rasterizer) {
    return nullptr;
  }

  fml::AutoResetWaitableEvent latch;
  std::unique_ptr<Shell> shell;
  fml::TaskRunner::RunNowOrPostTask(
      task_runners.GetPlatformTaskRunner(),
      fml::MakeCopyable([&latch,                                          //
                         vm = std::move(vm),                              //
                         &shell,                                          //
                         task_runners = std::move(task_runners),          //
                         window_data,                                     //
                         settings,                                        //
                         isolate_snapshot = std::move(isolate_snapshot),  //
                         on_create_platform_view,                         //
                         on_create_rasterizer                             //
  ]() mutable {
        shell = CreateShellOnPlatformThread(std::move(vm),
                                            std::move(task_runners),      //
                                            window_data,                  //
                                            settings,                     //
                                            std::move(isolate_snapshot),  //
                                            on_create_platform_view,      //
                                            on_create_rasterizer          //
        );
        latch.Signal();
      }));
  latch.Wait();
  return shell;
}

UI Task Runner

UI Task Runnerは、Dart VMが実行されるスレッドです。Flutter Engineに渡すLayer Treeを生成するまでを担当する、Dartのコードはここで動作します。主に以下の機能を実現しています。

  • Embedder (プラットフォーム側)からのVsync (垂直同期) の通知を受け取り、事前に登録されているコールバック関数経由で UI Task Runner 側のAnimatorに通知。 Platform Task Runner 側は Vsync イベントを受け取るとWidgetの更新処理に進みます
  • Dartのアプリケーションで作成されたツリー情報から、描画処理用のLayer Treeを作成
  • 作成した Layer Tree を GPU Task Runner に渡してラスタライズしてもらう

基本的にUI Task RunerはVsyncトリガーで処理が進みます。

それでは、ソースコードを少しだけ追ってみます。

embedder.ccが各プラットフォーム向けの共通APIが定義されているEmbedderの最上位のソースコードです。関数FlutterEngineOnVsync内のOnVsyncEventでプラットフォームのVsyncイベントを通知しています。更に追ってみます。

flutter/shell/platform/embedder/embedder.cc
FlutterEngineResult FlutterEngineOnVsync(FLUTTER_API_SYMBOL(FlutterEngine)
                                             engine,
                                         intptr_t baton,
                                         uint64_t frame_start_time_nanos,
                                         uint64_t frame_target_time_nanos) {

  // 〜 途中略

  if (!reinterpret_cast<flutter::EmbedderEngine*>(engine)->OnVsyncEvent(
          baton, start_time, target_time)) {
    return LOG_EMBEDDER_ERROR(
        kInternalInconsistency,
        "Could not notify the running engine instance of a Vsync event.");
  }

  return kSuccess;
}

単なるラッパーなので更に追います。

flutter/shell/platform/embedder/embedder_engine.cc
bool EmbedderEngine::OnVsyncEvent(intptr_t baton,
                                  fml::TimePoint frame_start_time,
                                  fml::TimePoint frame_target_time) {
  if (!IsValid()) {
    return false;
  }

  return VsyncWaiterEmbedder::OnEmbedderVsync(baton, frame_start_time,
                                              frame_target_time);
}

strong_waiter->FireCallbackで事前に登録されているコールバック関数をコールしています。
更に追います。

flutter/shell/platform/embedder/vsync_waiter_embedder.cc
bool VsyncWaiterEmbedder::OnEmbedderVsync(intptr_t baton,
                                          fml::TimePoint frame_start_time,
                                          fml::TimePoint frame_target_time) {
  if (baton == 0) {
    return false;
  }

  auto* weak_waiter = reinterpret_cast<std::weak_ptr<VsyncWaiter>*>(baton);
  auto strong_waiter = weak_waiter->lock();
  delete weak_waiter;

  if (!strong_waiter) {
    return false;
  }

  strong_waiter->FireCallback(frame_start_time, frame_target_time);
  return true;
}

task_runners_.GetUITaskRunner()->PostTaskForTimeの部分で UI Task Runner で事前に登録されているコールバックを呼んでいます。

flutter/shell/common/vsync_waiter.cc
void VsyncWaiter::FireCallback(fml::TimePoint frame_start_time,
                               fml::TimePoint frame_target_time) {

  // 〜 途中略

  if (callback) {
    auto flow_identifier = fml::tracing::TraceNonce();

    // The base trace ensures that flows have a root to begin from if one does
    // not exist. The trace viewer will ignore traces that have no base event
    // trace. While all our message loops insert a base trace trace
    // (MessageLoop::RunExpiredTasks), embedders may not.
    TRACE_EVENT0("flutter", "VsyncFireCallback");

    TRACE_FLOW_BEGIN("flutter", kVsyncFlowName, flow_identifier);

    task_runners_.GetUITaskRunner()->PostTaskForTime(
        [callback, flow_identifier, frame_start_time, frame_target_time]() {
          FML_TRACE_EVENT("flutter", kVsyncTraceName, "StartTime",
                          frame_start_time, "TargetTime", frame_target_time);
          fml::tracing::TraceEventAsyncComplete(
              "flutter", "VsyncSchedulingOverhead", fml::TimePoint::Now(),
              frame_start_time);
          callback(frame_start_time, frame_target_time);
          TRACE_FLOW_END("flutter", kVsyncFlowName, flow_identifier);
        },
        frame_start_time);
  }

AsyncWaitForVsyncで上記のコールバックを登録するみたいですので、これをコールしているところを更に追います。

flutter/shell/common/vsync_waiter.cc
void VsyncWaiter::AsyncWaitForVsync(const Callback& callback) {
  if (!callback) {
    return;
  }

  TRACE_EVENT0("flutter", "AsyncWaitForVsync");

  {
    std::scoped_lock lock(callback_mutex_);
    if (callback_) {
      // The animator may request a frame more than once within a frame
      // interval. Multiple calls to request frame must result in a single
      // callback per frame interval.
      TRACE_EVENT_INSTANT0("flutter", "MultipleCallsToVsyncInFrameInterval");
      return;
    }
    callback_ = std::move(callback);
    if (secondary_callback_) {
      // Return directly as `AwaitVSync` is already called by
      // `ScheduleSecondaryCallback`.
      return;
    }
  }
  AwaitVSync();
}

AnimatorのAwaitVSyncで登録出来るみたいで、 UI Task Runner で登録する処理がRequestFrame内にありますね。
追うのはここまでにして、こんな感じでつながっています。

flutter/shell/common/animator.cc
void Animator::RequestFrame(bool regenerate_layer_tree) {
  if (regenerate_layer_tree) {
    regenerate_layer_tree_ = true;
  }
  if (paused_ && !dimension_change_pending_) {
    return;
  }

  if (!pending_frame_semaphore_.TryWait()) {
    // Multiple calls to Animator::RequestFrame will still result in a
    // single request to the VsyncWaiter.
    return;
  }

  // The AwaitVSync is going to call us back at the next VSync. However, we want
  // to be reasonably certain that the UI thread is not in the middle of a
  // particularly expensive callout. We post the AwaitVSync to run right after
  // an idle. This does NOT provide a guarantee that the UI thread has not
  // started an expensive operation right after posting this message however.
  // To support that, we need edge triggered wakes on VSync.

  task_runners_.GetUITaskRunner()->PostTask([self = weak_factory_.GetWeakPtr(),
                                             frame_number = frame_number_]() {
    if (!self.get()) {
      return;
    }
    TRACE_EVENT_ASYNC_BEGIN0("flutter", "Frame Request Pending", frame_number);
    self->AwaitVSync();
  });
  frame_scheduled_ = true;
}

void Animator::AwaitVSync() {
  waiter_->AsyncWaitForVsync(
      [self = weak_factory_.GetWeakPtr()](fml::TimePoint frame_start_time,
                                          fml::TimePoint frame_target_time) {
        if (self) {
          if (self->CanReuseLastLayerTree()) {
            self->DrawLastLayerTree();
          } else {
            self->BeginFrame(frame_start_time, frame_target_time);
          }
        }
      });

  delegate_.OnAnimatorNotifyIdle(dart_frame_deadline_);
}

GPU Task Runner

GPU Task Runnerはその名前の通り、2Dグラフィック処理を実行します。 UI Task Runner のAnimatorから渡された Layer Tree を元に、2Dグラフィックライブラリの Skia を利用してレンダリングを行います。 Skia はベクタグラフィックスという点と線を結んだ情報で構成される画像を処理します。ベクタグラフィックスについては、ベクタグラフィックスのすすめを参考にしてください。

少しだけその部分のソースコードを追ってみます。

Animator がコールしているdelegate_.OnAnimatorDraw(layer_tree_pipeline_)を追います。

flutter/shell/common/animator.cc
void Animator::Render(std::unique_ptr<flutter::LayerTree> layer_tree) {
  if (dimension_change_pending_ &&
      layer_tree->frame_size() != last_layer_tree_size_) {
    dimension_change_pending_ = false;
  }
  last_layer_tree_size_ = layer_tree->frame_size();

  if (layer_tree) {
    // Note the frame time for instrumentation.
    layer_tree->RecordBuildTime(last_begin_frame_time_);
  }

  // Commit the pending continuation.
  producer_continuation_.Complete(std::move(layer_tree));

  delegate_.OnAnimatorDraw(layer_tree_pipeline_);
}

Rasterizerが GPU Task Runner で実行されていることが分かりますね。
Rasterizer では、 Skia が扱えるベクタ形式のデータに変換する機能を実現しています。

flutter/shell/common/shell.cc
void Shell::OnAnimatorDraw(fml::RefPtr<Pipeline<flutter::LayerTree>> pipeline) {
  FML_DCHECK(is_setup_);

  task_runners_.GetGPUTaskRunner()->PostTask(
      [& waiting_for_first_frame = waiting_for_first_frame_,
       &waiting_for_first_frame_condition = waiting_for_first_frame_condition_,
       rasterizer = rasterizer_->GetWeakPtr(),
       pipeline = std::move(pipeline)]() {
        if (rasterizer) {
          rasterizer->Draw(pipeline);

          if (waiting_for_first_frame.load()) {
            waiting_for_first_frame.store(false);
            waiting_for_first_frame_condition.notify_all();
          }
        }
      });
}

IO Task Runner

画像ファイルのDecode処理などのI/Oアクセスが絡む重い処理専用のTask Runnerです。

一例としてDecode処理関連のソースコードを追ってみます。

flutter/lib/ui/painting.dart
Future<Null> _decodeImageFromListAsync(Uint8List list,
                                       ImageDecoderCallback callback) async {
  final Codec codec = await instantiateImageCodec(list);  // ここでC++のコードをコール
  final FrameInfo frameInfo = await codec.getNextFrame(); // ここでC++のコードをコール
  callback(frameInfo.image);
}

以下がDartからコールされているC++のソースコード該当箇所です。decoder->Decodeの部分を更に追っていきます。

flutter/lib/ui/painting/single_frame_codec.cc
Dart_Handle SingleFrameCodec::getNextFrame(Dart_Handle callback_handle) {

  // 〜 途中略

  auto decoder = dart_state->GetImageDecoder();
  if (!decoder) {
    return tonic::ToDart("Image decoder not available.");
  }

  fml::RefPtr<SingleFrameCodec>* raw_codec_ref =
      new fml::RefPtr<SingleFrameCodec>(this);

  decoder->Decode(descriptor_, [raw_codec_ref](auto image) { // ここ!
    std::unique_ptr<fml::RefPtr<SingleFrameCodec>> codec_ref(raw_codec_ref);
    fml::RefPtr<SingleFrameCodec> codec(std::move(*codec_ref));

以下のようにデコード処理は IO Task Runner を指定して実行していることが分かります。

flutter/lib/ui/painting/image_decoder.cc
void ImageDecoder::Decode(ImageDescriptor descriptor,
                          const ImageResult& callback) {
  // 〜 途中略

  concurrent_task_runner_->PostTask(
      fml::MakeCopyable([descriptor,                              //
                         io_manager = io_manager_,                //
                         io_runner = runners_.GetIOTaskRunner(),  //
                         result,                                  //
                         flow = std::move(flow)                   //
  ]() mutable {
        // Step 1: Decompress the image.
        // On Worker.

        auto decompressed =
            descriptor.decompressed_image_info
                ? ImageFromDecompressedData(
                      std::move(descriptor.data),                  //
                      descriptor.decompressed_image_info.value(),  //
                      descriptor.target_width,                     //
                      descriptor.target_height,                    //
                      flow                                         //
                      )
                : ImageFromCompressedData(std::move(descriptor.data),  //
                                          descriptor.target_width,     //
                                          descriptor.target_height,    //
                                          flow);

        if (!decompressed) {
          FML_LOG(ERROR) << "Could not decompress image.";
          result({}, std::move(flow));
          return;
        }
}

ソースコードツリー解説

ソースコードディレクトリについて主要部分を簡単に解説します。

flutter/fml

  • Flutter Engime内で共通利用するUtility関数など

flutter/common

  • 内容的に個別にディレクトリが作成されているか不明。。 flutter/fmlではダメ?

flutter/lib/ui

  • Dart / C++ の FFI (Foreign function interface)

flutter/runtime

  • Dart VM管理
    • JIT, AOTモードに応じた起動処理やスナップショットデータのロードやセーブ対応
  • 上位レイヤへのruntime機能の提供

flutter/shell

  • shell機能本体

flutter/shell/common

  • プラットフォームに依存しない共通部分で、Engine中核部分
  • Animator, Platform View, Rasterizer等の実装

flutter/shell/gpu

  • レンダリング処理のSoft/OpenGL/Vulkan選択のための抽象化レイヤ

flutter/shell/platform

  • Embedder本体でmainループはここに存在
  • Flutter Engineとしてプラットフォーム向けにAPIを定義
  • 各種プラットフォーム依存部分のコードはサブディレクトリ (android, darwin, fuchsia, linux etc) 化して実装

Flutter Engineのビルド手順

ここからはFlutter Engineのビルド手順と、ビルドしたEngineの実行手順について解説します。
ターゲット環境はLinux/macOSですが、どちらも手順は同じです。Windowsは今回解説しません (ごめんなさい) 。

環境セットアップ

ビルドするためにはGoogle独自のツールを利用する必要がありますので、そのセットアップを行います。

ツールのダウンロード
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
適当な場所に置いてパスを通す
$ mv ./depot_tools ${INSTALL_PATH}
$ export PATH=$PATH:${INSTALL_PATH}/depot_tools/
作業フォルダを作成し、その中に.gclientファイルを作成
$ mkdir work
$ touch .gclient

.gclientファイル内のurl部分にはFlutter EngineのリポジトリのURLを指定します。もし、特定のバージョンやブランチなどで固定したい場合には、本家からフォークした自身のリポジトリのパスを記述すると良いです。

.gclientの編集
solutions = [
  {
    "managed": False,
    "name": "src/flutter",
    "url": "https://github.com/flutter/engine.git",
    "custom_deps": {},
    "deps_file": "DEPS",
    "safesync_url": "",
  },
]

ソースコード取得

gclient syncコマンドでFlutter Engine依存のソースコード類を取得します。

$ gclient sync

ビルド for Host PC環境

AndroidやiOS等向けのクロスビルドも可能ですが、ここではホストPC環境 (デスクトップ) 向けにビルドします。ビルドオプションはいろいろありますが、今回は一つだけ紹介します。

$ cd src
$ ./flutter/tools/gn --unoptimized
$ ninja -C out/host_debug_unopt

ビルドが成功すると、out/host_debug_unopt以下に成果物が生成されています。ライブラリやデータ等がいくつか生成されていますが、Linux環境であればlibflutter_engine.so、macOS環境であればlibflutter_engine.dylibが作成されていればおそらくビルドは成功しています。
※これら以外にもFlutterアプリ実行に必要なファイルはあります。

ビルドしたEngineでFlutterアプリを実行

flutter runコマンドのオプションを利用して、ローカルビルドしたEngineを利用することが出来ます。
注意点としては、Flutter SDKとFlutter Engineのバージョンを揃える必要があることです。これが揃ってないとFlutter Engine (Dart VM) の起動処理でエラーで落ちます。

$ flutter run --local-engine-src-path ${作業場所のパス}/src --local-engine=host_debug_unopt

Engineのデバッグ方法

Flutter Engine自体をデバッグする時にはgdbを利用します。
私は試していませんが、Debugging the engineの情報を参考に実行できるはずです。

参考文献

68
49
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
49