几百 KB 内存的 EC600M,怎么让"拍照 + 上传"不 OOM

一、背景

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()` 削峰 |

一句话总结:内存不够时,峰值比总量更重要;而削峰的关键,是让每一块大内存"来得晚、走得早"。

感谢分享。你的四个坑抓得很准,尤其"分片大小取 3 的倍数"与 newline=False 这两点,是 EC600M 这类小内存平台走 HTTP + JSON 上传时最容易踩的隐性坑;"峰值比总量更重要"也是受限 RAM 场景的核心思路。在此基础上补充几个可进一步削峰的方向:

1. 从协议层消灭 base64 膨胀
base64 天然放大 1.33 倍。若服务端可改,HTTP 直接用 multipart/form-data 二进制直传图片,不做 base64,即可省掉整条编码链路与 .decode() 的复制开销,峰值最低。

2. 编码与发送做成流式
不必等整个 bytearray 拼完再发。逐片读、逐片编码、逐片写入 socket(配合 HTTP chunked 或分块上传),峰值就只与 CHUNK 挂钩,与整图大小无关。

3. 用 memoryview 减少拷贝
f.read(CHUNK) 后再切片、拼接会产生临时对象,可用 memoryview 包装读缓冲,buf.extend(...) 追加,减少中间拷贝。

4. GC 时机可再加一档
除你列的关键节点外,可在分配大对象前临时下调自动回收阈值(gc.threshold()),用完复位,让回收更贴近大对象生命周期;但从 DMA 安全角度,仍以主动 gc.collect() 为主、阈值为辅。

5. DMA 持有对象的一致性
你提到全局 _preview 不被回收是对的;建议同类帧缓冲统一登记为长生命周期对象,避免局部变量被 GC 提前回收。

整体结论一致:小内存下"来得晚、走得早"的削峰策略是对的,分片 + 流式 + 主动 GC 是这类流程的稳定组合。

本回答仅供参考。若问题已解决,可标记为 :white_check_mark:解决方案 ;若仍有疑问,请等待官方技术人员回复,最终请以专业解答为准。