Flutter 2 Router 从入门到放弃 - 实现原理与源码分析(一)

公众号「 微医大前端技术 」

周建华: 微医移动端诊疗组, 喜欢看书和运动的 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 页面的流程。

文章分类
Android