Flutter Embedder是什么
Flutter Embedder是一个关键组件,它充当Flutter引擎和宿主平台之间的桥梁。它是在特定平台运行Flutter应用所必需的,在Flutter官方支持的macOS、Windows、Linux平台也是对接的Embedder。它包含了一组API和库,使得Flutter代码能够运行在各种不同的操作系统和硬件上。Flutter Embedder的工作原理和关键特性如下:
- 底层平台抽象:Flutter Embedder负责在不同的操作系统上创建窗口,管理视图和上下文,并处理用户输入,如触摸、键盘和鼠标事件。
- 引擎启动:它启动并运行Flutter引擎,这是一个跨平台的运行时,可以编译和运行Flutter代码。
- 渲染循环:Embedder负责创建和管理渲染循环,这是一个持续的过程,它允许屏幕上的图像能够以60Hz或更高的频率更新。这是实现平滑动画和响应式UI的关键部分。
- 插件支持:它提供了一种机制来支持原生插件。这意味着开发者可以为平台特定的功能编写原生代码,并通过Flutter代码调用它。
- 平台特定逻辑:开发者可以通过Embedder添加一些特定于平台的逻辑,例如使用系统API来实现深度集成的功能。
- 资源管理:它还负责管理应用程序资源,例如字体和图像,并将它们提供给Flutter框架。
Flutter Embedder是一个可以高度定制的组件,它使得Flutter能够运行在各种环境中,所以在一些官方没支持的平台上特别是一些嵌入式平台,如果要使用Flutter,就需要自行接入Embedder。
平台接入条件
想要平台能接入,需要一些前提
- 需要有支持该平台的Flutter Embedder静态库
官方默认提供macOS、Windows(x64)、Linux(x64)平台的Embedder静态库,我们可以去Google Cloud上查找官方编译好的库。如果该平台没有支持的Embedder库,就需要自行编译Flutter引擎来编译出Embedder库,所以也需要该平台能支持CMake、GN等编译工具。
- 图形和渲染支持
Flutter是一个UI框架,平台需要有能力支持OpenGL, Vulkan, Metal, DirectX或软件渲染等图形技术中的至少一种,这样Flutter的Skia图形引擎才能正常工作。如在一些无界面的Linux机器上无法接入。
- 操作系统API
平台必须提供必要的操作系统API,用于管理窗口、捕获用户输入事件等能力。
开始
为了便于学习,我会在QT中来接入Flutter Embedder作为例子。可能有人会有疑问,QT本身也是跨平台UI框架,跟Flutter算是竞争关系了,为什么选择QT。首先是QT不受限平台,更方便在我Mac电脑上操作,其次我想也有很多老项目使用了QT,在其中接入Flutter来平滑过渡也是个非常好的方案,当然其实最关键的是我有案例可以参考。
准备环境
首先我们需要准备环境,我目前使用的环境如下
CMake 3.22
QT Framework 6.6
Flutter 3.0.2
- QT
首先我使用的是QT6,在macOS上可以通过brew install qt6
来下载,我自己下载最终路径是/usr/local/Cellar/qt/6.6.0
- Flutter Embedder动态库
官方提供了每个版本的动态库,可以从自己Flutter安装路径下的/bin/internal/engine.version
中获取引擎的版本,然后访问https://console.cloud.google.com/storage/browser/flutter_infra_release/flutter/{engine.version}
下载与自己平台适配的Flutter Embedder库。我下载的是这个,需要说明的是Flutter官方并没有每个平台都提供Embedder库比如Linux arm64就没有提供,也不是Debug、Release、Profile都提供了,如我下载的darwin-x64
下的Embedder是Debug
版本的,如果需要Release版本,需要自行编译Flutter引擎。
- 提前编译Flutter产物
我们可以通过flutter build bundle
编译出Debug
版本的Flutter产物,如果需要编译Release
产物,可以参考Dart编译命令知多少。注意,编译使用的Flutter版本需要和Flutter Embedder库版本一致。
编写CMake
为了便于学习,我并没有使用Qt Create
进行开发,而是使用CMake
方式进行开发,所以首先我们需要引入QT和FlutterEmbedder库
cmake
#QT
set(CMAKE_PREFIX_PATH "${QT_PATH}/lib/cmake") # 此处设置本地qt cmake地址
set(QT_PLUGIN_PATH "${QT_PATH}/plugins") # 设置本地qt plugins地址
message("CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}")
find_package(Qt5 COMPONENTS Widgets Core OpenGL REQUIRED)
set(CMAKE_AUTOMOC ON)
#flutter
set(FLUTTER_ENGINE_LIB_PATH "/FlutterEmbedder.framework") # 设置下载到本地flutterEmbedder地址
set_target_properties(flutter_engine PROPERTIES
FRAMEWORK TRUE
IMPORTED_LOCATION ${FLUTTER_ENGINE_LIB_PATH}/FlutterEmbedder
INTERFACE_INCLUDE_DIRECTORIES ${FLUTTER_ENGINE_LIB_PATH}/Headers
)
add_executable(flutter_embedder_qt main.cpp)
target_link_libraries(flutter_embedder_qt PRIVATE
Qt5::Widgets
Qt5::Core
Qt5::OpenGL
flutter_engine)
定义QT窗口
首先我们需要定义一个用于渲染Flutter的QT窗口,我们可以使用QWindow
打开一个QT窗口,为了使用OpenGL渲染,我们在窗口中定义一个QOpenGLContext
对OpenGL进行初始化
cpp
class FlutterView : public QWindow {
public:
FlutterView(QWindow *parent) {
setSurfaceType(QSurface::OpenGLSurface);
create();
resize(1280, 720);
context = new QOpenGLContext;
context->create();
}
private:
QOpenGLContext *context;
};
定义FlutterEmbedderUtils类对接FlutterEmbedder
cpp
// flutter_embedder_utils.h
class FlutterEmbedderUtils {
private:
QOpenGLContext *mContext;
QWindow *mQwindow;
FlutterEngine mEngine;
public:
explicit FlutterEmbedderUtils(QOpenGLContext *glWidget, QWindow *qWindow);
}
mContext
用于与Flutter对接的OpenGL上下文,而mQwindow
则为Flutter能够渲染的窗口画布,这两个属性都是从上面FlutterView
类传递过来的,mEngine
是初始化FlutterEmbedder后的引擎。
OpenGL对接
我在FlutterEmbedderUtils
定义了run
方法来初始化FlutterEngine
,初始化FlutterEngine
会调用FlutterEngineInitialize
初始化引擎然后调用FlutterEngineRunInitialized
启动或者使用FlutterEngineRun
直接启动,我使用的FlutterEngineRun
。在启动引擎之前,我需要把一些必须的参数定义好。下面我们开始定义Flutter与OpenGL
对接的参数。
cpp
// flutter_embedder_utils.cpp
void FlutterEmbedderUtils::run() {
// 渲染模式相关配置
FlutterRendererConfig config = {};
// 设置OpenGL渲染
config.type = kOpenGL;
config.open_gl.struct_size = sizeof(config.open_gl);
// OpenGL渲染上下文,将Flutter里的OpenGL操作都绑定到mQwindow中
config.open_gl.make_current = [](void *userdata) -> bool {
FlutterEmbedderUtils *host = reinterpret_cast<FlutterEmbedderUtils *>(userdata);
host->mContext->makeCurrent(host->mQwindow);
return true;
};
// 设置clear_current回调,此回调在需要解除当前渲染上下文时调用
config.open_gl.clear_current = [](void *userdata) -> bool {
FlutterEmbedderUtils *host = reinterpret_cast<FlutterEmbedderUtils *>(userdata);
// 使用自定义的渲染器来清除当前的OpenGL上下文
host->mContext->doneCurrent();
return true;
};
// 设置资源上下文的回调,此回调在需要设置资源加载上下文时调用
config.open_gl.make_resource_current = [](void *userdata) -> bool {
FlutterEmbedderUtils *host = reinterpret_cast<FlutterEmbedderUtils *>(userdata);
// 在这里,我们检查是否在相同的线程上运行任务
return host->runsTasksOnSelfThread();
};
// 设置present_with_info回调,此回调在需要将渲染好的帧展示到屏幕上时调用
config.open_gl.present_with_info =
[](void *userdata, const FlutterPresentInfo *info) -> bool {
FlutterEmbedderUtils *host = reinterpret_cast<FlutterEmbedderUtils *>(userdata);
// 使用自定义的渲染器来交换帧缓冲区,展示新的帧
host->mContext->swapBuffers(host->mQwindow);
return true;
};
// 设置用于获取当前帧缓冲对象的回调,这可以用于优化,例如在多层渲染中
config.open_gl.fbo_with_frame_info_callback =
[](void *userdata, const FlutterFrameInfo *frameInfo) -> uint32_t {
// 我们总是返回默认的帧缓冲对象
return 0;
};
// 设置一个标志,表示在帧展示后帧缓冲对象是否应该被重置
// 这通常用于OpenGL上下文需要在每次渲染后重置状态的场景
config.open_gl.fbo_reset_after_present = false;
// 设置一个函数,用于解析OpenGL函数的地址
config.open_gl.gl_proc_resolver = [](void *userdata,
const char *procName) -> void * {
FlutterEmbedderUtils *host = reinterpret_cast<FlutterEmbedderUtils *>(userdata);
return (void *) host->mContext->getProcAddress(procName);
};
}
我是在FlutterView
初始化的OpenGL,同时OpenGL上下文是初始化在主线程的。make_resource_current
需要判断当前是否在OpenGL线程,我这里因为都是在主线程,所以只要判断线程不在主线程就返回false,这样就不会出现OpenGL相关调用在异步线程里执行而导致崩溃。
处理Render任务
上一步由于OpenGL的是在主线程进行的,所以我们还需要定义Flutter提供的render_task_runner
将Flutter内运行渲染任务的函数提交到主线程来执行。
cpp
void FlutterEmbedderUtils::init() {
// .......省略代码
FlutterTaskRunnerDescription render_task_runner = {};
render_task_runner.struct_size = sizeof(FlutterTaskRunnerDescription);
render_task_runner.user_data = this;
// 提供一个回调函数,用于检查当前线程是否是渲染线程
render_task_runner.runs_task_on_current_thread_callback =
[](void *userdata) -> bool {
FlutterEmbedderUtils *host = reinterpret_cast<FlutterEmbedderUtils *>(userdata);
return host->runsTasksOnSelfThread();
};
// 提供一个回调函数,用于将任务投递到渲染线程
render_task_runner.post_task_callback = [](FlutterTask task,
uint64_t target_time_nanos,
void *userdata) {
FlutterEmbedderUtils *host = reinterpret_cast<FlutterEmbedderUtils *>(userdata);
// 将任务投递到宿主平台的渲染线程
return host->postTask(task);
};
// 设置渲染任务运行器的唯一标识符,用于区别其它任务
render_task_runner.identifier = kRenderThreadIdentifer;
// 将自定义任务运行器组合起来
FlutterCustomTaskRunners custom_task_runners = {};
custom_task_runners.struct_size = sizeof(FlutterCustomTaskRunners);
// 将渲染任务运行器指定给自定义任务运行器
custom_task_runners.render_task_runner = &render_task_runner;
// 定义一个FlutterProjectArgs,用于最终传递给FlutterEngine
FlutterProjectArgs args = {};
args.struct_size = sizeof(FlutterProjectArgs);
// 绑定自定义的任务运行器
args.custom_task_runners = &custom_task_runners;
// .......
上面主要有两个部分,runs_task_on_current_thread_callback
用于Flutter引擎内检查当前线程是否是在渲染线程(主线程),post_task_callback
是将Flutter的任务提交到渲染线程执行。上面我定义了postTask
用于将任务提交到渲染线程也就是主线程执行,其中我用QT的connect
来实现。
先定义信号与槽
cpp
// flutter_embedder_utils.h
class FlutterEmbedderUtils
public slots:
void handleTask(FlutterTask task);
signals:
void handleMainTask(FlutterTask task);
连接信号槽并在非主线程将信号handleMainTask
提交到主线程的handleTask
中执行
cpp
// flutter_embedder_utils.cpp
FlutterEmbedderUtils::FlutterEmbedderUtils() {
// 注册FlutterTask自定义类型到Qt的元对象系统以便可以在信号和槽中使用。因为FlutterTask不是一个Qt内置类型。
qRegisterMetaType<FlutterTask>("FlutterTask");
// 连接handleMainTask信号到handleTask槽。
// 当handleMainTask信号被触发时,handleTask槽将被调用。
connect(this, &FlutterEmbedderUtils::handleMainTask, this,
&FlutterEmbedderUtils::handleTask, Qt::QueuedConnection);
}
// postTask函数负责在主线程中调度一个Flutter任务
void FlutterEmbedderUtils::postTask(FlutterTask task) {
// 检查是否当前线程是创建此对象的线程(即主线程)。
if (QThread::currentThread() == thread()) {
// 如果是在主线程,直接处理任务。
handleTask(task);
} else {
// 如果不是在主线程,通过发射信号来请求主线程去处理任务。
emit handleMainTask(task);
}
}
// handleTask函数实际上在Flutter引擎中执行任务。
void FlutterEmbedderUtils::handleTask(FlutterTask task) {
// 检查是否Flutter引擎实例是有效的。
if (!mEngine) {
// 如果引擎没有正确初始化,输出错误信息。
printf("engine not work\n");
return;
}
// 尝试在Flutter引擎中运行任务。
if (FlutterEngineRunTask(mEngine, &task) != kSuccess) {
// 如果任务不能被投递到Flutter引擎,输出错误信息。
printf("Could not post an engine task.\n");
}
}
传入编译好的Flutter资源
上面步骤我定义了FlutterProjectArgs
,FlutterProjectArgs
上有个assets_path
指向了编译后的Flutter资源地址。上面步骤执行flutter build bundle
编译出的文件会在工程目录下的build/flutter_assets
下。然后我们通过assets_path
指定到这个资源目录。
cpp
FlutterProjectArgs args = {};
// ...
args.assets_path = "~/flutter_sample/build/flutter_assets";
传入icudtl.dat地址
icudtl.dat
是FlutterEmbedder中提供的一个包含国际化组件数据的二进制文件,这个文件在Flutter中提供了国际化服务、日期/时间格式化、字符集转换等能力。在macOS中,icudtl.dat
在FlutterEmbedder.fromework/Resources/icudtl.dat
中。我们可以通过icu_data_path
将其传入引擎
cpp
args.icu_data_path = "FlutterEmbedder.fromework/Resource/icudtl.dat";
启动引擎
cpp
FlutterEngineResult result = FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args,
this, &mEngine);
初始化页面数据
上面启动引擎执行后,会发现界面是黑色的,并没有渲染出上面我用Flutter写的界面,原因是窗口的一些宽高数据还没发送给Flutter,我们需要通过FlutterEngineSendWindowMetricsEvent
将这些数据传给Flutter引擎。
cpp
// flutter_embedder_utils.cpp
void FlutterEmbedderUtils::init() {
// ...
FlutterEngineResult result = FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args,
this, &mEngine);
if (result != kSuccess) {
printf("FlutterEngineInitialize error: %d %p\n", result, mEngine);
} else {
printf("Flutter engine is running!\n");
mIsRunning = true;
handleWindowResize();
}
}
bool FlutterEmbedderUtils::handleWindowResize() {
// 获取窗口的像素比
double pixelRatio = mQwindow->devicePixelRatio();
FlutterWindowMetricsEvent event = {};
event.struct_size = sizeof(event);
event.width = mQwindow->size().width() * pixelRatio;
event.height = mQwindow->size().height() * pixelRatio;
event.pixel_ratio = pixelRatio;
FlutterEngineResult result = FlutterEngineSendWindowMetricsEvent(mEngine, &event);
return result == kSuccess;
}
启动QWindow
做完这些事后就可以启动QT窗口了。
cpp
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
FlutterView window(nullptr);
window.show();
return app.exec();
}
如果一切配置都没问题的话,将会看到Flutter绘制的窗口。
最后
上面为了把界面显示出来,我尽量少的调用FlutterEmbedder
的接口,很多功能都没实现,比如上面点击+会发现并不会有响应,因为手势/鼠标事件都没有接入进来,后面我会继续更新,接入手势/鼠标和键盘等事件。
上面的完整代码看这里flutter_embedder_qt