3. 内存优化:动态内存管理、静态分配策略、内存池配置

内存优化,说白了就是让 TouchGFX 在有限的 RAM 里跑得稳、跑得快。我见过太多项目,UI 卡顿、随机崩溃,最后查下来都是内存惹的祸。这一章咱们就把内存这块硬骨头啃下来。

3.1 动态内存管理:能不用就别用

TouchGFX 本身支持动态内存分配,比如 new、malloc。但我个人的习惯是——在嵌入式 UI 里,尽量避开动态分配。为什么?

  • 碎片问题:频繁分配释放,堆会碎成一片一片。你想想看,明明总内存还有,但就是分配不出一块连续的大空间。
  • 不确定性:分配耗时不可控。UI 渲染需要 60fps 的稳定性,一次 malloc 卡了几毫秒,帧率就掉了。
  • 调试困难:堆溢出、野指针,这些问题在资源紧张的 MCU 上排查起来非常痛苦。
我曾经在一个项目里用了动态分配来创建弹窗,结果运行 2 小时后 UI 突然卡死。查了一整天,发现是堆碎片导致 new 失败返回了空指针。从那以后,我对动态分配就格外谨慎。

如果实在要用,记住两个原则:

  1. 初始化时分配,运行时不再释放。比如在 setupScreen() 里一次性 new 好所有对象。
  2. 使用 RAII 封装,确保异常安全。

3.2 静态分配策略:把内存管死在编译期

静态分配,就是所有对象在编译期就确定好大小和位置。这是嵌入式 UI 最稳妥的做法。

TouchGFX 的 Model、Screen、View、Presenter 这些核心对象,默认就是静态分配的。但很多开发者会在自定义组件里踩坑。

核心原则:能用 constexpr 就别用 const,能用 static 就别用全局变量,能用栈就别用堆。

我建议你养成一个习惯:在项目初期就画一张内存布局图。比如这样:

区域 内容 大小(示例)
.text 代码 + 只读数据 512 KB
.data 已初始化全局变量 64 KB
.bss 未初始化全局变量 128 KB
堆 动态分配 32 KB
栈 局部变量、函数调用 16 KB
帧缓冲 UI 渲染缓冲区 300 KB
内存池 自定义对象池 48 KB

嗯,有了这张表,你就能一眼看出哪里是瓶颈。我在项目中遇到过,帧缓冲占了 60% 的 RAM,剩下的空间捉襟见肘。后来通过压缩颜色深度,才把内存池挤出来。

3.3 内存池配置:给对象一个专属仓库

内存池(Memory Pool),说白了就是预先划好一块区域,专门用来分配固定大小的对象。它没有碎片,分配速度是 O(1),非常适合 UI 中频繁创建销毁的小对象——比如 TextArea、Image、Button。

TouchGFX 提供了 MemoryPool 模板类,用法很简单:

// 定义一个内存池,最多容纳 10 个 MyWidget 对象
MemoryPool<MyWidget, 10> widgetPool;

// 从池中分配
MyWidget* w = widgetPool.allocate();
if (w != nullptr) {
    // 使用 w
}

// 释放回池中
widgetPool.free(w);

我个人习惯:把内存池的大小设成 2 的幂次,比如 8、16、32。这样内部对齐更高效,而且不容易浪费。

配置内存池时,有几个关键参数:

  • 对象大小:每个元素占多少字节。注意对齐,通常 4 字节或 8 字节。
  • 池容量:最多同时存在多少个对象。这个值要仔细算,多了浪费,少了会分配失败。
  • 对齐方式:ARM Cortex-M 系列建议 8 字节对齐,性能最好。

下面这张图展示了内存池的核心逻辑:

内存池(Memory Pool)核心结构 内存池(连续内存块,总大小 = 对象大小 × 容量) 对象 0 对象 1 对象 2 对象 3 空闲 空闲 ... 空闲链表(Free List) 空闲头指针 对象 2(已释放) 空闲块 NULL 分配 / 释放流程 allocate() 从空闲链表头部取一个 free() 放回空闲链表头部 O(1) 时间复杂度 无碎片,性能稳定

3.4 实战配置建议

说了这么多理论,咱们来点实际的。我在一个 STM32H750 的项目里,是这样配置内存的:

// TouchGFX 内存池配置示例
#define MAX_WIDGETS      32
#define MAX_IMAGES       16
#define MAX_TEXT_AREAS   24

// 为不同类型创建独立内存池
MemoryPool<CustomButton, MAX_WIDGETS>    buttonPool;
MemoryPool<CustomImage, MAX_IMAGES>      imagePool;
MemoryPool<CustomTextArea, MAX_TEXT_AREAS> textPool;

// 初始化时预分配
void initMemoryPools() {
    // 预分配所有按钮
    for (int i = 0; i < MAX_WIDGETS; i++) {
        CustomButton* btn = buttonPool.allocate();
        if (btn) {
            btn->setWidth(100);
            btn->setHeight(40);
            // 其他初始化...
        }
    }
}

关键点:每个池只管理一种固定大小的对象。这样内部管理最简单,性能也最好。如果你把不同大小的对象混在一个池里,那就退化成堆了,碎片问题照样会出现。

3.5 避坑指南

嗯,这里有几个我踩过的坑,分享给你:

  • 池容量估算不足:UI 里有些对象是动态创建销毁的,比如弹窗、列表项。一定要算清楚最大并发数。我曾经因为少算了 2 个,导致特定操作下分配失败,UI 直接白屏。
  • 忘记释放:内存池虽然快,但也要手动 free。我建议在对象的析构函数里自动归还,或者用 RAII 包装一下。
  • 对齐问题:有些 MCU 要求 8 字节对齐,否则访问会触发异常。TouchGFX 的 MemoryPool 默认对齐到 alignof(T),但如果你自定义了对齐要求,记得用 alignas 修饰。

一个小技巧:在调试阶段,可以给内存池加一个统计功能,记录最大使用量、分配失败次数。这样上线前就能发现容量瓶颈。