likes
comments
collection
share

Flutter Embedder之在QT中显示Flutter界面

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

Flutter Embedder是什么

Flutter Embedder是一个关键组件,它充当Flutter引擎和宿主平台之间的桥梁。它是在特定平台运行Flutter应用所必需的,在Flutter官方支持的macOS、Windows、Linux平台也是对接的Embedder。它包含了一组API和库,使得Flutter代码能够运行在各种不同的操作系统和硬件上。Flutter Embedder的工作原理和关键特性如下:

  1. 底层平台抽象:Flutter Embedder负责在不同的操作系统上创建窗口,管理视图和上下文,并处理用户输入,如触摸、键盘和鼠标事件。
  2. 引擎启动:它启动并运行Flutter引擎,这是一个跨平台的运行时,可以编译和运行Flutter代码。
  3. 渲染循环:Embedder负责创建和管理渲染循环,这是一个持续的过程,它允许屏幕上的图像能够以60Hz或更高的频率更新。这是实现平滑动画和响应式UI的关键部分。
  4. 插件支持:它提供了一种机制来支持原生插件。这意味着开发者可以为平台特定的功能编写原生代码,并通过Flutter代码调用它。
  5. 平台特定逻辑:开发者可以通过Embedder添加一些特定于平台的逻辑,例如使用系统API来实现深度集成的功能。
  6. 资源管理:它还负责管理应用程序资源,例如字体和图像,并将它们提供给Flutter框架。

Flutter Embedder是一个可以高度定制的组件,它使得Flutter能够运行在各种环境中,所以在一些官方没支持的平台上特别是一些嵌入式平台,如果要使用Flutter,就需要自行接入Embedder。

平台接入条件

想要平台能接入,需要一些前提

  1. 需要有支持该平台的Flutter Embedder静态库

官方默认提供macOS、Windows(x64)、Linux(x64)平台的Embedder静态库,我们可以去Google Cloud上查找官方编译好的库。如果该平台没有支持的Embedder库,就需要自行编译Flutter引擎来编译出Embedder库,所以也需要该平台能支持CMake、GN等编译工具。

  1. 图形和渲染支持

Flutter是一个UI框架,平台需要有能力支持OpenGL, Vulkan, Metal, DirectX或软件渲染等图形技术中的至少一种,这样Flutter的Skia图形引擎才能正常工作。如在一些无界面的Linux机器上无法接入。

  1. 操作系统API

平台必须提供必要的操作系统API,用于管理窗口、捕获用户输入事件等能力。

开始

为了便于学习,我会在QT中来接入Flutter Embedder作为例子。可能有人会有疑问,QT本身也是跨平台UI框架,跟Flutter算是竞争关系了,为什么选择QT。首先是QT不受限平台,更方便在我Mac电脑上操作,其次我想也有很多老项目使用了QT,在其中接入Flutter来平滑过渡也是个非常好的方案,当然其实最关键的是我有案例可以参考。

准备环境

首先我们需要准备环境,我目前使用的环境如下

CMake 3.22

QT Framework 6.6

Flutter 3.0.2

  1. QT

首先我使用的是QT6,在macOS上可以通过brew install qt6来下载,我自己下载最终路径是/usr/local/Cellar/qt/6.6.0

  1. Flutter Embedder动态库
  1. 提前编译Flutter产物

编写CMake

为了便于学习,我并没有使用Qt Create进行开发,而是使用CMake方式进行开发,所以首先我们需要引入QT和FlutterEmbedder库

#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(Qt6 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
        Qt6::Widgets
        Qt6::Core
        Qt6::OpenGL
        flutter_engine)

定义QT窗口

首先我们需要定义一个用于渲染Flutter的QT窗口,我们可以使用QWindow打开一个QT窗口,为了使用OpenGL渲染,我们在窗口中定义一个QOpenGLContext对OpenGL进行初始化

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

// 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对接的参数。

// 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内运行渲染任务的函数提交到主线程来执行。

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来实现。

先定义信号与槽

// flutter_embedder_utils.h
class FlutterEmbedderUtils
public slots:
    void handleTask(FlutterTask task);
signals:
    void handleMainTask(FlutterTask task);

连接信号槽并在非主线程将信号handleMainTask提交到主线程的handleTask中执行

// 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资源

上面步骤我定义了FlutterProjectArgsFlutterProjectArgs上有个assets_path指向了编译后的Flutter资源地址。上面步骤执行flutter build bundle编译出的文件会在工程目录下的build/flutter_assets下。然后我们通过assets_path指定到这个资源目录。

FlutterProjectArgs args = {};
// ...
args.assets_path = "~/flutter_sample/build/flutter_assets";

传入icudtl.dat地址

icudtl.dat是FlutterEmbedder中提供的一个包含国际化组件数据的二进制文件,这个文件在Flutter中提供了国际化服务、日期/时间格式化、字符集转换等能力。在macOS中,icudtl.datFlutterEmbedder.fromework/Resources/icudtl.dat中。我们可以通过icu_data_path将其传入引擎

args.icu_data_path = "FlutterEmbedder.fromework/Resource/icudtl.dat";

启动引擎

FlutterEngineResult result = FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args,
                                     this, &mEngine);

初始化页面数据

上面启动引擎执行后,会发现界面是黑色的,并没有渲染出上面我用Flutter写的界面,原因是窗口的一些宽高数据还没发送给Flutter,我们需要通过FlutterEngineSendWindowMetricsEvent将这些数据传给Flutter引擎。

// 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窗口了。

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    FlutterView window(nullptr);
    window.show();
    return app.exec();
}

如果一切配置都没问题的话,将会看到Flutter绘制的窗口。

Flutter Embedder之在QT中显示Flutter界面

最后

上面为了把界面显示出来,我尽量少的调用FlutterEmbedder的接口,很多功能都没实现,比如上面点击+会发现并不会有响应,因为手势/鼠标事件都没有接入进来,后面我会继续更新,接入手势/鼠标和键盘等事件。

上面的完整代码看这里flutter_embedder_qt

转载自:https://juejin.cn/post/7306450197630255123
评论
请登录