Flutter 引擎编译、运行与调试

介绍 Flutter 引擎编译、运行与调试的操作步骤。

编译

操作步骤

  • 安装 depot_tools添加到环境变量gclient 来自 depot_tools 工具
  • fork flutter/engine (注意配置 ssh 访问)
  • 创建空的 engine 目录并在目录中创建 .gclient 配置文件
  • engine 目录中执行 gclient sync (它会 git clone 必要的项目及其依赖)
1
2
3
4
5
6
7
8
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH
mkdir engine
cd engine
touch .gclient

# edit .gclient
gclient sync

.gitclient 配置如下:

1
2
3
4
5
6
7
8
9
10
solutions = [
{
"managed": False,
"name": "src/flutter",
"url": "https://github.com/<your_name>/engine.git",
"custom_deps": {},
"deps_file": "DEPS",
"safesync_url": "",
},
]
  • 切换源码。编译前的一个重要操作是将源码切换到 本地 Flutter SDK 的 engine version (一个 commit id) 对应的提交点,避免可能出现的报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 查看本地 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

# flutter 1.12 使用以下命令生成 host_debug_unopt 编译配置
# ./flutter/tools/gn --runtime-mode debug

# flutter 1.17 使用以下命令生成 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

尤其注意这里的 ninja -C out/host_debug_unopt 命令。官方文档提到它是必要的:

Note that if you use particular android or ios engine build, you will need to have corresponding host build available next to it: if you use android_debug_unopt, you should have built host_debug_unopt, android_profile -> host_profile, etc.

这个命令的编译结果包含 dart-sdk,使用自己构建的 Flutter 引擎编译 App 时会调用 dart-sdk 中相关工具。

编译完成后的目录如下:

编译后的Flutter产物

常见编译问题

编译 Flutter 其实并不复杂,运气好的话不会遇到任何问题,运气不好的话会遇到一些错误。不过出错通常是因为环境问题。

如果遇到错误,不妨先检查以下几项:

  • 确认当前使用的是 Python 2.7 - flutter/tools/gn 依赖的是 Python 2。如果当前环境中使用 Python 3 会出现各种诡异错误
  • 确认已经将 depot_tools 工具添加到环境变量 - depot_tools 工具包含几个用于编译 Flutter 引擎的命令 gclientninja。如果找不到这些命令,也会编译失败
  • 切换引擎源码 - Flutter 引擎源码和本地 Flutter SDK 不匹配时可能会编译失败,编译前应确认已经将 Flutter 引擎源码切换到对应的提交点

Python 版本问题

这里简单记录一下我编译 Flutter 引擎时遇到的 Python 版本问题及解决方法。如果你没有遇到 Python 版本问题,可以直接忽略这一节。

我的 MacBook 系统内置了 Python 2.7,安装的第三方软件 Anaconda 自带的 Python 3.7。命令行下 python 默认的是 Anaconda 的 Python 3.7,不做任何处理时调用 flutter/tools/gn 会报 Python 语法错误。

推荐使用 virtualenv 来为 Flutter 引擎源码工程创建虚拟的 Python 2.7 环境,激活该环境后再来编译 Flutter 源码,可以有效避免各种 Python 版本折腾。操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 切换到 Flutter 引擎源码目录
cd path/to/engine/source

# 创建名为 myenv 的 Python 2.7 环境
virtualenv -p /usr/bin/python myenv

# 激活 myenv 环境
source myenv/bin/activate

# 编译 Flutter 引擎
...

# 关闭 myenv 环境
deactivate

缺省头文件

在云主机(Centos 7)上编译 Flutter 引擎源码时,glfw 库报错如下:

1
2
3
../../third_party/glfw/src/x11_platform.h:39:10: fatal error: 'X11/Xcursor/Xcursor.h' file not found
#include <X11/Xcursor/Xcursor.h>
^~~~~~~~~~~~~~~~~~~~~~~

glfw 提到解决办法如下:

For example, on Ubuntu and other distributions based on Debian GNU/Linux, you need to install the xorg-dev package, which pulls in all X.org header packages.

安装相关的库即可,方式如下(你可能要根据实际情况调整):

1
2
3
4
yum install libX11-devel
yum install libXcursor-devel
yum install libXrandr-devel
yum install libXxf86vm-devel

运行

操作步骤

命令行中使用自定义引擎:

1
2
3
4
5
6
7
# 创建一个 Flutter 工程
flutter create --org com.yourdomain your_app_name

# 使用本地引擎运行 Flutter App
flutter run
--local-engine-src-path <engine path>/src
--local-engine=android_debug_unopt_arm64

IDE 中使用自定义引擎:

1
2
3
4
5
6
7
# 创建一个 Flutter 工程
flutter create --org com.yourdomain your_app_name

# 在 Android Studio 中打开 以上工程
# 注意这里是 Android 工程视角,即打开目录为 your_app_name/android

# 在 gradle.properties 文件中添加 localEngineOut 属性

在 gradle.properties 文件中添加 localEngineOut 属性,配置如下:

配置 localEngineOut

1
localEngineOut=<engine_dir>/out/android_debug_unopt_arm64

注意:

  • 应当指定的本地引擎应当跟 Android 系统架构匹配,比如 armeabi-v8a 机器上使用 android_debug_unopt_arm64
  • 尤其注意某些项目通过自行将 Flutter SDK libflutter.so 拷贝到代码库的方式来集成 Flutter,这会导致上述方式失效,实际运行时并不会加载指定的本地引擎。解决方法是将拷贝对应的目录下(如 android_debug_unopt_arm64)的 libflutter.so 覆盖代码库中已有有 libflutter.so 即可

常见运行错误

最常见的问题是找不到指定的引擎导致无法运行 Flutter App。原因通常包括:

  • 引擎文件路径写错
  • 架构不匹配。以我手头的测试机华为 Nova 2 为例,它要求使用 arm64 类型的引擎,而我编译时没有注意到这一点,选择的是 arm 类型,最后发现引擎架构不匹配
  • 缺少 host 产物。错误提示如下图

我自己遇到另外一个比较奇特的错误,通常应该不会遇到,但这里也记录下供参考。

事情经过是这样的:我的 MacBook 硬盘只有 256G,目前空间已经偏紧张,考虑编译 Flutter 引擎占 CPU 影响本地机器性能外加占用太多硬盘空间,所以我突发奇想。

  • 首先在个人的 Linux 云主机上编译 Flutter 引擎(果然比本地快很多)
  • 然后将 Linux 云主机的硬盘挂在 MacBook 当本地文件使用
  • 最后使用远程机器上的 Flutter 引擎运行 App:
1
2
3
4
5
# 挂载远程机器目录
sshfs root@dev-host:/data/github/ ~/RemoteMount

# 使用远程机器上的 Flutter 引擎
flutter run --local-engine-src-path ~/RemoteMount/src --local-engine=android_debug_unopt_arm6

最后出错,错误信息如下:

无法执行 dart 命令

无法执行 dart 命令!Mac 系统当然无法执行 Linux 平台的二进制文件。

调试

断点调试 Flutter 引擎来一步步观察引擎代码如何运行,是学习 Flutter 引擎代码的一个好办法。在介绍如何调试 Flutter 引擎前我们先来看看 Flutter 开发中可能遇到哪些调试场景:

  1. 调试 Flutter App Dart 代码
  2. 调试 Flutter SDK Dart 代码
  3. 调试 Flutter 引擎 Java 代码
  4. 调试 Flutter 引擎 C++ 代码

第一种场景非常简单,只要在 VS Code 中给 Flutter App 中的 Dart 代码打上断点即可进行调试。

第二种场景也比较简单,在 VS Code 中配置 Dart & Flutter 插件,允许调试第三方库和 Flutter SDK Dart 代码即可在相关源码中设置断点进行调试

允许调试第三方库

调试 Java 代码

再来看第三种场景,调试 Flutter 引擎中的 Java 代码。主要是参考以下资料(建议动手操作一下):

步骤如下:

  • 第一步,将 engine/src/flutter/shell/platform/android 工程(称之为Flutter 引擎工程)导入到 Android Studio。注意一定是这个目录!另外,确认该工程的 Android SDK 和 JDK 版本正确 (当前分别是 29 和 8)
  • 第二步,使用自定义 Flutter 引擎运行 Flutter App(称之为Flutter App 工程),具体见上文描述
  • 第三步,Flutter 引擎工程 中给源码设置断点并启动 Debugger 连接到已启动的 Flutter App 进程

各步骤的截图如下:

第一步,Flutter 引擎工程导入到 Android

导入引擎工程

引擎工程结构

第二步,Android Studio 中打开Flutter App 工程 并启动 Flutter App

1
2
3
4
5
# 创建 Flutter App
flutter create --org com.yourdomain your_app_name

# 在 Android Studio 中打开 Flutter App 工程
# 注意工程路径是 your_app_name/android

Android Studio 中启动 Flutter App

第三步,在 Flutter 引擎工程 将 Debugger 连接到已启动 Flutter App

AttachDebugger

选择应用进程

进入调试状态

有时候,我们想在更早的地方设置断点调试,比如在 libflutter.so 被加载前加断点调试,如何实现?

技巧在于第二步启动 Flutter App 时选择一个 合适的时机(通常是在断点之前) 调用 Debug.waitForDebugger 方法让应用启动后进入等待 Debugger 的状态。

示例:在 Application.onCreate() 中调用 Debug.waitForDebugger(),应用一直处于等待状态,直到有 Debugger 连接上来才继续执行。

让应用等待Debugger

应用等待Debugger中

调试 C++ 代码

最后来看怎样调试 Flutter 引擎 C++ 代码。主要参考资料是:

简单来说,是使用 lldb 来调试 Flutter 引擎 C++ 代码。可以使用不同的方式来完成 lldb 调试,但实际操作起都比较麻烦。

个人理解主要麻烦之处在于,一是如何将 lldb_server 推送到开发设备并运行起来,二是如何从开发机正确地启动 lldb_client

从这些麻烦的方法中,我选择了一个相对简单的动手实践了一下并加以总结。步骤概括如下:

步骤 操作 作用 备注
第一步 编译 Flutter 引擎 生成自定义引擎用于调试
第二步 导入 Flutter 引擎源码到 VS Code 支持 Flutter 引擎源码跳转及断点跟踪 后文称VS Code Flutter 引擎工程
第三步 安装 VS Code 插件 C/C++ for Visual Studio CodeCodeLLDB 在 VS Code 中集成 lldb 调试功能
第四步 安装 lldbflutter_lldb 1、lldb 用于调试 C++ 代码,2、flutter_lldb 用于简化 lldb 的使用
第五步 运行 Flutter App 加载和运行自定义引擎 flutter run 或 Android Studio 中运行均可
第六步 执行 flutter_lldb 命令 1、生成调试配置参数,2、在调试设备上启动 lldb_server
第七步 VS Code Flutter 引擎工程 中配置 lldb 调试配置参数
第八步 VS Code Flutter 引擎工程 中设置断点,F5 启动调试

为了便于理解和动手操作操作,这里给出一些截图。

第一步,编译 Flutter 引擎。注意确认已生成 out/host_debug_unopt

编译后的Flutter产物

第二步,导入 Flutter 引擎源码到 VS Code

第三步,安装 VS Code 插件,集成 lldb 调试功能

第四步,安装 lldbflutter_lldb

注意:

  • lldb 安装在 Android SDK 中。如 Android SDK 已有 lldb,则可略过这一步
  • 有一个坑,目前 Android Studio 内置 lldb,但却并不安装在 Android SDK 中,所以外部工具无法方便地调用 AS 内置 lldb,而 Android SDK Manager 软件列表中目前不再提供 lldb 安装,所以只能手工下载和安装 lldb

第五步,运行 Flutter App

第六步,执行 flutter_lldb 命令

flutter_lldb 输出的配置参数类似如下这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [

{
"name": "remote_lldb",
"type": "lldb",
"request": "attach",
"pid": "28006",
"initCommands": [
"platform select remote-android",
"platform connect unix-abstract-connect:///data/data/com.yourdomain.your_app_name/debug.socket"
],
"postRunCommands": [
"add-dsym /Users/abc/wd/engine/src/out/android_debug_unopt_arm64/libflutter.so",
"settings set target.source-map /Users/abc/wd/engine/src /Users/chenming/wd/engine/src"
],
}
]
}

注意:

  • flutter_lldb 脚本基于 Python 2.7,使用前请确认当前 Python 环境
  • flutter_lldb 脚本与调试设备通信,使用前请确认已连接调试设备且已启动应用

第七步,在 VS Code 中配置 lldb 命令调试参数。直接将上一步中 flutter_lldb 输出的配置参数拷贝到 launch.json 中即可

第八步,VS Flutter 引擎工程 中设置断点,F5 启动调试

一切正常的话,Debugger 将成功连接应用进程,进入调试状态,如下图:

进t入调试状态

Debug Console 输出显示已经成功连接到指定的进程

成功连接指定的进程

千万不要被上述繁琐的步骤吓到了。实际上,第一步到第四步只是准备阶段(只需准备一次),第五步到第八步才是真正的操作步骤(调试时需反复多次操作)。用一张图来总结下第五步到第八步:

-w767

操作几次之后,你会发现实际过程更简单。其实并不用真的的每次都要重新配置 launch.json,只不过是修改下 pid 而已。

UPDATE:使用小米手机能正常调试 Flutter 引擎,但华为手机 Nova 2 上可以成功到 Attached to process 却无法进入调试状态。使用 NDK Sample 中的 HelloJNI 华为 Nova 2 验证,发现也不能正常调试,所以推测是华为手机问题。

(2020-07-24 更新:以下是 iOS 上调试 Flutter 引擎的步骤)

  1. 将 Flutte Engine 工程的 products.xcodeproj 拖进需要调试的 Flutter Demo 工程目录
  2. Genrated.xcconfig 中添加如下配置
1
2
3
4
5
6
7
8
9
10
11
FLUTTER_ROOT=${FlutterSDK 路径}
FLUTTER_APPLICATION_PATH=${Demo工程路径}
FLUTTER_TARGET=${Demo工程路径}/lib/main.dart
FLUTTER_BUILD_DIR=build
SYMROOT=${SOURCE_ROOT}/../build/ios
FLUTTER_FRAMEWORK_DIR=${Flutter_Engine代码路径}/src/out/ios_debug_sim_unopt
FLUTTER_BUILD_NAME=1.0.0
FLUTTER_BUILD_NUMBER=1
FLUTTER_ENGINE=${Flutter_Engine代码路径}
LOCAL_ENGINE=${输出的路径(ios_debug_sim_unopt)}
ARCHS=${支持的架构(arm64)}

之后即可单步调试。

总结

  • 简单介绍如何编译 Flutter 引擎,以及常见问题
  • 介绍如何从命令行和 IDE 运行自定义 Flutter 引擎,以及常见问题
  • 简单介绍如何调试 Flutter App 及 Flutter SDK 中的 Dart 代码
  • 详细介绍如何调试 Flutter 引擎中的 Java 代码和 C++ 代码

参考