嵌入式Linux设备上Flutter图形界面开发实战
说明:本文以一个实际的示例工程为线索,带你完成在嵌入式 Linux 设备上使用 Flutter 开发图形界面应用的全过程。从环境准备、交叉编译、工程结构、运行调试,到示例代码解析,都提供了详细步骤与图解,帮助你快速上手。
目录
- 前言
- 方案概览与架构图
- 3.1 硬件与系统要求
- 3.2 交叉编译工具链
- 3.3 Flutter SDK 与必要源码
- 4.1 Flutter Engine 架构简介
- 4.2 图形子系统:EGL + DRM / Wayland
- 4.3 运行时与宿主层对接
- 5.1 新建 Flutter 应用模板
- 5.2 调整
pubspec.yaml
与依赖 - 5.3 简单 UI 代码示例:
main.dart
- 6.1 获取并编译 Flutter Engine(Linux ARM 版)
- 6.2 编写交叉编译 CMake 脚本
- 6.3 构建生成可执行文件(Target)
- 7.1 打包必要的库与资源
- 7.2 将二进制和资源拷贝到设备
- 7.3 启动方式示例(Systemd 服务 / 脚本)
- 图解:从 Host 到 Device
- 9.1 目录结构
- 9.2 关键文件剖析
- 10.1 日志输出与调试技巧
- 10.2 帧率监控与 GPU 帧分析
- 10.3 常见问题与解决方案
- 总结与后续拓展
前言
Flutter 作为 Google 出品的跨平台 UI 框架,除了手机与桌面端,还可以运行在 Linux 平台上。然而,嵌入式 Linux(例如基于 ARM Cortex-A 的开发板)并不自带完整的桌面环境,尤其缺少 X11/Wayland、完整的打包工具。因此,要在嵌入式设备上跑 Flutter,需要自定义编译 Flutter Engine、部署最小化的运行时依赖,并将 Flutter 应用打包成能够在裸机 Linux 环境下启动的可执行文件。
本文以“Rockchip RK3399 + Yocto 构建的 Embedded Linux”为例,演示如何完成这一流程。你可以根据自己的板卡型号和操作系统分发版本,做相应替换或微调。
方案概览与架构图
2.1 方案概览
Host 端(开发机)
- 安装 Ubuntu 20.04
- 配置交叉编译工具链(GCC for ARM 64)
- 下载并编译 Flutter Engine 的 Linux ARM 版本
- 创建 Flutter 应用,生成前端资源(Dart AOT、flutter\_assets)
- 生成一个可执行的二进制(包含 Flutter Engine + 应用逻辑)
Device 端(嵌入式 Linux 板卡)
- 运行最小化的 Linux(Kernel + BusyBox/Yocto Rootfs)
- 部署交叉编译后生成的可执行文件及相关动态库、资源文件
- 启动可执行文件,Flutter Engine 负责接管 DRM/EGL,渲染 UI
2.2 架构图
┌───────────────────────────────────────────┐
│ 开发机 (Host) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│ │Flutter │──▶│Flutter │──▶│交叉编译 ││
│ │工程 (Dart)│ │Engine │ │CMake ││
│ └──────────┘ └──────────┘ └────┬─────┘│
│ │
│ ┌───────────────────────────┐ │
│ │ 生成可执行文件(ARM64) │ │
│ └───────────────────────────┘ │
└───────────────────────────────────────────┘
↓ scp
┌───────────────────────────────────────────┐
│ 嵌入式 Linux 设备 (Device) │
│ │
│ ┌──────────┐ ┌────────────┐ ┌───────┐│
│ │Kernel │──▶│DRM/EGL │◀──│HDMI ││
│ │+Rootfs │ │渲染层 │ │显示屏 ││
│ └──────────┘ └────────────┘ └───────┘│
│ ▲ │
│ │ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐│
│ │ Flutter 可执行 │ App ││
│ │ (Engine + assets) │ ◀──│按键/触摸 ││
│ └──────────┘ └──────────┘ └───────────┘│
└───────────────────────────────────────────┘
- 描述:Host 上编译得到的可执行文件在 Device 上运行后,会调用 Linux Kernel 提供的 DRM/EGL 接口,直接在 HDMI 或 LCD 上渲染 Flutter UI。触摸或按键事件通过
/dev/input/eventX
传入 Flutter Engine,驱动应用逻辑。
环境准备
3.1 硬件与系统要求
主机 (Host):
- 操作系统:Ubuntu 20.04 LTS
- 内存:至少 8GB
- 硬盘:至少 50GB 可用空间
- 安装了 Git、Python3、curl、wget、gcc、g++ 等基本开发工具
嵌入式板卡 (Device):
- 处理器:ARM Cortex-A53/A72(例如 RK3399)
- 系统:基于 Yocto/Buildroot 构建的 Embedded Linux,内核版本 ≥ 4.19
- 已集成 DRM/KMS 驱动(带有 EGL 支持)
- 已准备好可与 Host 互通的网络环境(SSH、SCP)
3.2 交叉编译工具链
安装 ARM 64 位交叉编译工具链:
sudo apt update sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
检查交叉编译器版本:
aarch64-linux-gnu-gcc --version # 应输出类似:gcc (Ubuntu 9.4.0) 9.4.0 ...
说明:如果你使用 Yocto SDK,可以直接使用 Yocto 提供的交叉编译环境。本文以 Ubuntu 自带 gcc-aarch64-linux-gnu
为例,进行手动交叉编译。
3.3 Flutter SDK 与必要源码
下载 Flutter SDK(Host):
cd $HOME git clone https://github.com/flutter/flutter.git -b stable export PATH="$PATH:$HOME/flutter/bin" flutter doctor
- 确保
flutter doctor
未发现明显问题。 - 我们并不在 Host 上跑完整的 Flutter Desktop,只需要下载 SDK、命令行工具,以及用于编译 Engine 的源代码。
- 确保
获取 Flutter Engine 源码:
cd $HOME git clone https://github.com/flutter/engine.git -b master
- (Engine 源码较多,整个克隆可能需要几分钟)。
安装 Ninja、Dep等依赖:
sudo apt install -y ninja-build pkg-config libgtk-3-dev liblzma-dev sudo apt install -y curl python3 python3-pip git unzip xz-utils
提示:后面我们会用到gn
、ninja
来编译 Engine,如果缺少工具,会导致编译失败。
Flutter 在嵌入式 Linux 上的移植原理
为理解后续步骤,这里简要介绍 Flutter Engine 在 Linux 环境下的架构,以及如何将其移植到嵌入式设备。
4.1 Flutter Engine 架构简介
Dart 运行时(Dart VM / AOT)
- Flutter 应用会以 AOT(Ahead-of-Time)方式编译为机器码,生成一个
.so
库(libapp.so
),包含 Dart 代码与资源(flutter_assets
)。 - Engine 会加载这个 AOT 库,通过 Dart Entrypoint 调用用户的
main()
函数。
- Flutter 应用会以 AOT(Ahead-of-Time)方式编译为机器码,生成一个
Shell 层(PlatformEmbedder)
- 每个平台都有一个 “Shell”,负责桥接 Engine 与底层操作系统。例如 Linux Shell 会使用 GTK/GLX/EGL、X11 或者 DRM/KMS 进行渲染。
- 嵌入式场景中,我们使用 “Linux DRM Shell”或者 “Wayland Shell”来直接操作帧缓冲。
渲染子系统(Skia + OpenGL ES)
- Engine 通过 Skia 绘制所有 UI,渲染命令最终会转换为 OpenGL ES 或 Vulkan 调用,提交给 GPU。
- 在嵌入式设备上,通常使用 OpenGL ES + EGL,或者通过 DRM/KMS 直连 Framebuffer。
Platform Channels(插件层)
- Flutter 通过 Platform Channels 与 native 层交互,嵌入式上可以用这套机制实现硬件接口调用(GPIO、串口、I2C 等)。
4.2 图形子系统:EGL + DRM / Wayland
DRM/KMS:
- DRM (Direct Rendering Manager) / KMS (Kernel Mode Setting) 是 Linux Kernel 提供的图形输出子系统。
- Flutter Engine 可通过
dart:ffi
或者已集成的 “drm\_surface\_gl.cc”(Engine 的一部分)调用 DRM 接口,让 GPU 将渲染结果发送到 Framebuffer,然后通过 DRM 显示到屏幕上。
EGL:
- EGL 管理 OpenGL ES 上下文与 Surface。
- 在嵌入式上,Engine 需要为 DRM 创建一个
EGLSurface
,并将渲染结果直接呈现到设备的 Framebuffer。
Wayland(可选):
- 如果你的系统带有 Wayland Server,Engine 也可以基于 Wayland Shell 进行渲染,与上层 compositor 协作。这种方案在某些嵌入式发行版(如 Purism 的 PureOS)中会比较常见。
4.3 运行时与宿主层对接
输入事件
- 嵌入式设备的触摸或按键事件一般通过
/dev/input/eventX
抛出。Engine 的 DRM Shell 会打开相应的设备节点,监听鼠标/触摸/键盘事件,然后通过 Flutter 的事件管道(PointerEvent、KeyboardEvent)分发给 Flutter 框架层。
- 嵌入式设备的触摸或按键事件一般通过
音频与其他外设
- 如果需要用到麦克风或扬声器,可在 Engine 中编译 Audio 插件,或者自行编写 Platform Channel,通过 ALSA 等接口调用硬件。
了解了上述原理,下面进入具体的操作步骤。
创建并配置 Flutter 项目
5.1 新建 Flutter 应用模板
在 Host 上,打开终端,执行:
cd $HOME
flutter create -t template --platforms=linux my_flutter_embedded
-t template
:创建一个较为精简的模板,不带复杂插件。--platforms=linux
:指定仅生成 Linux 相关的配置(我们稍后会替换默认的 Desktop 支持)。- 最终在
$HOME/my_flutter_embedded
下会生成基础目录结构。
5.2 调整 pubspec.yaml
与依赖
编辑 my_flutter_embedded/pubspec.yaml
,添加必要依赖,例如:
name: my_flutter_embedded
description: A Flutter App for Embedded Linux
publish_to: 'none'
version: 0.1.0
environment:
sdk: ">=2.17.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
# 如果需要使用 Platform Channels 调用 native 接口,可添加如下依赖
# path_provider: ^2.0.0
# flutter_localizations:
# sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets:
- assets/images/
assets/images/
目录下可以放置 PNG、JPEG 等静态资源,打包进flutter_assets
。
5.3 简单 UI 代码示例:main.dart
将 lib/main.dart
修改为如下内容,展示一个简单的计数器加一个本机按钮示例(通过 Platform Channel 打印日志):
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
// 定义一个 MethodChannel,用于调用 native 层
const platform = MethodChannel('com.example.embedded/log');
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Embedded Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.dark,
),
home: const MyHomePage(title: '嵌入式 Flutter 示例'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({Key? key, required this.title}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
String _nativeLog = '';
void _incrementCounter() {
setState(() {
_counter++;
});
}
Future<void> _getNativeLog() async {
String log;
try {
// 调用 native 层的 log 函数
final String result = await platform.invokeMethod('log', {'message': '按钮被点击'});
log = 'Native Log: $result';
} on PlatformException catch (e) {
log = "调用失败:${e.message}";
}
setState(() {
_nativeLog = log;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Flutter 嵌入式示例页面', style: TextStyle(fontSize: 20)),
const SizedBox(height: 20),
Text('计数器:$_counter', style: Theme.of(context).textTheme.headlineMedium),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _incrementCounter,
child: const Text('++'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _getNativeLog,
child: const Text('获取 Native 日志'),
),
const SizedBox(height: 20),
Text(_nativeLog),
],
),
),
);
}
}
- 该界面展示了最常见的计数器示例,并通过
MethodChannel
调用名为com.example.embedded/log
的 native 接口。 - 稍后我们会在 C++ 层实现这一
log
方法,将输入字符串打印到终端或写入日志。
构建交叉编译环境
核心在于编译 Flutter Engine 并生成一个能在 ARM64 上直接运行的可执行文件。以下示例以 Linux+EGL+DRM Shell 为基础。
6.1 获取并编译 Flutter Engine(Linux ARM 版)
切换到 Engine 源码目录,执行依赖安装脚本:
cd $HOME/engine/src # 安装 GN、 Ninja 等 python3 build/linux/unpack_dart_sdk.py python3 build/linux/unpack_flutter_tools.py
创建 GN 编译配置文件
arm64_release.gn
(放在engine/src
下),内容如下:# arm64_release.gn import("//flutter/build/gn/standalone.gni") # 定义目标平台 target_os = "linux" is_debug = false target_cpu = "arm64" # 64-bit ARM use_x11 = false # 不使用 X11 use_ozone = true # Ozone + DRM use_drm_surface = true # 启用 DRM Surface use_system_libdrm = true # 使用系统库 libdrm use_egl = true use_vulkan = false # 关闭 Vulkan is_official_build = false symbol_level = 0
生成 Ninja 构建文件并编译:
cd $HOME/engine/src flutter/tools/gn --unoptimized --config=arm64_release.gn out/arm64_release ninja -C out/arm64_release
- 执行完毕后,会在
engine/src/out/arm64_release
下得到一系列.so
动态库及一个可执行的flutter_tester
或shell
二进制。 - 我们重点关注
libflutter_engine.so
以及 Linux Shell 可执行文件(如flutter_surface_drm
/flutter_engine
)。根据 Engine 版本不同,命名可能略有差异,但都包含 “drm” 或 “embedded” 字样。
- 执行完毕后,会在
注意:编译过程非常耗时(视硬件性能可能需要 30 分钟甚至更久),请耐心等待。
6.2 编写交叉编译 CMake 脚本
我们接下来创建一个 linux_embedder
目录,用于编译一个最小化的 C++ “宿主/Embedder” 项目,将 Flutter Engine 与我们的 Dart AOT 库链接,生成最终的可执行文件。
在项目根目录下创建
linux_embedder/
,目录结构大致如下:my_flutter_embedded/ ├── linux_embedder/ │ ├── CMakeLists.txt │ ├── embedder.h │ ├── embedder.cc │ └── linux_embedding/ │ ├── ComputePlatformTaskRunner.cc │ ├── LinuxContext.cc │ ├── LinuxContext.h │ ├── LinuxSurface.cc │ └── LinuxSurface.h └── ...
CMakeLists.txt
(交叉编译示例):cmake_minimum_required(VERSION 3.10) project(my_flutter_embedded_embedder LANGUAGES C CXX) # 设置交叉编译工具链 set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) # 交叉编译器路径 set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) # 设置 C++ 标准 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 指定 Flutter Engine 的输出目录 set(FLUTTER_ENGINE_DIR "/home/user/engine/src/out/arm64_release") set(FLUTTER_ENGINE_LIBS ${FLUTTER_ENGINE_DIR}/libflutter_engine.so ${FLUTTER_ENGINE_DIR}/libflutter_linux_egl.so ${FLUTTER_ENGINE_DIR}/libflutter_linux_surface.so # 视版本而定 ) # Dart AOT 库路径(待会生成) set(DART_AOT_LIB "${CMAKE_SOURCE_DIR}/../build/aot/libapp.so") # 包含头文件 include_directories( ${FLUTTER_ENGINE_DIR}/flutter/shell/platform/embedder ${FLUTTER_ENGINE_DIR}/flutter/shell/platform/linux_embedded ${CMAKE_SOURCE_DIR}/linux_embedding ) # 源码文件 file(GLOB EMBEDDER_SOURCES "${CMAKE_SOURCE_DIR}/linux_embedding/*.cc" "${CMAKE_SOURCE_DIR}/embedder.cc" ) add_executable(my_flutter_app ${EMBEDDER_SOURCES}) # 链接库 target_link_libraries(my_flutter_app ${FLUTTER_ENGINE_LIBS} ${DART_AOT_LIB} drm gbm EGL GLESv2 pthread dl m # 如需 OpenAL / PulseAudio,可在此添加 ) # 安装目标:将可执行文件复制到 bin 目录 install(TARGETS my_flutter_app RUNTIME DESTINATION bin)
embedder.h
:声明一些初始化和主循环接口#ifndef EMBEDDER_H_ #define EMBEDDER_H_ #include <flutter_embedder.h> #include <string> // 初始化 Flutter 引擎并运行 bool RunFlutter(const std::string& assets_path, const std::string& aot_lib_path); #endif // EMBEDDER_H_
embedder.cc
:实现 RunFlutter 函数,加载 AOT 库并启动 Engine#include "embedder.h" #include "LinuxContext.h" #include "LinuxSurface.h" #include "ComputePlatformTaskRunner.h" #include <flutter_embedder.h> #include <iostream> #include <unistd.h> bool RunFlutter(const std::string& assets_path, const std::string& aot_lib_path) { // 1. 创建 OpenGL ES 上下文(基于 DRM/KMS) LinuxContext context; if (!context.Setup()) { std::cerr << "Failed to setup EGL/GL context." << std::endl; return false; } // 2. 创建渲染表面 LinuxSurface surface; if (!surface.Initialize(context.getDisplay(), context.getConfig())) { std::cerr << "Failed to initialize surface." << std::endl; return false; } // 3. 获取 Task Runner flutter::TaskRunnerDescription runner_desc = ComputePlatformTaskRunner::Get(); // 4. 设置 Flutter 嵌入器配置 FlutterProjectArgs args = {}; args.struct_size = sizeof(FlutterProjectArgs); args.assets_path = assets_path.c_str(); args.icu_data_path = (assets_path + "/icudtl.dat").c_str(); args.aot_library_path = aot_lib_path.c_str(); args.platform_message_callback = nullptr; args.run_dart_code_before_main = nullptr; args.dart_entrypoint_argc = 0; args.dart_entrypoint_argv = nullptr; // 5. 选择刷新率与窗口大小(需与 DRM/KMS 匹配) FlutterRendererConfig render_config = {}; render_config.type = kOpenGL; render_config.open_gl.struct_size = sizeof(FlutterOpenGLRendererConfig); render_config.open_gl.make_current = [](void* data) -> bool { return static_cast<LinuxContext*>(data)->MakeCurrent(); }; render_config.open_gl.clear_current = [](void* data) -> bool { return static_cast<LinuxContext*>(data)->ClearCurrent(); }; render_config.open_gl.present = [](void* data) -> bool { auto* surface = static_cast<LinuxSurface*>(data); surface->SwapBuffers(); return true; }; render_config.open_gl.fbo_callback = [](void* data) -> uint32_t { auto* surface = static_cast<LinuxSurface*>(data); return surface->GetFBO(); }; render_config.open_gl.make_resource_current = [](void* data) -> bool { return static_cast<LinuxContext*>(data)->MakeResourceCurrent(); }; // 6. 初始化 Flutter Engine FlutterEngine engine = nullptr; FlutterEngineResult result = FlutterEngineRun( FLUTTER_ENGINE_VERSION, &render_config, &args, nullptr, &engine); if (result != kSuccess || !engine) { std::cerr << "Failed to start Flutter Engine: " << result << std::endl; return false; } // 7. 进入主循环(监听输入并刷新) while (true) { context.ProcessEvents(); // 读取 DRM/KMS 输入事件,转换为 Flutter pointerEvent usleep(16000); // Roughly 60 FPS } // 8. 退出:调用 FlutterEngineShutdown(engine); return true; } int main(int argc, char** argv) { if (argc < 3) { std::cerr << "Usage: " << argv[0] << " <assets_path> <aot_lib_path>" << std::endl; return -1; } const std::string assets_path = argv[1]; const std::string aot_lib_path = argv[2]; if (!RunFlutter(assets_path, aot_lib_path)) { std::cerr << "Failed to run Flutter." << std::endl; return -1; } return 0; }
linux_embedding
下的辅助文件LinuxContext.cc/h
: 负责创建 DRM/KMS 设备、初始化 EGL 显示与上下文。LinuxSurface.cc/h
: 基于 EGL 创建一个 Fullscreen Surface,并提供SwapBuffers()
。ComputePlatformTaskRunner.cc
: Flutter 需要一个 Task Runner 来处理 IO 和 GPU 线程,将 Linux 系统的epoll
/select
变换为 Flutter 可识别的 TaskRunner。
提示:这些文件可以参考 Flutter Engine 自带的 “linux\_embedded” 示例代码,并根据自己的板卡硬件(例如 DRM 接口名称、EDID 信息)做相应修改。完整示例请参阅 flutter/engine。
6.3 构建生成可执行文件(Target)
在
my_flutter_embedded/linux_embedder/
下创建一个build/
目录:cd $HOME/my_flutter_embedded/linux_embedder mkdir build && cd build
调用 CMake 并编译:
cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DFlutter_ENGINE_DIR=$HOME/engine/src/out/arm64_release \ -DDART_AOT_LIB=$HOME/my_flutter_embedded/build/aot/libapp.so make -j8
- 最终会在
linux_embedder/build/
下生成my_flutter_app
可执行文件。
注意:DART_AOT_LIB
需要先通过 Flutter 工具链生成。下面我们演示如何从 Dart 代码生成 AOT 库。
6.3.1 生成 Dart AOT 库 libapp.so
在 Flutter 项目根目录下,执行:
cd $HOME/my_flutter_embedded flutter build bundle \ --target-platform=linux-arm64 \ --release \ --target lib/main.dart \ --asset-dir=build/flutter_assets
- 该命令会生成
build/flutter_assets/
(包含flutter_assets
目录)和一个空的libapp.so
? - 但在 Linux 端,要生成 AOT 库,需要调用 engine 工具:
# 进入 Engine 源码 cd $HOME/engine/src # 生成 AOT 库,指定 DART_ENTRYPOINT=main python3 flutter/tools/gn --unoptimized --config=arm64_release.gn out/arm64_aot ninja -C out/arm64_aot shell # 只编译 AOT 所需部分
- 该过程会在 Engine 的输出目录里生成名为
libapp.so
的 AOT 库(路径如engine/src/out/arm64_aot/gen/.../libapp.so
)。 将此
libapp.so
拷贝到 Flutter 项目的build/aot/
目录下,并命名为libapp.so
:mkdir -p $HOME/my_flutter_embedded/build/aot cp $HOME/engine/src/out/arm64_aot/gen/flutter/obj/flutter_embedder/libapp.so \ $HOME/my_flutter_embedded/build/aot/libapp.so
- 该命令会生成
提示:不同版本的 Engine,AOT 库的生成路径会有所差异,请根据实际输出路径调整拷贝命令。
部署与运行
完成上述编译后,我们需要将以下内容部署到嵌入式设备:
my_flutter_app
(可执行文件)build/flutter_assets/
(Flutter 资源,包括 Dart 代码、vm_snapshot_data
、isolate_snapshot_data
、图标、图片等)build/aot/libapp.so
(Dart AOT 库)Flutter Engine 运行时所需的共享库:
libflutter_engine.so
libflutter_linux_egl.so
libflutter_linux_surface.so
(如果有)
- Duck 蔓延进所有依赖的系统库(DRM、EGL、GLESv2、pthread、dl、m 等,通常设备自带即可)。
7.1 打包必要的库与资源
在 Host 上创建一个打包脚本
package.sh
,内容示例:#!/bin/bash DEVICE_IP="192.168.1.100" TARGET_DIR="/home/root/flutter_app" FLUTTER_ENGINE_DIR="$HOME/engine/src/out/arm64_release" BUILD_DIR="$HOME/my_flutter_embedded/linux_embedder/build" # 1. 创建远程目录 ssh root@${DEVICE_IP} "mkdir -p ${TARGET_DIR}/lib ${TARGET_DIR}/flutter_assets" # 2. 拷贝可执行文件 scp ${BUILD_DIR}/my_flutter_app root@${DEVICE_IP}:${TARGET_DIR}/ # 3. 拷贝 AOT 库 scp $HOME/my_flutter_embedded/build/aot/libapp.so root@${DEVICE_IP}:${TARGET_DIR}/ # 4. 拷贝 flutter_assets scp -r $HOME/my_flutter_embedded/build/flutter_assets/* root@${DEVICE_IP}:${TARGET_DIR}/flutter_assets/ # 5. 拷贝 Engine 库 scp ${FLUTTER_ENGINE_DIR}/libflutter_engine.so root@${DEVICE_IP}:${TARGET_DIR}/lib/ scp ${FLUTTER_ENGINE_DIR}/libflutter_linux_egl.so root@${DEVICE_IP}:${TARGET_DIR}/lib/ scp ${FLUTTER_ENGINE_DIR}/libflutter_linux_surface.so root@${DEVICE_IP}:${TARGET_DIR}/lib/ # 6. 设置权限 ssh root@${DEVICE_IP} "chmod +x ${TARGET_DIR}/my_flutter_app"
- 将
${FLUTTER_ENGINE_DIR}
下的库拷贝到设备的${TARGET_DIR}/lib
。 - 将 AOT 库与资源拷贝到
${TARGET_DIR}
下。
- 将
执行打包脚本:
chmod +x package.sh ./package.sh
- 这一步会将所有必要文件传输到板卡上的
/home/root/flutter_app
目录。
- 这一步会将所有必要文件传输到板卡上的
7.2 启动方式示例
在嵌入式设备上,直接运行即可测试:
export LD_LIBRARY_PATH=/home/root/flutter_app/lib:$LD_LIBRARY_PATH
cd /home/root/flutter_app
./my_flutter_app flutter_assets libapp.so
参数说明:
- 第一个参数
flutter_assets
指向资源目录; - 第二个参数
libapp.so
是 AOT 库。
- 第一个参数
如果想让应用随系统启动,可以写一个简单的 Systemd 服务文件:
编辑
/etc/systemd/system/flutter_app.service
:[Unit] Description=Flutter Embedded App After=network.target [Service] Type=simple WorkingDirectory=/home/root/flutter_app ExecStart=/home/root/flutter_app/my_flutter_app flutter_assets libapp.so Restart=on-failure Environment=LD_LIBRARY_PATH=/home/root/flutter_app/lib [Install] WantedBy=multi-user.target
启用并启动服务:
systemctl daemon-reload systemctl enable flutter_app.service systemctl start flutter_app.service
- 使用
journalctl -u flutter_app.service -f
可以实时查看日志输出。
图解:从 Host 到 Device
下面通过一幅示意图,帮助理清从 Host 端编译到 Device 端运行的整体流程。
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ Host (开发机) │
│ │
│ 1. Flutter 工程 (Dart 代码 + 资源) │
│ ┌─────────────────────┐ │
│ │ lib/main.dart │ │
│ │ pubspec.yaml │ │
│ └─────────────────────┘ │
│ │ │
│ 2. flutter build bundle (生成 flutter_assets) │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ build/flutter_assets│ │
│ └─────────────────────┘ │
│ │
│ 3. Flutter Engine 源码 (Engine/src) │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ gn + ninja 编译 (arm64_release) │ │
│ │ ↓ │ │
│ │ 输出目录:out/arm64_release │ │
│ │ ┌────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ libflutter_engine.so, libflutter_linux_egl.so, …, flutter_shell(可执行) │ │ │
│ │ └────────────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ 4. 生成 AOT 库 libapp.so (Engine/src/out/arm64_aot) │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ build/aot/libapp.so │ │
│ └─────────────────────┘ │
│ │
│ 5. 编译嵌入式宿主 (linux_embedder) │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ CMakeLists.txt + embedder.cc + LinuxContext.cc 等 │ │
│ │ ↓ │ │
│ │ 输出可执行 my_flutter_app │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ 6. 打包:scp my_flutter_app, libflutter_*.so, libapp.so, flutter_assets → Device │
│ ▼ │
└─────────────────────────────────────────────────────────────────────────────────────┘
│
│ SSH / SCP
▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ Device (嵌入式 Linux) │
│ │
│ 1. Flutter Engine Shared Libs: │
│ /home/root/flutter_app/lib/libflutter_engine.so │
│ /home/root/flutter_app/lib/libflutter_linux_egl.so │
│ /home/root/flutter_app/lib/libflutter_linux_surface.so │
│ │
│ 2. AOT Library: /home/root/flutter_app/libapp.so │
│ │
│ 3. flutter_assets: /home/root/flutter_app/flutter_assets/* │
│ │
│ 4. 可执行文件: /home/root/flutter_app/my_flutter_app │
│ │ │
│ ▼ │
│ 5. 运行 my_flutter_app flutter_assets libapp.so │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ Flutter Engine 初始化 (DRM/EGL) │ │
│ │ ↓ │ │
│ │ Load AOT (libapp.so), 加载 flutter_assets │ │
│ │ ↓ │ │
│ │ Skia + OpenGL ES → 渲染到 Framebuffer │ │
│ │ ↓ │ │
│ │ 屏幕(HDMI/LCD)显示 Flutter UI │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 6. 输入事件 (/dev/input/event0……) → Flutter Engine → Dart 层 → UI 更新 │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘
示例工程详解
下面以我们已经构建好的 my_flutter_embedded
为例,详细介绍各关键文件的作用。
9.1 目录结构
my_flutter_embedded/
├── build/
│ ├── aot/
│ │ └── libapp.so # Dart AOT 库
│ └── flutter_assets/ # Flutter 资源 (Dart 编译产物)
├── lib/
│ └── main.dart # Flutter 应用入口
├── linux_embedder/
│ ├── CMakeLists.txt # 交叉编译脚本
│ ├── embedder.h # Embedder 接口声明
│ ├── embedder.cc # Embedder 主流程
│ └── linux_embedding/ # DRM/EGL Context & Surface 等
│ ├── LinuxContext.h # EGL 上下文初始化
│ ├── LinuxContext.cc
│ ├── LinuxSurface.h # EGL Surface 创建与 SwapBuffers
│ ├── LinuxSurface.cc
│ └── ComputePlatformTaskRunner.cc
├── pubspec.yaml # Flutter 应用元数据
├── pubspec.lock
├── package.sh # 部署脚本
└── README.md
9.2 关键文件剖析
linux_embedder/LinuxContext.h
/LinuxContext.cc
- 功能:打开 DRM 设备
/dev/dri/card0
,查询显示模式(例如 1920×1080\@60Hz),创建 EGLDisplay、EGLContext。 核心逻辑:
bool LinuxContext::Setup() { // 打开 DRM 设备 drm_fd_ = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); // 1. 获取 DRM 资源 (drmModeGetResources) // 2. 选择合适的 CRTC / Connector / Mode // 3. 创建 GBM device: gbm_create_device(drm_fd_) // 4. eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm_device_, nullptr) // 5. eglInitialize, eglBindAPI(EGL_OPENGL_ES_API) // 6. eglChooseConfig -> eglCreateContext return true; // 或 false }
- 作用:给后续的 Flutter Surface 提供一个可用的 OpenGL ES 上下文。
- 功能:打开 DRM 设备
linux_embedder/LinuxSurface.h
/LinuxSurface.cc
- 功能:基于前面创建的 EGLContext,创建
EGLSurface
,与 DRM/KMS 进行绑定。 核心逻辑:
bool LinuxSurface::Initialize(EGLDisplay display, EGLConfig config) { // 1. 从 GBM 创建一个 GBM surface (gbm_surface_create) // 2. eglCreateWindowSurface(display, config, gbm_surface, nullptr) // 3. 存储 frame buffer id,通过 DRM/KMS 进行 commit return true; } void LinuxSurface::SwapBuffers() { // 1. eglSwapBuffers(display_, egl_surface_); // 2. 获取新的 buffer handle, 调用 drmModePageFlip 提交给 KMS }
- 作用:每次 Flutter 绘制完一帧后,调用
SwapBuffers()
才能让画面切到屏幕。
- 功能:基于前面创建的 EGLContext,创建
linux_embedder/ComputePlatformTaskRunner.cc
- 功能:实现一个简单的 Task Runner,Flutter Engine 在渲染线程、IO 线程、UI 线程之类的异步任务调度,会通过该接口将任务队列调度到 Linux 主线程或子线程执行。
核心:
static void RunTask(flutter::Task task) { // 将 task.callback 在指定的时刻(task.targetTime)放到定时队列中 } flutter::TaskRunnerDescription ComputePlatformTaskRunner::Get() { return { /* struct_size */ sizeof(flutter::TaskRunnerDescription), /* user_data */ nullptr, /* runs_task_on_current_thread */ [](void* user_data) -> bool { /* return true/false */ }, /* post_task */ [](flutter::Task task, uint64_t target_time_nanos, void* user_data) { RunTask(task); }, }; }
- 作用:确保 Flutter Engine 内部的定时任务(如 Dart VM Tick、Repaint)能被 Linux 平台正确调度。
linux_embedder/embedder.cc
- 如前文所示,完成 Engine 初始化、创建 EGL 环境、进入主循环、处理事件等。
package.sh
- 将编译好的二进制、资源、依赖库一并打包到设备,简化部署流程。
Flutter 应用目录
lib/main.dart
- 负责 UI 布局,调用
MethodChannel
与 native 交互。若需要调用本地接口,可在embedder.cc
中注册 platform channel 回调,实现定制化功能。
- 负责 UI 布局,调用
调试与性能优化
10.1 日志输出与调试技巧
- 在
embedder.cc
中调用std::cout
或者__android_log_print
(如已集成),可以在设备上通过串口或者ssh
实时查看输出。 - 可以在
LinuxContext::ProcessEvents()
中打一些关键日志,例如检测到触摸事件、按键事件。
10.2 帧率监控与 GPU 帧分析
- Flutter Inspector(离线):在 Host 上,可使用
flutter trace
、flutter analyze
等工具模拟分析。 设备端 FPS 统计:
可以在应用中插入如下代码,获取帧率信息,然后打印在屏幕上:
WidgetsBinding.instance.addTimingsCallback((List<FrameTiming> timings) { for (var timing in timings) { final frameTimeMs = timing.totalSpan.inMilliseconds; print('Frame time: $frameTimeMs ms'); } });
- 将日志导出到串口或文件,查看是否稳定在 16ms (≈60 FPS) 以下。
Profiling GPU Load:
- 如果板卡支持
/sys/class/devfreq/
或者 GPU driver 提供的统计接口,可实时监控 GPU 占用。
- 如果板卡支持
10.3 常见问题与解决方案
问题 | 可能原因 | 解决方法 |
---|---|---|
应用在启动时卡死、无法显示 UI | - 找不到 EGL 显示 - AOT 库与 Engine 版本不匹配 | - 检查 /dev/dri/card0 是否正确- 确保 AOT 库与 Engine 一致 |
报错:FlutterEngineRun failed / invalid AOT snapshot | AOT 编译版本不对,或拷贝不全 | - 重新从 Engine 里生成 AOT 库 - 确保 libapp.so 和 flutter_assets 同时存在 |
触摸或按键无响应 | - linux_embedding 的 ProcessEvents() 未处理- /dev/input 权限不足 | - 确保应用有读写 /dev/input/event* 权限- 调试 ProcessEvents() 中的事件队列逻辑 |
缺少共享库:libdrm.so.2 not found | 设备系统中没有安装相应库 | - 在 Rootfs 中安装 libdrm , libgbm , libEGL , libGLESv2 等 |
帧率过低,不流畅 | - GPU 性能不足 - 渲染分辨率过高 | - 降低分辨率(修改 CRTC Mode) - 关闭多余的 Flutter 动画或阴影 |
总结与后续拓展
通过本文,你已经掌握了以下核心内容:
- Flutter Engine 移植原理:了解了 Engine 如何基于 DRM + EGL 在嵌入式 Linux 上渲染 UI,以及与 Dart AOT 库的对接方式。
- 交叉编译流程:从下载 Engine 源码、编写 GN 配置,到生成 ARM64 版
libflutter_engine.so
,并通过 CMake 将 Engine 与 App 组装成可执行文件。 - 部署与运行:使用
scp
将所有依赖拷贝到设备,设置LD_LIBRARY_PATH
,并使用 Systemd 或脚本启动应用。 - 示例工程结构:掌握了
linux_embedder
中各个文件的功能,以及如何处理渲染上下文、Surface、Task Runner、事件分发等关键部分。 - 调试与优化思路:掌握日志输出、帧率监控、常见错误排查方法,为后续性能优化打下基础。
后续拓展思考:
- 多点触控与手势:在
ComputePlatformTaskRunner
中,检测触摸设备的多点触控事件,将其打包为PointerEvent
发给 Flutter;- 定制化 Platform Channel:如果你需要访问摄像头、PWM、GPIO 等外设,可在
embedder.cc
中注册新的 method channel 回调,通过libdrm
或者libudev
等接口调用硬件;- 增加音频支持:集成 OpenAL 或 PulseAudio,使应用可播放音效或音乐;
- 集成 Wayland:如果设备带有 Wayland,使用 Engine 自带的 Linux Wayland Shell 替换 DRM Shell,以便与上层 compositor 协同工作;
- 安全性与权限控制:将应用打包成只读文件系统下的容器,限制对
/dev/
目录的访问;- 自动化构建:通过 CI/CD(如 GitLab CI、Jenkins)实现“Host 上拉取代码 → 编译 Engine → 编译 App → 打包 → 部署到 Device” 的全流程自动化。
希望本文能帮助你系统性地了解并掌握在嵌入式 Linux 设备上进行 Flutter 图形界面开发的全流程。
评论已关闭