内存优化基础:RAM与Flash的分配策略、减少动态内存分配、使用静态缓冲池

做TouchGFX开发,说白了就是跟内存打交道。我刚开始接手一个复杂UI项目时,总觉得内存不够用,动不动就崩。后来才明白,不是内存真的少,而是我没用好。这一章,咱们就聊聊内存分配的那些事儿。

RAM与Flash:谁该放什么?

先搞清楚两个概念。Flash是只读的,存代码、字体、图片这些不变的东西。RAM是读写的,存变量、缓存、动态数据。TouchGFX的UI框架本身会占用一部分RAM,你的业务逻辑、动画状态、屏幕缓存,全挤在RAM里。

我见过不少新手,把大图片直接当变量声明,结果RAM爆了。其实图片应该放Flash,运行时只加载到RAM里显示。你想想看,一张800x480的24位图,差不多1.1MB,RAM才多大?

存储类型 典型容量 适合存放的内容 不适合的内容
Flash 512KB ~ 16MB 代码、字体、图片、字符串常量 运行时变量、动态缓存
RAM 64KB ~ 2MB 栈、堆、屏幕缓存、对象实例 大块静态资源(图片、字体)

核心原则:能放Flash的,绝不占RAM。TouchGFX的BitmapCache默认从Flash加载,别自己再copy一份到RAM里。

减少动态内存分配

动态分配,就是运行时用malloc、new去申请内存。TouchGFX的UI框架本身很少用动态分配,但你的业务代码里可能到处都是。为什么说它不好?

  • 碎片化:频繁申请释放,内存越切越碎,最后大块申请失败。
  • 不可预测:你不知道哪次分配会失败,UI可能突然卡死。
  • 性能开销:每次分配都要走堆管理算法,费时。

我有个项目,客户反映UI偶尔闪退。查了两天,发现是一个弹窗里用了std::vector动态扩容,堆碎片导致分配失败。改成静态数组后,再没出过问题。

避坑指南:我曾经在TouchGFX的Screen构造函数里调用了new,结果切换屏幕时内存泄漏。后来全部改用预分配对象池,稳如老狗。

使用静态缓冲池

静态缓冲池,说白了就是提前在RAM里划一块固定区域,专门用来分配固定大小的对象。不用malloc,不用new,全在编译期定好。

TouchGFX里最典型的应用就是BitmapCache。它内部维护了一个静态缓冲池,用来缓存解码后的图片。你只需要在TouchGFXConfiguration.cpp里配置大小:

// 在 TouchGFXConfiguration.cpp 中
static uint8_t bitmapCacheBuffer[BITMAP_CACHE_SIZE];
BitmapCache::setCacheBuffer(bitmapCacheBuffer, BITMAP_CACHE_SIZE);

这样,所有图片解码后的数据都从这个池子里取,不会碎片化,也不会泄漏。

我自己写业务代码时,也喜欢用静态池。比如一个列表控件,最多显示20项,我就声明一个20个元素的数组,循环使用:

#define MAX_LIST_ITEMS 20
static ListItem listItems[MAX_LIST_ITEMS];
static uint8_t itemIndex = 0;

ListItem* getNextListItem() {
    ListItem* item = &listItems[itemIndex];
    itemIndex = (itemIndex + 1) % MAX_LIST_ITEMS;
    return item;
}

这样,没有动态分配,没有内存泄漏,性能还高。你想想看,是不是比每次new一个对象靠谱多了?

小技巧:如果你的UI有多个屏幕,每个屏幕的控件数量是固定的,可以在Screen类里声明静态成员变量。这样所有屏幕实例共享同一块内存,省空间。

内存分配策略对比

策略 优点 缺点 适用场景
动态分配 灵活,按需分配 碎片化、不可预测、性能差 极少使用,仅用于启动时一次性分配
静态全局数组 简单、无碎片、性能高 大小固定,浪费空间 控件池、缓存、固定大小的数据结构
静态缓冲池 可复用、无碎片、可控 实现稍复杂 图片缓存、消息队列、对象池

一张图看懂内存分配策略

下面这张图,是我自己总结的内存分配决策流程。每次写新功能前,我都会过一遍:

内存分配策略决策流程图 内存分配策略决策流程 需要分配内存 大小是否固定? 是 否 静态全局数组 是否频繁分配? 静态缓冲池 动态分配(慎用) 建议:90%的场景用静态数组或缓冲池 动态分配只用于启动时一次性申请

嗯,这张图我用了好几年。每次新功能来了,先问自己:大小固定吗?频繁分配吗?答案走下来,基本就知道该用哪种策略了。

实战:配置TouchGFX的静态缓冲池

最后,给一个完整的配置示例。假设你的MCU有512KB RAM,你打算给图片缓存留128KB:

// 在 TouchGFXConfiguration.cpp 中
#define BITMAP_CACHE_SIZE (128 * 1024)  // 128KB

static uint8_t bitmapCacheBuffer[BITMAP_CACHE_SIZE];

void touchgfx_init() {
    // 设置缓存池
    BitmapCache::setCacheBuffer(bitmapCacheBuffer, BITMAP_CACHE_SIZE);
    
    // 设置帧缓冲(双缓冲)
    static uint16_t frameBuffer0[480 * 272];  // 假设分辨率480x272
    static uint16_t frameBuffer1[480 * 272];
    HAL::getInstance()->setFrameBufferStartAddresses(
        (uint16_t*)frameBuffer0, 
        (uint16_t*)frameBuffer1
    );
    
    // 其余初始化...
}

这样配置后,所有图片解码都走静态池,不会产生堆碎片。帧缓冲也是静态的,不会动态申请。整个UI的内存行为变得可预测、可控。

个人习惯:我一般把缓存池大小设为总RAM的25%~30%。剩下的留给栈、全局变量和TouchGFX框架本身。如果图片多,可以适当调大,但别超过50%,否则其他模块可能不够用。

内存优化,说白了就是「预则立,不预则废」。提前规划好分配策略,比出了问题再调优要省心得多。这一章的内容,够你在实际项目中少踩不少坑了。