Flutter学习系列(7)— Flutter Engine初始化(上)

前面三篇文章我们通过Flutter Demo程序,从程序的初始化开始,到MainActivity的初始化(FlutterActivity),然后到FlutterView的初始化, 最后到FlutterView运行一个Flutter Bundle。整个过程中我们都是关注在Java层的代码。 但是通过FlutterJNI类我们知道,很多东西都是在C++层也就是Flutter Engine中实现的,  也就是Flutter.jar中的flutter.so文件。 所以这一篇从FlutterEngine的角度来看看初始化。

 

 

一 准备Flutter Engine源码

 

前面介绍Flutter.jar的时候有简单介绍一下下载Flutter Engine的源码。 源码地址:https://github.com/flutter/engine

建议使用git命令直接下载master分支

git clone https://github.com/flutter/engine.git

然后根据你Flutter SDK的版本来切换到对应的commit

➜  io git:(3757390fa) ✗ flutter --version
Flutter 1.2.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 8661d8aecd (7 weeks ago) • 2019-02-14 19:19:53 -0800
Engine • revision 3757390fa4
Tools • Dart 2.1.2 (build 2.1.2-dev.0.0 0a7dcf17eb)

切换到SDK Engine版本对应的源码,上面就是Flutter SDK 1.2.1对应的Engine的commit ID

flutter_engine git:(master) ✗ git checkout 3757390fa4
HEAD is now at 3757390fa Roll src/third_party/dart ecd7a88606..0a7dcf17eb (4 commits)

看源码工具

 

如果你是在Windows下查看源码,我推荐使用Source Insight ,它可以去解析代码中类之间的调用关系,可以很方便的定位到类的定义和查找引用,以前看Android源码和Framework开发都是使用这个。刚看了下2018年更新到了4.X版本。可惜只能Windows版本。

如果你使用Linux或者Mac那就还是使用VS Code 来查看源码吧

不过用VSCode查看Flutter Engine源码,无法进行代码间的跳转,因为VSCode不知道类之间的关系,这个时候可以使用ctags来生成。Mac上先安装ctags

brew install ctags

其实Mac中预装了ctags,但是是xcode自己版本,命令和标准的有些不一样,所以需要重新安装并设置alias替换系统的

alias ctags="brew –prefix/bin/ctags" 

最后可以把alias保存到你的环境变量

alias ctags >> ~/.bashrc

如果你使用了zsh或其他sh,保存到对应的rc文件中,建议是自己新建一个~/.profile文件来放环境变量信息,在不同的sh配置中引用一下就好了。

 

备注: flutter上推荐使用cquery,但是需要编译flutter engine的环境,直接从个git上拉的代码是无法编译的。

 

 

VSCode相关插件

 

在VSCode中打开终端,使用下面命令生成tags文件,tags文件其实就是源码中class的索引,有了这些索引,有可以建立起跳转的关系。 ctags命令还有一些参数,可以自己研究。当然还有gtags、cscope等多个类是工具,可以自行选择。

➜  flutter_engine git:(3757390fa) ✗ ctags -R -f .tags

要在VSCode中利这个tags文件进行跳转需要安装插件,我使用的是CTags Support

安装之后,用CMD+鼠标左键就可以跳转到要查看的类的定义,当然也可以使用右键菜单或者是快捷接盘查找定义和引用。 但是VSCode有个问题,目前不支持鼠标的前进和后退,只能用键盘快捷键,这个实在是很麻烦,所以介绍一个插件: Code Navigation 这个插件会在VSCode底部状态栏上增加2个前进后退按钮,类似IDE里面的。虽然不能直接使用鼠标按键前进后退,用这个也算方便一些了。

除此之外推荐安装以下插件

  • C++ Intellisense
  • C/C++
  • Visual Studio IntelliCode – Preview
  • Dart
  • Flutter
  • Java Dependency Viewer
  • Java Extension Pack

 

已知问题

导入源码后有些问题:

  1. 无法跳转到Android 相关源码, 比如Activity,因为VSCode找不到Android源码位置,而且目前也无法指定(或者是我也不知道)
  2. Flutter Engine C++代码中,无法通过include的.h文件直接打开对应.h文件,不过应该可以解决,在.vscode目录下的c_cpp_properties.json文件中设置include path (我没试过)

 

 

语言问题

Engine使用C++ 11标准语法,所以和以前C++ 98标准差别还是比较大,需要先简单了解一下c++ 11的语法,虽然这些语法其他语言都有了,但是书写方式上可能有一些差异。所以可以先了解下智能指针、拉姆达表达式、 JNI调用

 

 

 

二 Flutter Engine初始化

 

整个Flutter Engine源码包含了非常多的东西,DartVM、Render、Channel、Event等等,所以我们只能从相关的点去看,所以可能会有一些遗漏和不清楚的地方。就好比从Engine初始化来说,可以做的事情非常多,但是我们主要关注和Java层有关联的一些操作。相关的代码主要在flutter_engine/shell/platform/android/io/flutter目录下。前面几篇我们从App启动到页面显示,关注的是Java部分的初始化。下面介绍一下这个过程中和Engine相关的操作。

 

 

1. 启动过程的Native初始化

 

下面是启动到创建FlutterView的启动流程图,我们主要关注其中Navite初始化的分布

 

A. 加载Flutter Engine

 

FlutterApplication启动时使用了Java层的FlutterMain来进行初始化

    public static void startInitialization(Context applicationContext, Settings settings) {
        sSettings = settings;

        long initStartTimestampMillis = SystemClock.uptimeMillis();
        initConfig(applicationContext);
        initAot(applicationContext);
        initResources(applicationContext);
        System.loadLibrary("flutter");

        long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
        nativeRecordStartTimestamp(initTimeMillis);
    }

这里在初始化Flutter App资源之后加载了flutter.so,这个就是Flutter Engine源码编译后的产物。 在我们编译Flutter App时,它存在Flutter SDK的flutter.jar中,当生产APK之后,它存在于APK的lib目录下。而当运行时,它被Android虚拟机加载到虚拟内存中。(so是一个标准的ELF可执行文件,主要分为.data和.text段,分别包含了数据和指令,加载到虚拟内存后,指令可以被CPU执行)

 

下面是从我Android VM源码笔记中截取的关于System.loadLibrary是实现的描述:

Java程序在调用JNI时会先执行System.loadLibrary来加载动态库,这个方法反映到VM是调用Runtime_nativeLoad

  1. SetLdLibraryPath library的路径设置到全局的path中, 调用的是android linker中得android_update_LD_LIBRARY_PATH
  2. 调用JavaVMExt的LoadNativeLibrary
    1. 根据path从一个ShareLibrary 数组libraries_中获取一个ShareLibrary对象
    2. 如果获取到说明可能已经加载,检查这个lib是否已经被加载, 如果加载library->GetClassLoader()是存在的
    3. 如果不存在调用dlopen 打开这个lib,获得lib得地址handle
    4. 如果不存在 创建SharedLibrary(env, self, path, handle, class_loader))
    5. ShareLibrary加入到libraries_
    6. 调用dlsym(handle, “JNI_OnLoad”);查找lib中的JNI_OnLoad方法,如果找到就调用这个方法
    7. 最后返回结果

从上面我们知道,当我们加载了flutter.so之后,最先被执行的是里面的JNI_OnLoad方法,所以我们去Engine的源码里找一下,发现在 flutter_engine/shell/platform/android/library_loader.cc

// This is called by the VM when the shared library is first loaded.
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  // Initialize the Java VM.
  fml::jni::InitJavaVM(vm);

  JNIEnv* env = fml::jni::AttachCurrentThread();
  bool result = false;

  // Register FlutterMain.
  result = shell::FlutterMain::Register(env);
  FML_CHECK(result);

  // Register PlatformView
  result = shell::PlatformViewAndroid::Register(env);
  FML_CHECK(result);

  // Register VSyncWaiter.
  result = shell::VsyncWaiterAndroid::Register(env);
  FML_CHECK(result);

  return JNI_VERSION_1_4;
}

一开始进行了Java VM的初始化,其实保存一下当前的Java VM对象到一个全局的变量中, JavaVM的初始化在zygote进程中已经进行了,每个Android进程都是fork出来的。

static JavaVM* g_jvm = nullptr;

void InitJavaVM(JavaVM* vm) {
  FML_DCHECK(g_jvm == nullptr);
  g_jvm = vm;
}

一个进程中只有一个JavaVM对象,如果要在线程中访问JavaVM,就需要把当前的thread和JavaVM关联起来。所以调用AttachCurrentThread 我们可以得到一个JNIEnv对象, 每个线程都有一个。JNIEnv定义了很多和JNI调用相关的方法。

JNIEnv* AttachCurrentThread() {
  FML_DCHECK(g_jvm != nullptr)
      << "Trying to attach to current thread without calling InitJavaVM first.";
  JNIEnv* env = nullptr;
  jint ret = g_jvm->AttachCurrentThread(&env, nullptr);
  FML_DCHECK(JNI_OK == ret);
  return env;
}

最后调用了Shell下的3个方法,这里我们先主要FlutterMain,这个是C++层的代码,看起来和我们Java层的FlutterMain很有关联。

bool FlutterMain::Register(JNIEnv* env) {
  static const JNINativeMethod methods[] = {
      {
          .name = "nativeInit",
          .signature = "(Landroid/content/Context;[Ljava/lang/String;Ljava/"
                       "lang/String;Ljava/lang/String;Ljava/lang/String;)V",
          .fnPtr = reinterpret_cast<void*>(&Init),
      },
      {
          .name = "nativeRecordStartTimestamp",
          .signature = "(J)V",
          .fnPtr = reinterpret_cast<void*>(&RecordStartTimestamp),
      },
  };

  jclass clazz = env->FindClass("io/flutter/view/FlutterMain");

  if (clazz == nullptr) {
    return false;
  }

  return env->RegisterNatives(clazz, methods, arraysize(methods)) == 0;
}

写过JNI的话应该很熟悉上面的代码,就是把Java层的native方法和C++层的方法关联起来。这里是通过env->RegisterNatives方法把Java层的FlutterMain的方法和C++层的关联起来。(前面说过JNI_OnLoad不是必须的,如果没有进行native方法的注册,JavaVM会默认按照java方法的package name + method name + params 来映射c++方法,如果找不到就会报错 )

另外两个的作用是一样的,也是注册对应的native方法,具体代码在:

flutter_engine/shell/platform/android/platform_view_android.h

flutter_engine/shell/platform/android/vsync_waiter_android.h

 

 

B. 第一个native方法

 

Java层的FlutterMain里执行的第一个native方式是记录初始化的时间,根据上面的注册关系,它对应C++层的FlutterMain::RecordStartTimestamp方法

static void RecordStartTimestamp(JNIEnv* env,
                                 jclass jcaller,
                                 jlong initTimeMillis) {
  int64_t initTimeMicros =
      static_cast<int64_t>(initTimeMillis) * static_cast<int64_t>(1000);
  blink::engine_main_enter_ts = Dart_TimelineGetMicros() - initTimeMicros;
}

这里把Java层传入的毫秒转换为微妙,然后调用了Dart_TimelineGetMicros()方法,Flutter Engine中并没有这个方法,看起来应该是c++调用了dart的方法,而且引用的头源文件dart_tools_api.h也不在Flutter Engine源码中,而在Dart源码中,所以这里是获取了Dart的时间,然后减去初始化用的时间,得到了Engine的启动时间。我们回过头在看下注释

 // We record the initialization time using SystemClock because at the start of the
 // initialization we have not yet loaded the native library to call into dart_tools_api.h.
 // To get Timeline timestamp of the start of initialization we simply subtract the delta
 // from the Timeline timestamp at the current moment (the assumption is that the overhead
 // of the JNI call is negligible).
 long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
 nativeRecordStartTimestamp(initTimeMillis);

因为在启动的时候我们还没有加载flutter.so,所以没办法使用dart来获取时间。

 

 

C. NativeInit

 

当FlutterApplication初始化完成后,MainActivity(FlutterActivity)就会被唤起。也就是上图的第12步,这里调用了NativeInit这个native方法,对应的FlutterMain.cc::Init方法。

void FlutterMain::Init(JNIEnv* env,
                       jclass clazz,
                       jobject context,
                       jobjectArray jargs,
                       jstring bundlePath,
                       jstring appStoragePath,
                       jstring engineCachesPath) {
  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);

  settings.assets_path = fml::jni::JavaStringToString(env, bundlePath);

  // Restore the callback cache.
  // TODO(chinmaygarde): Route all cache file access through FML and remove this
  // setter.
  blink::DartCallbackCache::SetCachePath(
      fml::jni::JavaStringToString(env, appStoragePath));

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

  blink::DartCallbackCache::LoadCacheFromDisk();

  if (!blink::DartVM::IsRunningPrecompiledCode()) {
    auto application_kernel_path =
        fml::paths::JoinPaths({settings.assets_path, "kernel_blob.bin"});

    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);
  };
  g_flutter_main.reset(new FlutterMain(std::move(settings)));
}

这个初始化主要是根据传入的参数生成了一个Settings对象,Demo传入的args如下

从args解析出Settings的过程在 flutter_engine/shell/common/switches.cc, 这里关注几个重要的snapshot路径的构建

 if (aot_shared_library_path.size() > 0) {
    settings.application_library_path = aot_shared_library_path;
  } else if (aot_snapshot_path.size() > 0) {
    settings.vm_snapshot_data_path = fml::paths::JoinPaths(
        {aot_snapshot_path, aot_vm_snapshot_data_filename});
    settings.vm_snapshot_instr_path = fml::paths::JoinPaths(
        {aot_snapshot_path, aot_vm_snapshot_instr_filename});
    settings.isolate_snapshot_data_path = fml::paths::JoinPaths(
        {aot_snapshot_path, aot_isolate_snapshot_data_filename});
    settings.isolate_snapshot_instr_path = fml::paths::JoinPaths(
        {aot_snapshot_path, aot_isolate_snapshot_instr_filename});
  }

构建完成的路径就是进程初始化拷贝到本地的路径。(DEBUG下的kernel_blob.bin好像没有设置?

然后加载了Dart相关的cache文件(目前不清楚具体作用),最后生成了一个FlutterMain对象保存在了全局静态变量中。

 

 

 

2. FlutterView中的Native操作

 

FlutterView是真正运行Flutter App的地方,前面提到FlutterNativeView通过FlutterJNI负责和engine交互,而FlutterView负责和Java层交互。下面图是FlutterView初始化流程图的前一部分。

从图中我们可以看到FlutterJNI调用的是native方法在platform_view_android_jni.cc文件中, 这个文件实现了FlutterJNI方法中全部的native的方法。 之前我们在FlutterJNI方法中还看到setRenderSurface、setPlatformMessageHandler、addEngineLifecycleListener这三个方法并没有被Java层调用,其实是被native调用了,调用代码也在这个文件中。

 

nativeAttach

 

public void attachToNative(boolean isBackgroundView) {
  ensureNotAttachedToNative();
  nativePlatformViewId = nativeAttach(this, isBackgroundView);
}

FlutterJNI调用这个native方法和native关联,返回一个int值表示platformViewId,目前我们并不清楚什么是PlatformView。 这个方法实际调用了下面方法

static jlong AttachJNI(JNIEnv* env,
                       jclass clazz,
                       jobject flutterJNI,
                       jboolean is_background_view) {
  fml::jni::JavaObjectWeakGlobalRef java_object(env, flutterJNI);
  auto shell_holder = std::make_unique<AndroidShellHolder>(
      FlutterMain::Get().GetSettings(), java_object, is_background_view);
  if (shell_holder->IsValid()) {
    return reinterpret_cast<jlong>(shell_holder.release());
  } else {
    return 0;
  }
}

这一段代码短小精悍。

  1. 把传入的flutterJNI保存到java_object,这里传入的是JAVA层的FlutterJNI对象。(在Java和C++交互中这种操作非常常见,C++保存Java对象的地址或者Java层保存C++对象的地址,这样就饿可以方便的使用)
  2. 创建了一个AndroidShellHolder 对象
  3. 把创建的AndroidShellHolder对象的地址返回给了Java层,所以FlutterJNI中的nativePlatformViewId实际上是native层对象的地址。

 

 

AndroidShellHolder

 

这个类从名字看想是一个holder,为什么叫Android Shell Holder? 因为无论对于Android还是IOS,Engine部分的功能是完全一样的(Flutter架构图),但是和上层App交互以及和低层系统交互是不同的,对于低层系统有一层Embedder,对上层也有一层叫Shell,所以整个flutter.jar对应的源码目录也叫做shell,它负责和不同平台进行上层的交互。

 private:
  const blink::Settings settings_;
  const fml::jni::JavaObjectWeakGlobalRef java_object_;
  fml::WeakPtr<PlatformViewAndroid> platform_view_;
  ThreadHost thread_host_;
  std::unique_ptr<Shell> shell_;
  bool is_valid_ = false;
  pthread_key_t thread_destruct_key_;

既然叫Holder,先看下成员变量,其中有两个很关键的对象,PlatformViewAndroid和Shell对象。整个构造函数内容很多,都是围绕创建上面这些成员变量。只介绍关键的

 

 

ThreadHost

  if (is_background_view) {
    thread_host_ = {thread_label, ThreadHost::Type::UI};
  } else {
    thread_host_ = {thread_label, ThreadHost::Type::UI | ThreadHost::Type::GPU |
                                      ThreadHost::Type::IO};
  }

is_background_view终于被用到了,表示FlutterView是不是后台的View吗,所谓后台的View就是不用显示的。这里创建了ThreadHost对象。

struct ThreadHost {
  enum Type {
    Platform = 1 << 0,
    UI = 1 << 1,
    GPU = 1 << 2,
    IO = 1 << 3,
  };

  std::unique_ptr<fml::Thread> platform_thread;
  std::unique_ptr<fml::Thread> ui_thread;
  std::unique_ptr<fml::Thread> gpu_thread;
  std::unique_ptr<fml::Thread> io_thread;

};

看一下定义,里面包含了4个Thread,从名字就能看出每个线程的作用。Flutter的github上介绍了这4个线程,这里就不在复制一遍了: The Engine architecture

当前初始化的线程是Android 的UI主线程,所以被作为了platform线程,并且确保建立了了消息循环(实际创建了一个MessageLoop对象),所以Task是从MessageLoop中获取的

 fml::MessageLoop::EnsureInitializedForCurrentThread();
  fml::RefPtr<fml::TaskRunner> platform_runner =
      fml::MessageLoop::GetCurrent().GetTaskRunner();

而其他3个线程的Task有所不同:

if (is_background_view) {
    auto single_task_runner = thread_host_.ui_thread->GetTaskRunner();
    gpu_runner = single_task_runner;
    ui_runner = single_task_runner;
    io_runner = single_task_runner;
  } else {
    gpu_runner = thread_host_.gpu_thread->GetTaskRunner();
    ui_runner = thread_host_.ui_thread->GetTaskRunner();
    io_runner = thread_host_.io_thread->GetTaskRunner();
  }

 

Shell

 

上面的流程图第13部是创建Shell对象。

shell_ =
      Shell::Create(task_runners,             // task runners
                    settings_,                // settings
                    on_create_platform_view,  // platform view create callback
                    on_create_rasterizer      // rasterizer create callback
      );

创建Shell对象时传入了4个参数,其中on_create_platform_viewon_create_rasterizer 是在创建Shell时回调AndroidShellHolder的方法来创建PlatformView和Rasterizer。因为创建这2个对象需要先创建Shell,所以使用了回调的方式。

  fml::WeakPtr<PlatformViewAndroid> weak_platform_view;
  Shell::CreateCallback<PlatformView> on_create_platform_view =
      [is_background_view, java_object, &weak_platform_view](Shell& shell) {
        std::unique_ptr<PlatformViewAndroid> platform_view_android;
        if (is_background_view) {
          platform_view_android = std::make_unique<PlatformViewAndroid>(
              shell,                   // delegate
              shell.GetTaskRunners(),  // task runners
              java_object              // java object handle for JNI interop
          );

        } else {
          platform_view_android = std::make_unique<PlatformViewAndroid>(
              shell,                   // delegate
              shell.GetTaskRunners(),  // task runners
              java_object,             // java object handle for JNI interop
              shell.GetSettings()
                  .enable_software_rendering  // use software rendering
          );
        }
        weak_platform_view = platform_view_android->GetWeakPtr();
        return platform_view_android;
      };

  Shell::CreateCallback<Rasterizer> on_create_rasterizer = [](Shell& shell) {
    return std::make_unique<Rasterizer>(shell.GetTaskRunners());
  };

这里实际上定义的是lamda表达式,主要关注PlatformViewAndroid, 会根据是否后台View来创建不同的PlatformView。

 

 

 

三 总结

 

这一篇主要分析了Android启动过程中用到的native调用,以及创建FlutterView时engine中的实现。我们大致可以有以下概念:

  1. Flutter Engine中有4个线程,负责不同的工作。
  2. 在Engine中有一个PlatformView和FlutterView对应。
  3. AndroidShellHolder中包含了PlatformView和Shell。
  4. Shell是负责App和Flutter Framework直接交互的。所以IOS和Android 是分别实现的。
  5. 看起来一个FlutterView就对应一个AndroidShellHolder,所以如果开多个包含FlutterView的Activity会有多个。

因为Shell::Create的内容很多,所以下一篇文章单独介绍这一块的内容。

0

如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

发表评论

电子邮件地址不会被公开。 必填项已用*标注