Flutter Windows Desktop从Create到Run详解

  • flutter create在create什么?
  • flutter build偷偷在背后做了什么?
  • flutter assemble很重要
  • flutter run
    • 整体项目结构分析
    • flutter windows desktop运行视图
    • flutter windows desktop启动流程
    • flutter多线程模型
    • Dart VM介绍

前言

在文章开始前很有必要对Flutter的整体框架做一个简单的介绍,方便不了解flutter的小伙伴能够更顺畅的阅读后续内容。

anchor text

                                                 Flutter框架图(图片来源于Flutter官网)


上图是flutter的框架的分层设计,从下而上,分别对应

  • Embedder 平台嵌入层,Flutter在每一个平台都包含了一个平台特定的嵌入层,用来与操作系统对接,使得flutter能够使用其操作系统的能力,访问诸如 surface 渲染、辅助功能和输入等服务,管理事件循环队列。并且通过嵌入层、使得上层过滤掉操作系统的差异,使其flutter具备跨平台的前置条件。在windows、linux上嵌入层使用C++实现、Android使用C++和JAVA实现,macOS和ios使用的是Object-C和Object-C++。如果flutter目前的嵌入层还不支持你的操作系统平台,如鸿蒙,你甚至可以写一个鸿蒙的嵌入层,使得flutter支持更多的平台,当然只是举个例子,skia支不支持鸿蒙也不清楚。
  • Engine层,也是Flutter最重要的一层,它主要使用 C++ 编写,提供了 Flutter 核心 API 的底层实现,包括图形(通过 Skia)、文本布局、文件及网络 IO、辅助功能支持、插件架构和 Dart 运行环境及编译环境的工具链。引擎将底层 C++ 代码包装成 Dart 代码,通过 dart:ui 暴露给 Flutter 框架层。
  • Flutter框架层,是面向使用者的一层,该框架提供了以 Dart 语言编写的现代响应式框架。它包括由一系列层组成的一组丰富的平台,布局和基础库。

1. Flutter Create Application命令

create命令介绍

接触flutter避免不了创建一个项目,flutter create命令支持生成指定平台或者指定类型的项目模版生成,如下面的指令

// 最简单的创建一个appliation
flutter create myapplication

// 创建一个指定目录的application
flutter create D:\\MyDir\myapplication

// 创建一个plugin
flutter create --template=plugin myplugin

// 查看帮助
flutter create -h
复制代码

tempalte 选项

flutter 支持plugin、package、application等,template选项的值决定了flutter create命令最终创建出来的项目模版

[app]              (default) Generate a Flutter application.
[module]           Generate a project to add a Flutter module to an existing Android or iOS application.
[package]          Generate a shareable Flutter project containing modular Dart code.
[plugin]           Generate a shareable Flutter project containing an API in Dart code with a
                   platform-specific implementation for Android, for iOS code, or for both.
[skeleton]         Generate a List View / Detail View Flutter application that follows community best
                   practices.
复制代码

platform 选项

flutter支持create ios、android、windows、linux、mac、windows、web项目,如果不指定platform选项,所有系统都将会被创建。

flutter create --platform=windows D:\Code\flutter-code\myapplication
复制代码

flutter windows项目初始结构

image.png

windows 目录

image.png

重点关注windows目录,如上图,它是一个使用CMake构建系统管理的C++项目,看到是一个cmake项目,我们就尝试不借助flutter build,直接使用cmake生成一个visual studio解决方案。

生成的过程中失败了,提示缺少generate_config.cmake,在仔细看了flutter帮我们生成windows模版中,发现少了很多文件,比如内部引用的flutter_windows.h,链接的动态库等等都不知在哪。目前来看flutter build命令的源码或许能够解决我的疑问。

2. Flutter 上层框架源码调式环境搭建

flutter的命令行工具由flutter上层框架提供,使用Dart编写,为了更方便的分析后续flutter提供的各种命令,有必须要搭建一个调式环境。

在搭建调式环境之前,默认你已经自己安装好了Flutter,为了方便快捷,采取直接在安装好的flutter上进行源码调式。

寻找到flutter安装目录

where flutter
复制代码

找到源码路径使用VsCode打开即可

VS Code调式环境搭建

打开VSCode的launch.json文件,全局搜索packages\\flutter_tools(flutter的命令源码就是在这个项目)。

flutter源码修改支持BUILD_ROOT配置

file: flutter_command.dart: run (1151) context.run函数首行

return context.run<void>(
      name: 'command',
      overrides: <Type, Generator>{FlutterCommand: () => this},
      body: () async {
        // 下面的四行就是需要添加的代码
        final Map<String, String> env = Platform.environment;
        if (env.containsKey("BUILD_ROOT")) {
          globals.fs.currentDirectory = env["BUILD_ROOT"] ;
        }

				// ......
      },
    )
复制代码

当然你也可以选择修改vscode的项目当前工作目录cwd配置来完成。

launch.json配置

{
	"name": "flutter_tools",
	"cwd": "packages\\flutter_tools",
	"request": "launch",
	"type": "dart",
	"program": "lib\\executable.dart",
	"args": [
		"build", 
	],
	"flutterMode": "debug",
	"env": {
		"FLUTTER_ROOT": "你的flutter根目录",
		"BUILD_ROOT": "你需要build的项目目录"
	}
}
复制代码

后续需要进行其他命令调式,只需要修改args参数即可。

3. Flutter Build命令

前面查看flutter创建的windows项目,产生了几个疑问,cmake项目中缺少的文件是在什么时候补全的,通过flutter build源码调式,flutter build命令主要干了下面几件事情。

  1. windows\flutter\ephemeral目录下创建一个generated_config.cmake,里面记录了一些flutter和构建项目的环境变量
  2. 新建了一个.plugin_symlinks文件
  3. 更新CMake文件
  4. 执行cmake config命令配置C++项目
  5. 执行cmake build命令构建C++项目
  6. 执行cmake install命令

通过上述步骤还是没有解决我的全部疑问,只是找到了generated_config.cmake的生成,如flutter engine的动态库什么时候拷贝到运行目录,以及不完整的C++项目依赖源文件什么时候拷贝的,目前看来玄机要从cmake文件中寻找了。

CMake自定义命令

file: windows/flutter/CMakeLists.txt

add_custom_command(
  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
    ${CPP_WRAPPER_SOURCES_APP}
    ${PHONY_OUTPUT}
  COMMAND ${CMAKE_COMMAND} -E env
    ${FLUTTER_TOOL_ENVIRONMENT}
    "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
      windows-x64 $<CONFIG>
  VERBATIM
)
复制代码

解释:上面的CMake代码会在cmake build时执行,会调用${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat脚本,OUTPUT是执行完成之后需要输出的内容,如${FLUTTER_LIBRARY}就是flutter_windows.dll,其中有一行COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIROMENT},其中${FLUTTER_TOOL_ENVIROMENT}定义如下, 这些环境变量会被传入到tool_backend.bat。

file: windows\flutter\ephermeral\generated_cofig.cmake

# Generated code do not commit.
file(TO_CMAKE_PATH "C:\\Users\\Administrator\\Documents\\flutter_windows_2.5.3-stable\\flutter" FLUTTER_ROOT)
file(TO_CMAKE_PATH "D:\\Code\\flutter-code\\myapplication" PROJECT_DIR)

# Environment variables to pass to tool_backend.sh
list(APPEND FLUTTER_TOOL_ENVIRONMENT
  "FLUTTER_ROOT=C:\\Users\\Administrator\\Documents\\flutter_windows_2.5.3-stable\\flutter"
  "PROJECT_DIR=D:\\Code\\flutter-code\\myapplication"
  "FLUTTER_ROOT=C:\\Users\\Administrator\\Documents\\flutter_windows_2.5.3-stable\\flutter"
  "FLUTTER_EPHEMERAL_DIR=D:\\Code\\flutter-code\\myapplication\\windows\\flutter\\ephemeral"
  "PROJECT_DIR=D:\\Code\\flutter-code\\myapplication"
  "FLUTTER_TARGET=D:\\Code\\flutter-code\\myapplication\\lib\\main.dart"
  "DART_DEFINES=RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ=="
  "DART_OBFUSCATION=false"
  "TRACK_WIDGET_CREATION=true"
  "TREE_SHAKE_ICONS=false"
  "PACKAGE_CONFIG=D:\\Code\\flutter-code\\myapplication\\.dart_tool\\package_config.json"
)
复制代码

tool_backend.bat

file: FLUTTER_ROOT\packages\fluttertools\bin\tool_backend.bat

tool_backend.bat执行了tool_backend.dart代码,然后tool_backend.dart代码运行flutter assemble命令。

cmake install

install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
  COMPONENT Runtime)

install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
  COMPONENT Runtime)

install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
  DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)

message("AOT_LIBRARY : ${AOT_LIBRARY}")

# Install the AOT library on non-Debug builds only.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
  CONFIGURATIONS Profile;Release
  COMPONENT Runtime)
复制代码

解释:cmake install命令会在cmake build完成之后执行,会复制一些诸如,AOT编译产物、资源文件等到可执行文件目录。

4. Flutter Assemble命令

在前面我们知道了cmake在build过程中会执行flutter assemble命令,接下来看flutter assembble做了什么。

首先还是调式环境搭建, flutter assemble launch.json配置如下:

{
	"name": "flutter_tools",
	"cwd": "packages\\flutter_tools",
	"request": "launch",
	"type": "dart",
	"program": "lib\\executable.dart",
	"args": [
		"assemble", 
		"--no-version-check",
		"--output=build",
		"-dTargetPlatform=windows-x64",
		"-dTrackWidgetCreation=true",
		"-dBuildMode=release",
		"-dTargetFile=lib\\main.dart",
		"-dTreeShakeIcons=true",
		"-dDartObfuscation=false",
		"release_bundle_windows_assets"
	],
	"flutterMode": "debug",
	"env": {
		"FLUTTER_ROOT": "你的flutter根目录",
		"BUILD_ROOT": "你的项目根目录"
	}
}
复制代码

flutter assemlbe整体运作流程

Screen Shot 2022-03-13 at 10.59.40.png

UnpackWindows

file: windows.dart : UnpackWindows

UnpackWindows会将之前cmkae build需要的文件(.h, .cc, .dll)拷贝到指定目录,具体拷贝了哪些文件可以在你的应用程序项目根目录unpack_windows.stamp文件查看,举个例子,会将FLUTTER_ROOT/bin/cache/artifacts/engine/windows-x64-release/flutter_windows.dll拷贝到project_dir/windows/flutter/ephemeral

Dart编译

file: common.dart: KernelSnapshot file: compile.dart: KernelCompiler file: common.dart: AOTElfDart

KernalSnapshot和AOTElfBase负责dart代码的编译工作,dart编译根据不同的编译模式会采用不同的编译形式,在debbug模式下就会跳过AOTElfBase步骤。

Dart Release和Debug编译

  • Dart在Debbug模式下采用JIT(Just In Time)模式进行编译,生成一个中间语言(app.dill),使得能够有效的实现热重载
  • Dart在Release模型下采用AOT(Ahead Of Time)模式进行编译,生成对应平台的机器代码(app.so),使得执行效率提高,不过在AOT模式的编译也需要先进行JIT编译

Windows 编译产物

Screen Shot 2022-03-13 at 11.44.02.png Flutter windows的Release产物分为了两部分,第一部分是由dart代码产生的,dart代码通过frontend_server前端编译器将dart代码转换成AST抽象语法树,并且产生app.dill的dart内核产物,在release模式下在使用gen_snapshot对中间代码(app.dill)进行一系列优化之后再根据不同平台生成不同的二进制文件,在windows、linux、android上二进制文件采用linux elf二进制文件格式。第二部分是由flutter引擎代码产生的,这个通常是已经编译好的,最后由flutter create创建出来的C++项目模版进行编译生成一个可执行二进制文件。

Dart编译完的产物可以在当前项目根目录的./dart_tool/flutter_build目录查看

WindowsAotBundle & BundleWindowsAssests

file: windows.dart: WindowsAotBundle file: windows.dart: BundleWindowsAssets

WindowsAotBundle和BundleWindowsAssets仅仅只是对之前的生成产物进行拷贝操作,如Dart编译出来的app.dill(这个文件会被重命名为kenerl_blob.bin),app.so,assets等。

assets可以在当前项目根目录的build/flutter_assets查看(kenerl_blob.bin也在这个目录中) app.so可以在当前项目根目录的build/windows查看

小总结

  1. flutter create创建一个flutter项目模版,其中包含了一个C++项目,但其项目并不完整
  2. flutter build命令首先生成cmake_generated_.cmake文件,使其cmake config可以正常执行,之后通过执行cmake build命令进行C++项目的构建,由于C++项目的不完整,所以在cmake编译项目之前利用cmkae命令调用了tool_backend.bat脚本
  3. tool_backend.bat脚本执行tool_backend.dart代码,tool_backend.dart代码执行flutter assemble命令
  4. flutter assemble命令首先会生成cmake build所需要的一些C++文件和库,之后编译Dart代码并将编译产物拷贝到对应位置
  5. flutter build的cmake build执行完tool_backend.bat之后开始进行C++代码编译,最终生成application.exe
  6. flutter build又利用cmake install命令将application.exe运行时所依赖的资源拷贝到运行目录

5. Flutter Run

在执行完flutter build之后就可以在当前项目根目录的build/windows/runner/Release目录下看到最终flutter项目所生成的产物了,这个目录可以直接进行打包,不过flutter的C++项目的编译模式是MD,所以最终打包时还需要将windows C++所需的运行时环境拷贝到Release目录。

Flutter release目录

│  flutter_windows.dll 
│  myapplication.exe
│
└─data
    │  app.so
    │  icudtl.dat
    │
    └─flutter_assets
        │  AssetManifest.json
        │  FontManifest.json
        │  NOTICES.Z
        │
        ├─fonts
        │      MaterialIcons-Regular.otf
        │
        └─packages
            └─cupertino_icons
                └─assets
                        CupertinoIcons.ttf
复制代码
  • app.so是Dart AOT产物
  • icudtl.dat是国际化文件
  • flutter_assets里面有一些fonts、icon等

Flutter Windows Application(C++项目)调式环境搭建

由于后面需要对flutter windows application的运行视图和启动流程进行分析,所以避免不了进行源码调式,在这里给出一种简单的环境搭建方式,当然如果你想要细致且更方便的分析flutter engine源码也可以自己重新编译flutter engine, flutter引擎官方源码编译教程

1. 下载flutter engine源码

git clone https://github.com/flutter/engine.git
复制代码

其实调式整个flutter windows项目的启动流程和运行视图,直接下载flutter源码就足够了,当然如果你还希望进行skia或者dart vm代码里面调式,需要下载google的depot_tools, 然后执行glient sync更新第三方库代码。

2. 切换到指定本地flutter版本对应engine分支

# 查看本地flutter使用的engine
flutter --version

# 切换到revision版本
git checkout your_revision
复制代码

3. 添加pdb符号文件

在vs的工具→选项→调式→符号添加 your_project_path/windows/flutter/ephemeral路径。

4. 运行程序、确定符号已经加载 (myapplication.exe)

image.png 之后就可以在visual studio中打开flutter engine源码进行调式了。

Flutter Windows Application运行视图

Screen Shot 2022-03-13 at 17.59.23.png

Flutter启动流程

配合Flutter Windows Application运行视图观看

  • 首先Flutter Applicationn由一个FlutterWindow组成,FlutterWindow中包含了一个Win32的主窗口,在main函数中会创建一个FlutterWindow,并且创建一个Win32主窗口显示,FlutterWindow监听了win32的窗口创建事件,在windows窗口创建回调中初始化了FlutterViewController对象
  • FlutterViewController主要是将引擎渲染结果显示到对应的FlutterView上,它创建FlutterView和FlutterEngine,FlutterView和FlutterEngine通过Embedder API,也就是flutter_windows.h提供的C API调用Flutter嵌入层,告知嵌入层进行FlutterWindowsView和FlutterWindowsEngine的创建,而FlutterWindowsView也就是FlutterView的真实对象其实也就是一个windows窗口,在win32的主窗口创建事件中将其设置为了主窗口的子窗口
  • FlutterViewController在创建的时候还会通过嵌入层引擎创建ThreadHost和Shell,Shell非常重要,是flutter中的一个中枢神经,负责引擎的创建、Root Isolate创建、DartVM 启动、PlatformView创建等,创建完成Shell后,会启动Shell、启动Shell会带动启动Flutter引擎、引擎启动又会创建DartVM然后经过层层调用走到Dart _runMainZoned函数,_runMainZeoned再调用dart main函数

Flutter多线程模型

在嵌入层引擎创建ThreadHost,ThreadHost会创建四个线程,分别是Platform线程(这个不是由flutter创建,是直接接管的main线程)、UI Thread、Rasterizer(GPU)线程、IO Thread,并且这四个线程维护着四个TaskRunner,TaskRunner,TaskRunner的原理和chromium里面的线程通信很像,都是内部拥有一个MessageLoop用来循环等待并处理消息队列里面的任务。每一个TaskRunner对应着自己的职责,如下:

  • Platfom TaskRunner:运行在主线程,主要负责处理用户输入时间、如键盘、鼠标等事件的监听,以及处理外部和引擎层的通信
  • UI TaskRunner:运行在ui线程,主要负责引擎的渲染工作
  • Rasterizer TaskRunner: 主要负责与GPU相关的工作
  • IO TaskRunner:IO主要负责执行耗时操作,如读取本地图片将其解压转换成CPU能够处理的格式并Post GPU TaskRunner

Dart虚拟机

Dart虚拟机和Root Isolate会伴随着Flutter整个引擎的启动而被创建。

Isolate是什么,它是Dart为了实现并发而发明出来的,Dart语言本身是一个单线程语言,所有代码都是顺序执行的,Dart提供了一种机制Isolate,使得Dart可以支持并发编程,Isolate内部采用线程实现,但是表现形式和进程特别相似。

DartVM自身也拥有自己的Isolate,完全由虚拟机自己管理的,Flutter引擎也无法直接访问。Dart的UI相关操作,是由Root Isolate通过Dart的C++调用,或者是发送消息通知的方式,将UI渲染相关的任务提交到UI TaskRunner执行,这样就可以跟Flutter引擎相关模块进行交互。

  • Isolate内部实现里面的堆是运该isolate中代码分配的所有对象的GC管理的内存存储
  • Isolate堆能引用vm isolate堆中的对象,但vm isolate不能引用isolate堆
  • Isolate和Isolate之间是完全隔离的,彼此之间不能相互引用
  • 每一个Isolate都有一个负责执行dart代码的Mutator Thread,还有一个Helper Thread用来处理虚拟机内部任务(如GC、JIT)等,Isolate可以在虚拟机中存在多个,但是像进程一样,它们彼此之间内存是不共享的,不能够直接访问,只能够通过Dart特有的Port通信。

参考链接

Flutter architectural overview | Flutter

The Engine architecture · flutter/flutter Wiki · GitHub

The flutter tool · flutter/flutter Wiki · GitHub

Flutter 跨平台演进及架构开篇 - Gityuan博客 | 袁辉辉的技术博客

Supongo que te gusta

Origin juejin.im/post/7076048326692438029
Recomendado
Clasificación