Flutter Engine 编译与调试(2023)
概念
一、Flutter架构层
Engine 是Flutter 的核心,它主要使用 C++ 编写,并提供了 Flutter 应用所需的原语。当需要绘制新一帧的内容时,引擎将负责对需要合成的场景进行栅格化。它提供了 Flutter 核心 API 的底层实现,包括图形(通过 Skia)、文本布局、文件及网络 IO、辅助功能支持、插件架构和 Dart 运行环境及编译环境的工具链。
引擎将底层 C++ 代码包装成 Dart 代码,通过 dart:ui
暴露给 Flutter 框架层。该库暴露了最底层的原语,包括用于驱动输入、图形、和文本渲染的子系统的类。
通常,开发者可以通过 Flutter 框架层 与 Flutter 交互,该框架提供了以 Dart 语言编写的现代响应式框架。它包括由一系列层组成的一组丰富的平台,布局和基础库。从下层到上层,依次有:
- 基础的 foundational 类及一些基层之上的构建块服务,如 animation**、 painting 和 **gestures,它们可以提供上层常用的抽象。
- 渲染层 用于提供操作布局的抽象。有了渲染层,你可以构建一棵可渲染对象的树。在你动态更新这些对象时,渲染树也会自动根据你的变更来更新布局。
- widget 层 是一种组合的抽象。每一个渲染层中的渲染对象,都在 widgets 层中有一个对应的类。此外,widgets 层让你可以自由组合你需要复用的各种类。响应式编程模型就在该层级中被引入。
- Material 和 Cupertino 库提供了全面的 widgets 层的原语组合,这套组合分别实现了 Material 和 iOS 设计规范。
二、应用剖析
下图为你展示了一个通过 flutter create
命令创建的应用的结构概览。该图展示了引擎在架构中的定位,突出展示了 API 的操作边界,并且标识出了每一个组成部分。
1、Dart App
就是我们编写Flutter 应用dart代码的地方,一般都是指/lib下的代码
2、Framework
在我们下载下来的Flutter SDK flutter/packages/flutter/lib/
3、Engine
我们平时所用到的engine是编译好的引擎产物,所在位置如下:
这里我们以Android平台arm64为例,打开jar我们看看都有什么东西在里边,我们看到lib下包含一个so文件,文件大小33M
再看下lib下,主要包含Flutter的 相关的字节码文件1.3M
这样看来,引擎是由.java和c++代码编译而来,或者是.class和c++代码编译后组合而来。我们看到其中有app、和embedding文件夹 ,对应上面应用剖析图中的Embedder部分
我们简单看下源码长什么样子,右边的部分都属于flutter的源码
4、Embedder
上面我们了解到引擎产物中的embedding对应Embedder,源码对应engine源码库的这里
dev/engine/src/flutter/shell/platform
5、Runner
这个没什么好说的,对应各个平台的宿主APP。
那么我们如何自己修改和定制我们自己的引擎满足特殊需求,修改引擎源码,然后编译出引擎产物?当然可以,下面我们来一步一步开启引擎定制化操作。
准备
- 畅通的网络(‼️)
- github、配好ssh
- depot_tools
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
配置环境变量:export PATH=$PATH:/path/to/depot_tools
源码Clone
1、fork Flutter Engine到自己的Github仓库
2、创建引擎存放目录、添加.gclient文件
mkdir engine
cd engine
touch .gclient
.gclient内容如下(替换为自己的Github Flutter Engine仓库)
solutions = [
{
"managed": False,
"name": "src/flutter",
"url": "git@github.com:<YOUR_NAME>/engine.git",
"custom_deps": {},
"deps_file": "DEPS",
"safesync_url": "",
},
]
3、同步代码
gclient sync
同步代码过程较为漫长(总共22G左右),当进度为100%时,依然会下载4~5个G的内容,请不要中断,可以在活动监视器中观察网络使用情况。
与官方仓库关联
1、查看当前远程仓库
cd src/flutter
git remote -v
origin git@github.com:<YOUR_NAME>/engine.git (fetch)
origin git@github.com:<YOUR_NAME>/engine.git (push)
2、添加指向官方仓库的upstream
git remote add upstream git@github.com:flutter/engine.git
3、查看origin和upstream
git remote -v
origin git@github.com:<YOUR_NAME>/engine.git (fetch)
origin git@github.com:<YOUR_NAME>/engine.git (push)
upstream git@github.com:flutter/engine.git (fetch)
upstream git@github.com:flutter/engine.git (push)
4、从原仓库拉取代码并直接合并代码
git pull upstream
匹配版本
1、在实际开发中,一般不直接使用master的代码直接编译,都是需要获取指定版本的engine代码。可以通过本地安装的Flutter SDK版本来获取所对应的engine版本。
flutter channel stable//切换通道到稳定版
flutter upgrade //升级flutter sdk 到最新
cat dev/flutter/bin/internal/engine.version //获取当前版本commit id
1837b5be5f0f1376a1ccf383950e83a80177fb4e
2、切换分支同步代码
cd engine/src/flutter
git reset --hard 1837b5be5f0f1376a1ccf383950e83a80177fb4e
gclient sync -D --with_branch_heads --with_tags -v
本次 sync 时间较长(本人梯子6-8M/s要等待大概10分钟左右),依然要同步4~5个G的内容,请耐心等待。我的经验是关注活动监视器网络情况,如果收到数据速度小于梯子正常速度,中断后再次执行同步命令即可。
编译
一、创建目标工程
- Android
./flutter/tools/gn --android --unoptimized
./flutter/tools/gn --android --unoptimized --android-cpu=arm64
./flutter/tools/gn --android --runtime-mode=release
./flutter/tools/gn --android --android-cpu=arm64 --runtime-mode=release
- MacOS
./flutter/tools/gn --unoptimized
./flutter/tools/gn --runtime-mode=release
二、编译引擎
- Android
ninja -C out/android_debug_unopt
ninja -C out/android_debug_unopt_arm64
ninja -C out/android_release
ninja -C out/android_release_arm64
- MacOS
ninja -C out/host_debug_unopt
ninja -C out/host_release
各平台首次编译时间较长,大概30-60分钟,以后改动代码后再次编译为增量更新,大大缩短编译时间。
应用产物
上面编译好 Flutter Engine 之后,就可以通过使用 Flutter tools 在编译我们App项目的时候指定为我们编译出来的 Flutter Engine 了。
一、调试阶段
先创建一个 Flutter App 工程:
flutter create --org com.fluency.engine engineplay
使用以下参数启动刚刚创建的应用
cd engineplay
flutter run --local-engine-src-path /Users/fluency/dev/engine/src --local-engine=android_debug_unopt_arm64
flutter run --local-engine-src-path /Users/fluency/dev/engine/src --local-engine=host_debug_unopt
或将启动参数配置在IDE中
二、实际使用
替换本地Flutetr sdk 中的engine产物,引擎在sdk的位置:
/bin/cache/artifacts/engine
Android直接替换到flutter sdk中的engine产物不会生效.因为flutter 1.12.x之后打包流程会直接走远程下载flutter engine产物。我们需要修改flutter.gradle脚本,直接从本地读取engine编译打包。
Android 修改gradle.properties
local-engine-repo=engine/src/out/android_release # 这个自己通过gclient sync下载flutter engine的路径
local-engine-out=engine/src/out/android_release # arm64平台对应使用android_release_arm64
local-engine-build-mode=release
验证引擎
如何验证编译App的时候,确实是用了我们自编译的 Flutter Engine 呢?我们可以修改 Flutter Eingine ,加一些日志输出看看。
使用 Xcode 打开 engine/src/out/android_debug_unopt_arm64/flutter_engine.xcodeproj 工程文件。
然后,打开 代码文件 engine/src/flutter/shell/common/engine.cc 源代码文件。
再次编译引擎
ninja -C out/android_debug_unopt_arm64
再次使用自编译引擎启动app,观察控制台输出
发现启动app后,在引擎启动时确实打印了刚刚自己加入的log,说明当前应用确实使用了自己改动后编译的引擎,验证成功。
调试引擎
由于Host是Mac,所以这里以Mac为例调试引擎源码
找到flutter app 工程下macos/flutter/ephemeral/Flutter-Generated.xcconfig文件(IOS是Generated.xcconfig文件)
添加如下配置项:
FLUTTER_ENGINE=/Users/fluency/dev/engine/src
LOCAL_ENGINE=host_debug_unopt
ARCHS=x86_64 //(ios不加)
用Xcode打开Runner.xcworkspace
将对应平台的产物下flutter_engine.xcodeproj拖入Xcode中
拖入Xcode后:
此时我们可以随意在Flutter engine 引擎中打断点调试了,打断点后使用Xcode启动应用,代码会在断点位置停止
IOS的调试过程与Mac相同,如果断点没有断住,请检查Flutter-Generated.xcconfig和Generated.xcconfig的配置本地引擎地址和平台是否正确。好了,以上就是引擎调试的过程。
实际应用
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: RichText(
text: const TextSpan(
text:
"Engine 是Flutter 的核心,它主要使用 C++ 编写,并提供了 Flutter 应用所需的原语。当需要绘制新一帧的内容时,引擎将负责对需要合成的场景进行栅格化。它提供了 Flutter 核心 API 的底层实现,包括图形(通过 Skia)、文本布局、文件及网络 IO、辅助功能支持、插件架构和 Dart 运行环境及编译环境的工具链。",
style: TextStyle(color: Colors.black)),
textAlign: TextAlign.justify,
),
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
以上是Flutter demo中加入一段RichText Widget,当内容足够多时,文本块内容并不能完美对齐,我们看到textAlign已经设置为TextAlign.justify自适应对其,但效果并不令人满意。
按图索骥我们要修改TextAlign.justify的代码逻辑使其满足我们的需求,保证在任何情况下文字排版两边对其。最终我们找到引擎源码下该文件
/Users/fluency/dev/engine/src/third_party/skia/modules/skparagraph/src/TextLine.cpp
将justify方法的逻辑改为如下所示
void TextLine::justify(SkScalar maxWidth) {
// Count words and the extra spaces to spread across the line
// TODO: do it at the line breaking?..
constexpr auto kWhiteSpaceNumOfStart = 2;
size_t allCharNums = 0;
SkScalar textLen = 0;
size_t posOfCharFirst = -1;
size_t firstResult = 0;
this->iterateThroughClustersInGlyphsOrder(
false, false, [&](const Cluster* cluster, bool ghost) {
textLen += cluster->width();
posOfCharFirst++;
if (posOfCharFirst == firstResult && firstResult < kWhiteSpaceNumOfStart &&
cluster->isWhitespaceBreak()) {
firstResult++;
return true;
}
if (posOfCharFirst == 0 || (posOfCharFirst == firstResult)) {
return true;
}
++allCharNums;
return true;
});
if (allCharNums == 0) {
return;
}
SkScalar step = (maxWidth - textLen) / allCharNums;
SkScalar shift = 0;
// Deal with the ghost spaces
auto ghostShift = maxWidth - this->fAdvance.fX;
// Spread the extra whitespaces
size_t posOfCharSecond = -1;
size_t result = 0;
this->iterateThroughClustersInGlyphsOrder(false, true, [&](const Cluster* cluster, bool ghost) {
posOfCharSecond++;
if (ghost) {
if (cluster->run().leftToRight()) {
shiftCluster(cluster, ghostShift, ghostShift);
}
return true;
}
auto prevShift = shift;
if (posOfCharSecond == result && result < kWhiteSpaceNumOfStart &&
cluster->isWhitespaceBreak()) {
result++;
return true;
}
if (posOfCharSecond == 0 || posOfCharSecond == result) {
return true;
}
shift += step;
shiftCluster(cluster, shift, prevShift);
return true;
});
SkAssertResult(nearlyEqual(shift, maxWidth - textLen));
this->fWidthWithSpaces += ghostShift;
this->fAdvance.fX = maxWidth;
}
大概逻辑是计算每一行文字总宽度和空格数量,用排版最大宽度减去每行文字总宽度除以空格数量,得到每一个空格偏移量,重新遍历整行文字,在每个空格地方加上之前算的偏移量,最终等分每一个文字之间的间距。
执行ninja -C out/host_debug_unopt重新编译引擎源码,再看效果:
可以看到排版已经可以完美达到两边对齐了。至此通过修改引擎代码完成了“定制化”的需求。
总结
本人以Android开发者的角度来看,Flutter Engine更像Android生态中安装在手机中的操作系统的一部分,就像Flutter官方架构图一样,比如负责渲染、栅格化、提供Dart VM等等,只不过Android这一部分早已被提前安装进了手机硬件,而Flutter需要自己接管这一部分就需要将引擎打入应用包中,额外使用Embedder调用宿主系统API。
在绝大多数Flutter开发过程中不需要定制化引擎,但Flutter Engine也是代码编译的,有代码的地方就一定有bug,定制引擎给我们提供了一条解决“特殊”需求,或修复官方认为不是bug的bug一条出路。
转载自:https://juejin.cn/post/7215854856731508797