はじめに
Flutterのアーキテクチャは以下の通り、大きく分けて3つのコンポーネント (Framework
, Engine
, Embedder
) から構成されています。今回はその3つのコンポーネントのうち、Flutter Framework (Dart) 以外の部分について詳しく解説します。
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アクセスを伴う時間がかかる処理を行う専用のスレッド |
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(プロセス間通信)
のメッセージの交換でやり取りすることで対応します。そのやり取りするデータ自体は、共有メモリ (共有ファイル) の方式を採用している様子です。
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をインスタンスしている部分です。
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イベントを通知しています。更に追ってみます。
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;
}
単なるラッパーなので更に追います。
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
で事前に登録されているコールバック関数をコールしています。
更に追います。
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 で事前に登録されているコールバックを呼んでいます。
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
で上記のコールバックを登録するみたいですので、これをコールしているところを更に追います。
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
内にありますね。
追うのはここまでにして、こんな感じでつながっています。
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_)
を追います。
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 が扱えるベクタ形式のデータに変換する機能を実現しています。
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
処理関連のソースコードを追ってみます。
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
の部分を更に追っていきます。
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 を指定して実行していることが分かります。
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/
$ mkdir work
$ touch .gclient
.gclient
ファイル内のurl
部分にはFlutter EngineのリポジトリのURLを指定します。もし、特定のバージョンやブランチなどで固定したい場合には、本家からフォークした自身のリポジトリのパスを記述すると良いです。
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の情報を参考に実行できるはずです。