3. 内存优化:动态内存管理、静态分配策略、内存池配置
内存优化,说白了就是让 TouchGFX 在有限的 RAM 里跑得稳、跑得快。我见过太多项目,UI 卡顿、随机崩溃,最后查下来都是内存惹的祸。这一章咱们就把内存这块硬骨头啃下来。
3.1 动态内存管理:能不用就别用
TouchGFX 本身支持动态内存分配,比如 new、malloc。但我个人的习惯是——在嵌入式 UI 里,尽量避开动态分配。为什么?
- 碎片问题:频繁分配释放,堆会碎成一片一片。你想想看,明明总内存还有,但就是分配不出一块连续的大空间。
- 不确定性:分配耗时不可控。UI 渲染需要 60fps 的稳定性,一次
malloc卡了几毫秒,帧率就掉了。 - 调试困难:堆溢出、野指针,这些问题在资源紧张的 MCU 上排查起来非常痛苦。
new 失败返回了空指针。从那以后,我对动态分配就格外谨慎。
如果实在要用,记住两个原则:
- 初始化时分配,运行时不再释放。比如在
setupScreen()里一次性 new 好所有对象。 - 使用 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 字节对齐,性能最好。
下面这张图展示了内存池的核心逻辑:
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修饰。
一个小技巧:在调试阶段,可以给内存池加一个统计功能,记录最大使用量、分配失败次数。这样上线前就能发现容量瓶颈。