一、背景
EC600M 的可用 RAM 只有几百 KB(实测空闲约 957KB)。而一次"拍照、base64 编码、上传"是全流程内存峰值最高的环节,比拍照本身还高。
本文记录我在这个环节踩的坑和最终做法。
二、坑 1:base64 才是内存大户
照片是二进制,上传走 HTTP + JSON(文本协议),所以必须 base64 编码。问题是:
- base64 后体积变成原来的 1.33 倍;
- `.decode()` 成字符串时还会再复制一份。
100KB 的照片,编码加解码过程可能瞬时吃掉 200KB 以上。在 PC 上无感,在这里足以触发 `MemoryError` 或板卡复位。
做法:分片读加逐片编码。不要 `f.read()` 一次读完整张照片,而是逐片读、逐片编码、拼接到 bytearray:
```python
CHUNK = 240 # 必须是 3 的倍数
buf = bytearray()
while True:
chunk = f.read(CHUNK)
if not chunk:
break
buf += b2a_base64(chunk, newline=False) # 关键:newline=False
```
三、坑 2:base64 的两个"隐形杀手"
这两个坑非常隐蔽,我各踩了一次。
1. 分片大小必须是 3 的倍数
base64 是"每 3 字节编码为 4 字符"。如果分片大小不是 3 的倍数,每片末尾都会被补等号,拼接后中间夹着一堆等号,整串非法,服务端直接 400。取 240(等于 3 乘 80)正好整除。
2. 必须 newline=False
MicroPython 的 `b2a_base64()` 默认每次调用末尾加一个换行符 `\n`。分片拼接后中间全是换行,同样导致解码失败。要么传 `newline=False`,要么 `.rstrip(b"\n")` 兜底。
一个小小的换行,让我对着"上传返回 400"排查了很久。
四、坑 3:GC 和 DMA 是会打架的
这点最反直觉:`gc.collect()` 不是越勤越好。
原因有两个:
1. 它是同步阻塞的,会停顿当前线程几十到几百毫秒。所以只能放在"没有实时性要求、且刚产生过大对象"的间隙,比如拍照后、上传后。
2. GC 会移动或释放内存。如果某个对象正被硬件 DMA 使用(比如摄像头帧缓冲、音频播放缓冲),GC 在 DMA 还没读完时把它回收,DMA 就取到野指针,直接 dump。
所以:
- 要被硬件长期持有的对象,必须放在全局变量里,不能被 GC 回收(比如全局的 `_preview`)。
- 用完的对象,才在安全的节点主动 `gc.collect()`。
```python
# 上传成功后,立刻释放大对象并回收
del b64, payload
gc.collect()
```
五、坑 4:等自动 GC 就太晚了
自动 GC 的触发时机是"分配失败时才回收",等它触发,往往已经太晚。所以我在关键节点主动回收:
- 拍照前关预览,回收一次
- 拍照对象用完立刻释放,削内存峰值
- base64 生成后回收读取缓冲
- 上传成功后 `del` 再 `collect()`
六、小结
| 坑 | 解法 |
|—|—|
| base64 内存翻倍 | 分片读加逐片编码,避免一次性大对象 |
| 分片补 `=` 导致 400 | 分片大小取 3 的倍数(如 240) |
| 末尾换行导致 400 | 用 `b2a_base64(…, newline=False)` |
| GC 与 DMA 冲突 | 硬件持有的对象放全局,不被回收 |
| 自动 GC 太晚 | 关键节点主动 `gc.collect()` 削峰 |
一句话总结:内存不够时,峰值比总量更重要;而削峰的关键,是让每一块大内存"来得晚、走得早"。