likes
comments
collection
share

Flutter Engine 编译与调试(2023)

作者站长头像
站长
· 阅读数 7

概念

一、Flutter架构层

Flutter Engine 编译与调试(2023)

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 的操作边界,并且标识出了每一个组成部分。

Flutter Engine 编译与调试(2023)

1、Dart App

就是我们编写Flutter 应用dart代码的地方,一般都是指/lib下的代码

2、Framework

在我们下载下来的Flutter SDK flutter/packages/flutter/lib/

Flutter Engine 编译与调试(2023)

3、Engine

我们平时所用到的engine是编译好的引擎产物,所在位置如下:

Flutter Engine 编译与调试(2023)

这里我们以Android平台arm64为例,打开jar我们看看都有什么东西在里边,我们看到lib下包含一个so文件,文件大小33M

Flutter Engine 编译与调试(2023)

再看下lib下,主要包含Flutter的 相关的字节码文件1.3M

Flutter Engine 编译与调试(2023)

这样看来,引擎是由.java和c++代码编译而来,或者是.class和c++代码编译后组合而来。我们看到其中有app、和embedding文件夹 ,对应上面应用剖析图中的Embedder部分

我们简单看下源码长什么样子,右边的部分都属于flutter的源码

Flutter Engine 编译与调试(2023)

4、Embedder

上面我们了解到引擎产物中的embedding对应Embedder,源码对应engine源码库的这里

dev/engine/src/flutter/shell/platform 

Flutter Engine 编译与调试(2023)

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中

Flutter Engine 编译与调试(2023)

二、实际使用

替换本地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 源代码文件。

Flutter Engine 编译与调试(2023)

再次编译引擎

 ninja -C out/android_debug_unopt_arm64   

再次使用自编译引擎启动app,观察控制台输出

Flutter Engine 编译与调试(2023)

发现启动app后,在引擎启动时确实打印了刚刚自己加入的log,说明当前应用确实使用了自己改动后编译的引擎,验证成功。

调试引擎

由于Host是Mac,所以这里以Mac为例调试引擎源码

找到flutter app 工程下macos/flutter/ephemeral/Flutter-Generated.xcconfig文件(IOS是Generated.xcconfig文件)

Flutter Engine 编译与调试(2023)

添加如下配置项:

FLUTTER_ENGINE=/Users/fluency/dev/engine/src
LOCAL_ENGINE=host_debug_unopt
ARCHS=x86_64  //(ios不加)

用Xcode打开Runner.xcworkspace

将对应平台的产物下flutter_engine.xcodeproj拖入Xcode中

Flutter Engine 编译与调试(2023)

拖入Xcode后:

Flutter Engine 编译与调试(2023)

此时我们可以随意在Flutter engine 引擎中打断点调试了,打断点后使用Xcode启动应用,代码会在断点位置停止

Flutter Engine 编译与调试(2023)

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自适应对其,但效果并不令人满意。

Flutter Engine 编译与调试(2023) 按图索骥我们要修改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重新编译引擎源码,再看效果:

Flutter Engine 编译与调试(2023) 可以看到排版已经可以完美达到两边对齐了。至此通过修改引擎代码完成了“定制化”的需求。

总结

本人以Android开发者的角度来看,Flutter Engine更像Android生态中安装在手机中的操作系统的一部分,就像Flutter官方架构图一样,比如负责渲染、栅格化、提供Dart VM等等,只不过Android这一部分早已被提前安装进了手机硬件,而Flutter需要自己接管这一部分就需要将引擎打入应用包中,额外使用Embedder调用宿主系统API。

在绝大多数Flutter开发过程中不需要定制化引擎,但Flutter Engine也是代码编译的,有代码的地方就一定有bug,定制引擎给我们提供了一条解决“特殊”需求,或修复官方认为不是bug的bug一条出路。