Flutter 2 Router 從入門到放棄 - 實現原理與原始碼分析(一)

微醫前端團隊發表於2021-08-11

周建華: 微醫移動端診療組, 喜歡看書和運動的 Android 程式猿

前言

在上一篇文章Flutter 2 Router 從入門到放棄 - 基本使用、區別&優勢中,主要講了多引擎混合開發的基本用法以及多引擎和單引擎混合開發的區別,本文我們主要通過原始碼看看多引擎複用是如何實現。

一、Flutter 2 原始碼編譯除錯

工欲善其事,必先利其器,這裡我們先對原始碼編譯和除錯步驟進行說明:

原始碼編譯

安裝 depot_tools,配置環境變數

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH
複製程式碼

建立空的 engine 目錄並在目錄中建立 .gclient 配置檔案,在 .gclient 中配置從 flutter/engine 主工程 fork 出的 github 工程地址,.gclient 配置如下

solutions = [
  {
    "managed": False
    "name": "src/flutter"
    "url": "https://github.com/Alex0605/engine.git"
    "custom_deps": {}
    "deps_file": "DEPS"
    "safesync_url": ""
  }
]
複製程式碼

engine 目錄中執行 gclient sync

切換原始碼。編譯前的一個重要操作是將原始碼切換到本地 Flutter SDKengine version 對應的提交點

# 檢視本地 Flutter SDK 引擎版本, 這個檔案中是包含對應的 commit id
vim src/flutter/bin/internal/engine.version

# 調整程式碼
cd engine/src/flutter
git reset --hard <commit id>
gclient sync -D --with_branch_heads --with_tags

# 準備構建檔案
cd engine/src

#Android
# 使用以下命令生成 host_debug_unopt 編譯配置
./flutter/tools/gn --unoptimized
# android arm (armeabi-v7a) 編譯配置
./flutter/tools/gn --android --unoptimized
# android arm64 (armeabi-v8a) 編譯配置
./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
# unopt-debug
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm64

./flutter/tools/gn --unoptimized --runtime-mode debug --ios-cpu arm
./flutter/tools/gn --unoptimized --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
複製程式碼

編譯完成後的目錄如下:

3285aaa1-2323-41ff-85d0-5d1641cac174.png

原始碼執行除錯

通過命令建立一個 flutter 工程

flutter create --org com.wedotor.flutter source_code

android studio 開啟建立的 android 工程

gradle.properties 檔案中新增 localEngineOut 屬性,配置如下:

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
複製程式碼

將 engine/src/flutter/shell/platform/android 工程(稱之為* Flutter 引擎工程*)匯入到 Android Studio

使用自定義 Flutter 引擎執行 Flutter App(稱之為 Flutter App 工程),具體如 1-3 步所述

Flutter 引擎工程 中給原始碼設定斷點並啟動 Debugger 連線到已啟動的 Flutter App 程式

PS:這裡 C++ 程式碼我用的是 Clion 閱讀,這裡配置比較簡單,將上面生成的 compile_commands.json 檔案複製到 src/flutter 目錄中,然後使用 Clion 開啟專案,indexing 之後便可以跟蹤跳轉

二、Flutter 2 原始碼閱讀

進行原始碼分析之前,先了解一下官方文件中提供的核心架構圖,它也代表著整個 Flutter 架構

90928503-5814-423c-b3de-0886e7e3dda3.png Flutter 的架構主要分成三層:Framework,EngineEmbedder

1)、FrameworkFramework 使用 dart 實現,包括 Material Design 風格的 Widget,Cupertino(針對 iOS)風格的 Widgets,文字/圖片/按鈕等基礎 Widgets,渲染,動畫,手勢等。此部分的核心程式碼是:flutter 倉庫下的 flutter package,以及 sky_engine 倉庫下的 io,async ,ui (dart:ui 庫提供了 Flutter 框架和引擎之間的介面)等 package。其中 dart:ui 庫是對 Engine 中 Skia 庫的 C++ 介面的繫結。向上層提供了 window、text、canvas 等通用的繪製能力,通過 dart:ui 庫就能使用 Dart 程式碼操作 Skia 繪製引擎。所以我們實際上可以通過例項化 dart:ui 包中的類(例如 Canvas、Paint 等)來繪製介面。然而,除了繪製,還要考慮到協調佈局和響應觸控等情況,這一切實現起來都異常麻煩,這也正是 Framework 幫我們做的事。渲染層 Rendering 是在 ::dart:ui 庫之上的第一個抽象層,它為你做了所有繁重的數學工作。為了做到這一點,它使用 RenderObject 物件,該物件是真正繪製到螢幕上的渲染物件。由這些 RenderObject 組成的樹處理真正的佈局和繪製。

2)、EngineEngine 使用 C++ 實現,主要包括:SkiaDartTextSkia 是開源的二維圖形庫,提供了適用於多種軟硬體平臺的通用 API。在安卓上,系統自帶了 Skia,在 iOS 上,則需要 APP 打包 Skia 庫,這會導致 Flutter 開發的 iOS 應用安裝包體積更大。 Dart 執行時則可以以 JIT、JIT Snapshot 或者 AOT 的模式執行 Dart 程式碼。

3)、EmbedderEmbedder 是一個嵌入層,即把 Flutter 嵌入到各個平臺上去,這裡做的主要工作包括渲染 Surface 設定,執行緒設定,以及外掛等。從這裡可以看出,Flutter 的平臺相關層很低,平臺(如 iOS)只是提供一個畫布,剩餘的所有渲染相關的邏輯都在 Flutter 內部,這就使得它具有了很好的跨端一致性。

2、啟動 app 時會在 Application onCreate 方法中建立 FlutterEngineGroup 物件

public void onCreate() {
    super.onCreate();
    // 建立 FlutterEngineGroup 物件
    engineGroup = new FlutterEngineGroup(this);
}
複製程式碼

3、在建立 FlutterEngineGroup 時,使通過該引擎組建立的子引擎共享資源,比單獨通 FlutterEngine 建構函式建立,建立速度的更快、佔用記憶體更少,在建立或重新建立第一個引擎時,行為與通過 FlutterEngine 建構函式建立相同。當建立後續的引擎時,會重新使用現有的引擎中的資源。共享資源會一直保留到最後一個引擎被銷燬。刪除 FlutterEngineGroup 不會使其現有的已建立引擎失效,但它無法再建立更多的 FlutterEngine

//src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java
public FlutterEngineGroup(@NonNull Context context, @Nullable String[] dartVmArgs) {
  FlutterLoader loader = FlutterInjector.instance().flutterLoader();
  if (!loader.initialized()) {
    loader.startInitialization(context.getApplicationContext());
    loader.ensureInitializationComplete(context, dartVmArgs);
  }
}
複製程式碼

4、FlutterLoaderstartInitialization 將載入 Flutter 引擎的本機庫 flutter.so 以啟用後續的 JNI 呼叫。還將查詢解壓打包在 apk 中的 dart 資源,而且方法只會被呼叫一次。該方法具體呼叫步驟:

1)、settings 屬性是否賦值來確定方法是否執行過;

2)、方法必須在主執行緒中執行,否則拋異常退出;

3)、獲取 app 上下文;

4)、VsyncWaiter 是同步幀率相關的操作;

5)、記錄初始化耗時時間;

6)、從 flutter2 開始,初始化配置、初始化資源、載入 flutter.so 動態庫,都放在後臺子執行緒中執行,加快了初始化速度。

//src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java
public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
  //初始化方法只能執行一次
  if (this.settings != null) {
    return;
  }
	//必須在主執行緒上呼叫 startInitialization
  if (Looper.myLooper() != Looper.getMainLooper()) {
    throw new IllegalStateException("startInitialization must be called on the main thread");
  }

  // 獲取 app 的上下文
  final Context appContext = applicationContext.getApplicationContext();

  this.settings = settings;

  initStartTimestampMillis = SystemClock.uptimeMillis();
	//獲取 app 相關資訊
  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);
					//載入 fluter 本地 so 庫
          flutterJNI.loadLibrary();
			
          Executors.newSingleThreadExecutor()
              .execute(
                  new Runnable() {
                    @Override
                    public void run() {
											//預載入 skia 字型庫
                      flutterJNI.prefetchDefaultFontManager();
                    }
                  });

          if (resourceExtractor != null) {
						//等待初始化時的資源初始化完畢後才會向下執行,否則會一直阻塞
            resourceExtractor.waitForCompletion();
          }

          return new InitResult(
              PathUtils.getFilesDir(appContext),
              PathUtils.getCacheDirectory(appContext),
              PathUtils.getDataDirectory(appContext));
        }
      };
  initResultFuture = Executors.newSingleThreadExecutor().submit(initTask);
}
複製程式碼

5、initResources:將 apk 中的資原始檔複製到應用本地檔案中,在 DEBUG 或者在 JIT_RELEASE 模式下安裝 Flutter 資源,主要由 ResourceExtractor 來非同步執行資原始檔的解壓縮操作,最終會將 apk 中 assets 中的 Dart 資源 vm_snapshot_data、isolate_snapshot_data、kernel_blob.bin 檔案安裝到應用目錄 app_flutter 目錄下。

private ResourceExtractor initResources(@NonNull Context applicationContext) {
  ResourceExtractor resourceExtractor = null;
  if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
		//獲取 flutter 資料儲存路徑
    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;
}
複製程式碼

6、在初始化 startInitialization 時,也會呼叫 ensureInitializationComplete 方法確認初始化是否完成,然後裡面會把 so 檔案的地址給到 shellArgs 裡傳入 FlutterJNI,因此我們可以通過修改 flutter 生成的程式碼或者使用 hook 等方式替換 List shellArgsadd 方法,從而改變 so 的路徑,進行熱修復。

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<>();

		//	此處省略具體引數配置程式碼...

    long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;

		// 初始化 JNI
    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);
  }
}
複製程式碼

FlutterJNI 初始化

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");
    }
		//呼叫 JNI 中 flutter 初始化方法
    FlutterJNI.nativeInit(
        context, args, bundlePath, appStoragePath, engineCachesPath, initTimeMillis);
    FlutterJNI.initCalled = true;
  }
複製程式碼

7、在初始化資源之後就開始載入 flutter.so,這個就是 Flutter Engine 原始碼編譯後的產物。當執行時,它被 Android 虛擬機器載入到虛擬記憶體中。(so 是一個標準的 ELF 可執行檔案,主要分為 .data 和 .text 段,分別包含了資料和指令,載入到虛擬記憶體後,指令可以被 CPU 執行) 載入了 flutter.so 之後,最先被執行的是裡面的 JNI_OnLoad 方法 ,會註冊 FlutterMain 、PlatformView、VSyncWaiterjni 方法。

//src/flutter/shell/platform/android/library_loader.cc

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  // 開始進行 Java VM 的初始化,是儲存當前的 Java VM 物件到一個全域性的變數中
  fml::jni::InitJavaVM(vm);

	//把當前的 thread 和 JavaVM 關聯起來
  JNIEnv* env = fml::jni::AttachCurrentThread();
  bool result = false;

  // 註冊 FlutterMain,就是把 Java 層的 native 方法和 C++層的方法關聯起來
  result = flutter::FlutterMain::Register(env);
  FML_CHECK(result);

  // 註冊 PlatformView
  result = flutter::PlatformViewAndroid::Register(env);
  FML_CHECK(result);

  // 註冊 VSyncWaiter.
  result = flutter::VsyncWaiterAndroid::Register(env);
  FML_CHECK(result);

  return JNI_VERSION_1_4;
}
複製程式碼

系統初始化完成之後,會呼叫 NativeInit 這個 native方法,對應的 FlutterMain.cc::Init 方法。這裡初始化主要是根據傳入的引數生成了一個 Settings 物件。

// src/flutter/shell/platform/android/flutter_main.cc
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);

  int64_t init_time_micros = initTimeMillis * 1000;
  settings.engine_start_timestamp =
      std::chrono::microseconds(Dart_TimelineGetMicros() - init_time_micros);

  flutter::DartCallbackCache::SetCachePath(
      fml::jni::JavaStringToString(env, appStoragePath));

  fml::paths::InitializeAndroidCachesPath(
      fml::jni::JavaStringToString(env, engineCachesPath));

  flutter::DartCallbackCache::LoadCacheFromDisk();

  if (!flutter::DartVM::IsRunningPrecompiledCode() && kernelPath) {
    auto application_kernel_path =
        fml::jni::JavaStringToString(env, kernelPath);

    if (fml::IsFile(application_kernel_path)) {
      settings.application_kernel_asset = application_kernel_path;
    }
  }

  settings.task_observer_add = [](intptr_t key, fml::closure callback) {
    fml::MessageLoop::GetCurrent().AddTaskObserver(key, std::move(callback));
  };

  settings.task_observer_remove = [](intptr_t key) {
    fml::MessageLoop::GetCurrent().RemoveTaskObserver(key);
  };

  settings.log_message_callback = [](const std::string& tag,
                                     const std::string& message) {
    __android_log_print(ANDROID_LOG_INFO, tag.c_str(), "%.*s",
                        (int)message.size(), message.c_str());
  };

#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
  auto make_mapping_callback = [](const uint8_t* mapping, size_t size) {
    return [mapping, size]() {
      return std::make_unique<fml::NonOwnedMapping>(mapping, size);
    };
  };

  settings.dart_library_sources_kernel =
      make_mapping_callback(kPlatformStrongDill, kPlatformStrongDillSize);
#endif  
	//建立 Flutter 全域性變數
  g_flutter_main.reset(new FlutterMain(std::move(settings)));
  g_flutter_main->SetupObservatoryUriCallback(env);
}
複製程式碼

args 解析出 Settings 的過程在 flutter_engine/shell/common/switches.cc,這裡最重要的是 snapshot 路徑的構建,構建完成的路徑就是程式初始化拷貝到本地的路徑, 最後生成了一個 FlutterMain 物件儲存在全域性靜態變數中。

if (aot_shared_library_name.size() > 0) {
    for (std::string_view name : aot_shared_library_name) {
      settings.application_library_path.emplace_back(name);
    }
  } else if (snapshot_asset_path.size() > 0) {
    settings.vm_snapshot_data_path =
        fml::paths::JoinPaths({snapshot_asset_path, vm_snapshot_data_filename});
    settings.vm_snapshot_instr_path = fml::paths::JoinPaths(
        {snapshot_asset_path, vm_snapshot_instr_filename});
    settings.isolate_snapshot_data_path = fml::paths::JoinPaths(
        {snapshot_asset_path, isolate_snapshot_data_filename});
    settings.isolate_snapshot_instr_path = fml::paths::JoinPaths(
        {snapshot_asset_path, isolate_snapshot_instr_filename});
  }
複製程式碼

後記

以上主要是 Flutter 2 FlutterEngineGroup 初始化的過程,下一節我們開始學習通過 FlutterEngineGroup建立 FlutterEngine 並繫結的 UI 頁面的流程。

相關文章