Flutter 嵌入式开发:在 Linux Framebuffer 上直接运行 Flutter Engine

好的,我们开始今天的讲座。

Flutter 嵌入式开发:在 Linux Framebuffer 上直接运行 Flutter Engine

今天我们要探讨的是一个比较前沿的话题:如何在嵌入式 Linux 系统上,直接利用 Framebuffer 运行 Flutter Engine,从而实现高效、流畅的图形界面。这与传统的 Flutter 应用开发略有不同,因为它绕过了操作系统提供的窗口管理系统(如 X11 或 Wayland),直接控制底层硬件。

1. 为什么要选择 Framebuffer?

在嵌入式系统环境中,资源往往非常有限。传统的桌面环境通常需要运行一套完整的窗口系统,这会消耗大量的 CPU 和内存资源。对于一些资源受限的设备,例如智能家居设备、工业控制面板等,运行窗口系统可能会导致性能瓶颈,甚至无法运行。

Framebuffer 提供了一种更加轻量级的解决方案。它直接将应用程序的图形输出写入到显存中,而无需经过窗口系统的处理。这样可以显著减少资源消耗,提高图形渲染的效率。

特性 Framebuffer 窗口系统(例如 X11)
资源消耗
性能 低(尤其是在资源受限的设备上)
复杂度 较低
适用场景 资源受限的嵌入式系统 通用桌面环境
窗口管理
设备支持 部分设备需要手动配置 一般提供较好的设备支持

2. Flutter Engine 的架构简述

在深入研究如何在 Framebuffer 上运行 Flutter Engine 之前,我们需要简单了解一下 Flutter Engine 的架构。 Flutter Engine 主要由以下几个部分组成:

  • Dart VM: 用于执行 Dart 代码。
  • Skia: 一个跨平台的 2D 图形库,用于渲染 UI。
  • Platform Channel: 用于与宿主平台(例如 Android、iOS、Linux)进行通信。
  • Embedder: 一个平台特定的层,负责将 Flutter Engine 嵌入到宿主环境中。它处理窗口管理、输入事件、平台 API 调用等。

在我们的场景中,Embedder 需要直接与 Framebuffer 进行交互,而不是通过窗口系统。

3. 实现步骤:自定义 Flutter Embedder

要在 Framebuffer 上运行 Flutter Engine,我们需要创建一个自定义的 Embedder。这个 Embedder 需要完成以下任务:

  1. 初始化 Framebuffer: 打开 Framebuffer 设备,获取屏幕的尺寸和像素格式。
  2. 创建 Skia Surface: 基于 Framebuffer 的内存创建一个 Skia Surface,Flutter Engine 将在这个 Surface 上进行渲染。
  3. 处理输入事件: 监听输入设备(例如触摸屏、键盘),并将事件传递给 Flutter Engine。
  4. 刷新屏幕: 将 Skia Surface 的内容复制到 Framebuffer 中,从而更新屏幕显示。

下面是一个简化的示例代码,展示了如何创建一个自定义的 Framebuffer Embedder(使用 C++)。

#include <iostream>
#include <fcntl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <unistd.h>
#include <memory>

#include "flutter_embedder.h" // Flutter Embedder 相关的头文件

class FramebufferEmbedder {
public:
    FramebufferEmbedder(const char* device_path) : device_path_(device_path) {}

    bool Initialize() {
        // 1. 打开 Framebuffer 设备
        fb_fd_ = open(device_path_, O_RDWR);
        if (fb_fd_ == -1) {
            std::cerr << "Failed to open framebuffer device: " << device_path_ << std::endl;
            return false;
        }

        // 2. 获取 Framebuffer 信息
        if (ioctl(fb_fd_, FBIOGET_VSCREENINFO, &vinfo_) == -1) {
            std::cerr << "Failed to get variable screen info." << std::endl;
            close(fb_fd_);
            return false;
        }

        if (ioctl(fb_fd_, FBIOGET_FSCREENINFO, &finfo_) == -1) {
            std::cerr << "Failed to get fixed screen info." << std::endl;
            close(fb_fd_);
            return false;
        }

        screen_width_ = vinfo_.xres;
        screen_height_ = vinfo_.yres;
        bytes_per_pixel_ = vinfo_.bits_per_pixel / 8;
        buffer_size_ = finfo_.smem_len;

        std::cout << "Screen width: " << screen_width_ << std::endl;
        std::cout << "Screen height: " << screen_height_ << std::endl;
        std::cout << "Bytes per pixel: " << bytes_per_pixel_ << std::endl;

        // 3. 映射 Framebuffer 内存
        framebuffer_ = (char*)mmap(0, buffer_size_, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd_, 0);
        if (framebuffer_ == MAP_FAILED) {
            std::cerr << "Failed to map framebuffer memory." << std::endl;
            close(fb_fd_);
            return false;
        }

        return true;
    }

    void Shutdown() {
        if (framebuffer_ != MAP_FAILED) {
            munmap(framebuffer_, buffer_size_);
        }
        if (fb_fd_ != -1) {
            close(fb_fd_);
        }
    }

    int GetScreenWidth() const { return screen_width_; }
    int GetScreenHeight() const { return screen_height_; }
    int GetBytesPerPixel() const { return bytes_per_pixel_; }
    char* GetFramebufferAddress() const { return framebuffer_; }
    size_t GetBufferSize() const { return buffer_size_; }
private:
    const char* device_path_;
    int fb_fd_ = -1;
    struct fb_var_screeninfo vinfo_;
    struct fb_fix_screeninfo finfo_;
    int screen_width_ = 0;
    int screen_height_ = 0;
    int bytes_per_pixel_ = 0;
    size_t buffer_size_ = 0;
    char* framebuffer_ = MAP_FAILED;
};

// Flutter 渲染回调函数
bool FlutterFramebufferRender(void* userdata, const FlutterFrameInfo* frame_info) {
  FramebufferEmbedder* embedder = static_cast<FramebufferEmbedder*>(userdata);
  if (embedder == nullptr) {
    std::cerr << "Embedder userdata is null." << std::endl;
    return false;
  }

  // 假设 Skia 已经渲染到了一个内存区域,这里简单地将该内存复制到 Framebuffer
  // 这部分代码需要根据你的 Skia 渲染实现进行调整
  // 例如,如果使用 SkSurface::getPixels() 获取像素数据,然后复制到 framebuffer_

  //  这里只是一个占位符,需要你实现具体的内存拷贝逻辑
  // memcpy(embedder->GetFramebufferAddress(), skia_render_buffer, embedder->GetBufferSize());

  return true;
}

int main() {
    // 1. 创建 FramebufferEmbedder 实例
    FramebufferEmbedder embedder("/dev/fb0");  // 替换为你的 Framebuffer 设备路径
    if (!embedder.Initialize()) {
        return 1;
    }

    // 2. 配置 Flutter 引擎
    FlutterRendererConfig renderer_config = {};
    renderer_config.type = kFlutterRendererTypeRaster;
    renderer_config.raster.callback = FlutterFramebufferRender;
    renderer_config.raster.user_data = &embedder;

    FlutterProjectArgs args = {};
    args.assets_path = "flutter_assets"; // 替换为你的 assets 目录
    args.icu_data_path = "icudtl.dat"; // 替换为你的 icudtl.dat 文件路径
    args.renderer_config = renderer_config;

    FlutterEngine engine = nullptr;
    FlutterResult result = FlutterEngineRun(FLUTTER_ENGINE_VERSION, &args, &engine);
    if (result != kFlutterResultSuccess) {
        std::cerr << "Failed to run Flutter engine: " << result << std::endl;
        embedder.Shutdown();
        return 1;
    }

    // 获取屏幕尺寸
    size_t width = embedder.GetScreenWidth();
    size_t height = embedder.GetScreenHeight();

    // 3. 创建 Flutter 窗口
    FlutterWindowDescription window_description = {};
    window_description.width = width;
    window_description.height = height;
    result = FlutterEngineCreateWindow(engine, &window_description);

    if (result != kFlutterResultSuccess) {
        std::cerr << "Failed to create Flutter window: " << result << std::endl;
        FlutterEngineShutdown(engine);
        embedder.Shutdown();
        return 1;
    }

    // 4. 发送初始路由
    result = FlutterEngineSendWindowMetricsEvent(engine,
                                                &(const FlutterWindowMetricsEvent){
                                                    .struct_size = sizeof(FlutterWindowMetricsEvent),
                                                    .width = width,
                                                    .height = height,
                                                    .pixel_ratio = 1.0,
                                                });

    if (result != kFlutterResultSuccess) {
        std::cerr << "Failed to send window metrics event: " << result << std::endl;
        FlutterEngineShutdown(engine);
        embedder.Shutdown();
        return 1;
    }

    const char * initial_route = "/"; // 设置初始路由
    FlutterEngineSendPlatformMessage(
            engine,
            &(const FlutterPlatformMessage){
                    .struct_size = sizeof(FlutterPlatformMessage),
                    .channel = "flutter/navigation",
                    .message = reinterpret_cast<const uint8_t *>(initial_route),
                    .message_size = strlen(initial_route),
                    .response_handle = nullptr,
            }
    );

    // 5. 运行 Flutter 引擎 (保持运行直到手动停止)
    std::cout << "Flutter engine running. Press Enter to quit." << std::endl;
    getchar();  // 等待用户输入

    // 6. 关闭 Flutter 引擎
    FlutterEngineShutdown(engine);
    embedder.Shutdown();

    return 0;
}

代码解释:

  • FramebufferEmbedder 类: 封装了 Framebuffer 的初始化、关闭、以及获取 Framebuffer 信息的逻辑。
  • Initialize() 函数: 打开 Framebuffer 设备,获取屏幕信息,并映射 Framebuffer 内存。
  • Shutdown() 函数: 释放 Framebuffer 内存,关闭设备。
  • FlutterFramebufferRender() 函数: Flutter Engine 渲染完成后的回调函数。 注意: 这个函数需要你实现具体的内存拷贝逻辑,将 Skia 渲染的结果复制到 Framebuffer 中。
  • main() 函数: 创建 FramebufferEmbedder 实例,配置 Flutter Engine,创建 Flutter 窗口,并运行 Flutter 引擎。

重要提示:

  • 你需要安装 Flutter Engine 的 C++ 嵌入器库。
  • 你需要将 flutter_assets 目录和 icudtl.dat 文件复制到正确的位置。 flutter_assets 目录包含你的 Flutter 应用的 Dart 代码和资源文件。 icudtl.dat 文件包含 Unicode 数据。
  • 你需要根据你的 Skia 渲染实现,修改 FlutterFramebufferRender() 函数中的内存拷贝逻辑。
  • 这个示例代码只是一个框架,你需要根据你的具体需求进行修改和完善。 例如,你需要实现输入事件的处理逻辑。

4. 输入事件处理

在嵌入式系统中,输入事件的处理通常比较复杂,因为你需要直接与底层硬件进行交互。你需要监听输入设备(例如触摸屏、键盘),并将事件转换为 Flutter Engine 可以理解的格式。

以下是一些常见的输入设备和事件类型:

  • 触摸屏: 触摸事件(按下、移动、抬起)。
  • 键盘: 按键事件(按下、释放)。
  • 鼠标: 鼠标事件(移动、点击)。

你可以使用 Linux 的 evdev 接口来监听输入事件。 evdev 是一个通用的输入事件接口,可以用于访问各种输入设备。

示例代码(简化的触摸事件处理):

#include <linux/input.h>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>

// 假设你已经打开了触摸屏设备文件描述符 touch_fd
// 并读取了 input_event 事件
void HandleTouchEvent(int touch_fd) {
    struct input_event ev;
    ssize_t n;

    n = read(touch_fd, &ev, sizeof(ev));
    if (n == (ssize_t)-1) {
        perror("read");
        return;
    }

    if (n != sizeof(ev)) {
        errno = EIO;
        perror("read");
        return;
    }

    if (ev.type == EV_ABS) {
        if (ev.code == ABS_X) {
            std::cout << "X: " << ev.value << std::endl;
            // 将 X 坐标转换为 Flutter Engine 可以理解的格式,并发送事件
        } else if (ev.code == ABS_Y) {
            std::cout << "Y: " << ev.value << std::endl;
            // 将 Y 坐标转换为 Flutter Engine 可以理解的格式,并发送事件
        }
        // ... 处理其他触摸事件
    } else if (ev.type == EV_KEY) {
        if (ev.code == BTN_TOUCH) {
            if (ev.value == 1) {
                std::cout << "Touch Down" << std::endl;
                // 发送触摸按下事件
            } else {
                std::cout << "Touch Up" << std::endl;
                // 发送触摸抬起事件
            }
        }
    }
}

// 在主循环中调用 HandleTouchEvent
int main() {
    int touch_fd = open("/dev/input/event0", O_RDONLY); // 替换为你的触摸屏设备路径
    if (touch_fd == -1) {
        perror("open");
        return 1;
    }

    while (true) {
        HandleTouchEvent(touch_fd);
    }

    close(touch_fd);
    return 0;
}

将输入事件传递给 Flutter Engine:

你需要使用 FlutterEngineSendPointerEvent 函数将输入事件传递给 Flutter Engine。这个函数需要一个 FlutterPointerEvent 结构体,其中包含事件的类型、位置、时间戳等信息。

#include "flutter_embedder.h"

// 假设你已经获取了触摸事件的 X 和 Y 坐标
void SendFlutterPointerEvent(FlutterEngine engine, double x, double y, FlutterPointerPhase phase) {
    FlutterPointerEvent event = {};
    event.struct_size = sizeof(FlutterPointerEvent);
    event.phase = phase; // kDown, kMove, kUp, kCancel
    event.x = x;
    event.y = y;
    event.timestamp = FlutterEngineGetCurrentTime(); // 当前时间戳,单位为微秒
    event.pointer = 0;  // 指针 ID,用于区分多个触摸点
    event.device = kFlutterPointerDeviceKindTouch; // 设备类型

    FlutterEngineSendPointerEvent(engine, &event, 1); // 1 表示事件的数量
}

// 例如,在触摸按下事件的处理函数中调用:
// SendFlutterPointerEvent(engine, x, y, kDown);

5. Skia 集成

Flutter Engine 使用 Skia 进行图形渲染。 你需要在你的自定义 Embedder 中集成 Skia,并将 Skia 的渲染结果复制到 Framebuffer 中。

这部分内容比较复杂,涉及到 Skia 的初始化、Surface 创建、渲染循环等。 你可以参考 Skia 的官方文档和示例代码,了解如何将 Skia 集成到你的项目中。

基本步骤:

  1. 初始化 Skia: 创建 GrDirectContext 对象。
  2. 创建 Skia Surface: 基于 Framebuffer 的内存创建一个 SkSurface 对象。 你需要使用 SkImageInfo 结构体来描述 Framebuffer 的像素格式和尺寸。
  3. 渲染循环:FlutterFramebufferRender() 函数中,获取 SkCanvas 对象,使用 Skia API 进行渲染,然后将渲染结果复制到 Framebuffer 中。
  4. 刷新屏幕: 调用 SkSurface::flushAndSubmit() 函数,确保 Skia 的渲染结果被提交到 GPU,并将 Framebuffer 的内容显示在屏幕上。

6. 性能优化

在嵌入式系统上,性能优化至关重要。以下是一些常见的性能优化技巧:

  • 减少渲染区域: 只渲染需要更新的区域,避免全屏刷新。
  • 使用硬件加速: 尽可能利用 GPU 进行硬件加速。
  • 优化 Skia 代码: 避免不必要的绘制操作,使用高效的 Skia API。
  • 使用 AOT 编译: 提前编译 Dart 代码,提高执行效率。
  • 减少内存分配: 避免频繁的内存分配和释放。
  • 选择合适的像素格式: 选择合适的像素格式,例如 RGB565,可以减少内存消耗和带宽需求。

7. 调试技巧

在嵌入式系统上进行调试通常比较困难。以下是一些常用的调试技巧:

  • 使用串口输出: 通过串口输出调试信息。
  • 使用 GDB 调试器: 使用 GDB 调试器远程调试你的应用程序。
  • 使用日志记录: 将日志信息记录到文件中,方便分析问题。
  • 使用性能分析工具: 使用性能分析工具,例如 Perf,分析应用程序的性能瓶颈。

8. 构建和部署

构建和部署 Flutter 应用程序到嵌入式 Linux 系统涉及交叉编译和文件传输。

  1. 交叉编译 Flutter Engine: 为目标架构(例如 ARM)交叉编译 Flutter Engine。
  2. 构建 Flutter 应用: 使用 Flutter SDK 构建你的 Flutter 应用。
  3. 创建部署包: 将 Flutter Engine、Flutter 应用的 assets 目录、icudtl.dat 文件、以及你的自定义 Embedder 打包成一个部署包。
  4. 传输部署包: 使用 SSH、SCP 等工具将部署包传输到嵌入式设备。
  5. 运行应用程序: 在嵌入式设备上运行你的应用程序。

总结一下:

Framebuffer 提供了一种轻量级的图形输出方案,适用于资源受限的嵌入式系统。通过自定义 Flutter Embedder,我们可以直接在 Framebuffer 上运行 Flutter Engine,实现高效、流畅的图形界面。 输入事件处理、Skia 集成、性能优化、调试技巧和构建部署是嵌入式 Flutter 开发的关键环节。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注