Unlocking Flutter 2 Router: Deep Dive into Implementation and Source Code
This article guides developers through compiling, debugging, and analyzing the Flutter 2 Router source code, covering engine setup, architecture layers, key initialization classes, and practical steps to run a custom Flutter engine on Android and iOS.
Flutter 2 Router: From Basics to Deep Source Analysis (Part 1)
Zhou Jianhua: Mobile medical diagnosis team, Android developer who loves reading and sports.
Preface
In the previous article we covered the basic usage of multi‑engine mixed development and the differences between multi‑engine and single‑engine approaches. This article focuses on how multi‑engine reuse is implemented by examining the source code.
1. Flutter 2 Source Compilation and Debugging
Source Compilation
First install depot_tools and add it to the PATH.
<code>git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH
</code>Create an empty engine directory and a .gclient configuration file that points to a fork of the flutter/engine repository.
<code>solutions = [
{
"managed": False,
"name": "src/flutter",
"url": "https://github.com/Alex0605/engine.git",
"custom_deps": {},
"deps_file": "DEPS",
"safesync_url": "",
},
]
</code>Run gclient sync inside the engine directory.
Switch the source to the engine version that matches the local Flutter SDK commit:
<code># View the engine version used by the local Flutter SDK
vim src/flutter/bin/internal/engine.version
# Adjust the code
cd engine/src/flutter
git reset --hard <commit id>
gclient sync -D --with_branch_heads --with_tags
</code>Generate build configurations for Android and iOS, then compile:
<code># Android
./flutter/tools/gn --unoptimized
./flutter/tools/gn --android --unoptimized
./flutter/tools/gn --android --unoptimized --runtime-mode=debug --android-cpu=arm64
ninja -C out/host_debug_unopt -j 16
ninja -C out/android_debug_unopt -j 16
ninja -C out/android_debug_unopt_arm64 -j 16
# iOS
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm64
ninja -C out/ios_debug_unopt_arm
ninja -C out/ios_debug_unopt
ninja -C out/host_debug_unopt_arm
ninja -C out/host_debug_unopt
</code>After compilation the output directories look like this:
Source Runtime Debugging
Create a new Flutter project:
flutter create --org com.wedotor.flutter source_codeOpen the generated Android project in Android Studio.
Add the localEngineOut property to gradle.properties :
<code>org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
android.enableJitfier=true
localEngineOut=/Users/zhoujh/myproj/3-proj/flutter/engine/src/out/android_debug_unopt_arm64
</code>Import the engine source directory
engine/src/flutter/shell/platform/android(the *Flutter Engine project*) into Android Studio.
Run a Flutter App using the custom engine (the *Flutter App project*) and set breakpoints in the engine project to debug the running process.
For C++ debugging, copy the generated
compile_commands.jsoninto
src/flutterand open the project with CLion.
2. Flutter 2 Source Reading
The official architecture diagram (shown below) represents the three layers of the Flutter architecture:
Framework : Implemented in Dart, includes Material and Cupertino widgets, basic widgets, rendering, animation, and gesture handling. Core code lives in the
flutterpackage and the
sky_enginepackage (e.g.,
dart:uibindings to Skia).
Engine : Implemented in C++, comprises Skia, the Dart runtime, and text handling. Skia provides a cross‑platform 2D graphics API; the Dart VM can run in JIT, JIT‑snapshot, or AOT mode.
Embedder : The platform‑specific layer that embeds Flutter into Android, iOS, etc., handling surface setup, threading, and plugins. This low‑level layer gives Flutter its cross‑platform consistency.
App Startup and FlutterEngineGroup
When the app starts,
FlutterEngineGroupis created in
Application.onCreate:
<code>public void onCreate() {
super.onCreate();
// Create FlutterEngineGroup object
engineGroup = new FlutterEngineGroup(this);
}
</code> FlutterEngineGroupallows sub‑engines to share resources, resulting in faster creation and lower memory usage compared with creating separate
FlutterEngineinstances.
<code>public FlutterEngineGroup(@NonNull Context context, @Nullable String[] dartVmArgs) {
FlutterLoader loader = FlutterInjector.instance().flutterLoader();
if (!loader.initialized()) {
loader.startInitialization(context.getApplicationContext());
loader.ensureInitializationComplete(context, dartVmArgs);
}
}
</code>FlutterLoader.startInitialization
This method loads
flutter.so, extracts Dart assets, and prepares VSync handling. Key steps include:
Check whether settings have been assigned.
Must run on the main thread.
Obtain the application context.
Initialize
VsyncWaiter.
Record initialization time.
Run heavy I/O (resource extraction, library loading) on a background thread.
<code>public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
if (this.settings != null) return;
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("startInitialization must be called on the main thread");
}
final Context appContext = applicationContext.getApplicationContext();
this.settings = settings;
initStartTimestampMillis = SystemClock.uptimeMillis();
flutterApplicationInfo = ApplicationInfoLoader.load(appContext);
VsyncWaiter.getInstance((WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE)).init();
Callable<InitResult> initTask = new Callable<InitResult>() {
@Override
public InitResult call() {
ResourceExtractor resourceExtractor = initResources(appContext);
flutterJNI.loadLibrary();
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
flutterJNI.prefetchDefaultFontManager();
}
});
if (resourceExtractor != null) {
resourceExtractor.waitForCompletion();
}
return new InitResult(PathUtils.getFilesDir(appContext),
PathUtils.getCacheDirectory(appContext),
PathUtils.getDataDirectory(appContext));
}
};
initResultFuture = Executors.newSingleThreadExecutor().submit(initTask);
}
</code>initResources
Copies assets from the APK to the app’s local storage (e.g.,
vm_snapshot_data,
isolate_snapshot_data,
kernel_blob.bin) when in DEBUG or JIT_RELEASE mode.
<code>private ResourceExtractor initResources(@NonNull Context applicationContext) {
ResourceExtractor resourceExtractor = null;
if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
final String dataDirPath = PathUtils.getDataDirectory(applicationContext);
final String packageName = applicationContext.getPackageName();
final PackageManager packageManager = applicationContext.getPackageManager();
final AssetManager assetManager = applicationContext.getResources().getAssets();
resourceExtractor = new ResourceExtractor(dataDirPath, packageName, packageManager, assetManager);
resourceExtractor
.addResource(fullAssetPathFrom(flutterApplicationInfo.vmSnapshotData))
.addResource(fullAssetPathFrom(flutterApplicationInfo.isolateSnapshotData))
.addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB));
resourceExtractor.start();
}
return resourceExtractor;
}
</code>ensureInitializationComplete
Verifies that initialization has finished and passes the library path to
FlutterJNIvia
shellArgs. This method must also run on the main thread.
<code>public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {
if (initialized) return;
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("ensureInitializationComplete must be called on the main thread");
}
if (settings == null) {
throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization");
}
try {
InitResult result = initResultFuture.get();
List<String> shellArgs = new ArrayList<>();
// ... configure shellArgs ...
long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
flutterJNI.init(applicationContext, shellArgs.toArray(new String[0]), kernelPath,
result.appStoragePath, result.engineCachesPath, initTimeMillis);
initialized = true;
} catch (Exception e) {
Log.e(TAG, "Flutter initialization failed.", e);
throw new RuntimeException(e);
}
}
</code>FlutterJNI.init
<code>public void init(@NonNull Context context,
@NonNull String[] args,
@Nullable String bundlePath,
@NonNull String appStoragePath,
@NonNull String engineCachesPath,
long initTimeMillis) {
if (FlutterJNI.initCalled) {
Log.w(TAG, "FlutterJNI.init called more than once");
}
FlutterJNI.nativeInit(context, args, bundlePath, appStoragePath, engineCachesPath, initTimeMillis);
FlutterJNI.initCalled = true;
}
</code>Loading flutter.so and JNI_OnLoad
After resources are ready,
flutter.sois loaded by the Android VM. The first native entry point is
JNI_OnLoad, which registers
FlutterMain,
PlatformView, and
VSyncWaiter.
<code>JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
fml::jni::InitJavaVM(vm);
JNIEnv* env = fml::jni::AttachCurrentThread();
bool result = false;
result = flutter::FlutterMain::Register(env);
FML_CHECK(result);
result = flutter::PlatformViewAndroid::Register(env);
FML_CHECK(result);
result = flutter::VsyncWaiterAndroid::Register(env);
FML_CHECK(result);
return JNI_VERSION_1_4;
}
</code>FlutterMain::Init
Parses the
argsarray into a
Settingsobject, sets up paths for snapshots, caches, and registers callbacks for task observation and logging.
<code>void FlutterMain::Init(JNIEnv* env,
jclass clazz,
jobject context,
jobjectArray jargs,
jstring kernelPath,
jstring appStoragePath,
jstring engineCachesPath,
jlong initTimeMillis) {
std::vector<std::string> args;
args.push_back("flutter");
for (auto& arg : fml::jni::StringArrayToVector(env, jargs)) {
args.push_back(std::move(arg));
}
auto command_line = fml::CommandLineFromIterators(args.begin(), args.end());
auto settings = SettingsFromCommandLine(command_line);
// ... set timestamps, cache paths, load snapshots, etc. ...
g_flutter_main.reset(new FlutterMain(std::move(settings)));
g_flutter_main->SetupObservatoryUriCallback(env);
}
</code>Postscript
The above mainly covers the initialization process of FlutterEngineGroup in Flutter 2. In the next section we will explore how to create FlutterEngine instances from the group and bind UI pages.
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.