原生内存模型与 RAII:C++ 为什么更快也更危险
Kotlin/Java 有 GC,很多对象不用你手动释放。C++ 不一样。C++ 给你更直接的资源控制,也把“什么时候释放、释放几次、释放后还能不能用”的责任交给你。
这篇先把 native 内存最容易踩的坑讲清楚,再引入 RAII 这种工程化资源管理方式。
C++ 里常见的几类内存
栈内存:函数调用期间自动分配和释放
堆内存:用 new/malloc 申请,需要你释放
静态区:全局变量、静态变量,进程生命周期内存在
映射内存:mmap 等系统调用映射的区域
栈像临时桌面。函数进来摆东西,函数走了自动收走。
堆像仓库。你申请一块空间,用完必须归还。
映射内存像借用一段文件或设备窗口,用完也必须解除映射。
为什么 native 内存错误更严重
Kotlin 空指针通常会抛异常,堆栈还能清楚指到业务代码。
C++ 内存错误可能直接破坏进程地址空间,表现为 native crash。
常见错误:
Use After Free:释放后继续使用
Double Free:同一块内存释放两次
Buffer Overflow:写出数组边界
Memory Leak:忘记释放
Null Pointer:空指针解引用
Data Race:多个线程同时改同一块内存
Use After Free
Packet* packet = new Packet();
delete packet;
packet->ptsUs = 1000;
delete 后,packet 指针变量还存着原地址,但那块内存已经不属于你。继续写入就是 UAF。
这类 bug 很难凭肉眼定位,因为释放后那块内存可能还没被别人复用,看起来“偶尔正常”。
Double Free
uint8_t* buffer = new uint8_t[1024];
delete[] buffer;
delete[] buffer;
第二次释放会破坏分配器内部状态,可能当场崩,也可能过一会儿在完全不相关的位置崩。
Buffer Overflow
uint8_t buffer[4];
buffer[4] = 1;
数组下标从 0 开始,buffer[4] 已经越界。越界写可能覆盖旁边变量、返回地址或 allocator 元数据。
RAII 是什么
RAII 是 Resource Acquisition Is Initialization。人话说:资源跟对象生命周期绑定。
构造函数里拿资源
析构函数里释放资源
对象离开作用域时自动析构
它像借书卡:对象创建时借书,对象销毁时自动还书,避免你忘记。
用 unique_ptr 管对象
std::unique_ptr<Packet> makePacket() {
auto packet = std::make_unique<Packet>();
packet->ptsUs = 1000;
return packet;
}
unique_ptr 表示唯一所有权。离开作用域时,它会自动 delete。
用自定义 deleter 管 C API
NDK C API 常见创建/释放成对出现。
AMediaExtractor_new -> AMediaExtractor_delete
AMediaFormat_new -> AMediaFormat_delete
ANativeWindow_fromSurface -> ANativeWindow_release
可以用 unique_ptr 包装。
struct ExtractorDeleter {
void operator()(AMediaExtractor* extractor) const {
if (extractor != nullptr) {
AMediaExtractor_delete(extractor);
}
}
};
using ExtractorPtr = std::unique_ptr<AMediaExtractor, ExtractorDeleter>;
ExtractorPtr createExtractor() {
return ExtractorPtr(AMediaExtractor_new());
}
这样函数提前返回、异常、错误分支都不会漏释放。
播放器资源所有权图
PlayerController
owns DemuxEngine
owns AMediaExtractor
owns VideoDecoder
owns AMediaCodec
owns SurfaceSession
owns ANativeWindow
owns PacketQueue
谁创建,谁释放。谁拥有,谁负责生命周期。不要让多个对象同时以为自己拥有同一份资源。
错误分支为什么最容易泄漏
AMediaExtractor* extractor = AMediaExtractor_new();
if (!open(extractor)) {
return false;
}
AMediaExtractor_delete(extractor);
打开失败时直接 return,extractor 没释放。
RAII 改写:
ExtractorPtr extractor(AMediaExtractor_new());
if (!open(extractor.get())) {
return false;
}
return true;
无论成功失败,extractor 离开作用域都会释放。
本章实验
先写一个有泄漏的 extractor 打开函数。
再用 RAII 包装它。
人为制造 open 失败,确认释放逻辑仍然执行。
建议给每个资源加日志。
Extractor create
Extractor delete
Codec create
Codec delete
Window acquire
Window release
日志成对出现,资源生命周期才清楚。
工程风险与观测
native 资源释放要做成可审计项,而不是靠代码 review 肉眼记忆。
建议为每类资源统计计数。
extractor_created / extractor_deleted
codec_created / codec_deleted
window_acquired / window_released
thread_started / thread_joined
packet_allocated / packet_released
release 后,这些计数应该回到平衡状态。
如果不平衡,先不要继续加功能。资源释放问题会放大所有并发问题,尤其是在播放器反复打开、关闭、seek、切后台时。
发布前最少做一次压力实验。
打开播放器
播放 5 秒
退出页面
重复 100 次
观察 native heap 和资源计数
如果 native heap 持续上涨,就要回到所有权图逐项审计。
本章结论
C++ 不是危险在“语法难”,而是资源生命周期不自动兜底。NDK 工程要尽早建立所有权模型,用 RAII 管住 C API 指针、窗口、codec、队列和线程。资源边界清楚,崩溃会少很多。