OBS Studio 内存管理 API 详解:bmem 模块(libobs/util)的分配、对齐与泄漏计数
本篇围绕 OBS Studio 文档中的 libobs/util 内存管理模块(bmem)展开,系统讲解 bmalloc、brealloc、bfree 等分配函数的语义与设计动机,并结合 libobs/util/bmem.c 的源码剖析其 32 字节内存对齐技巧、原子泄漏计数机制,以及前端程序如何利用 bnum_allocs() 在退出时输出内存泄漏数量,帮助开发者在编写 OBS 插件与扩展时正确使用这套内存 API 并规避泄漏。
模块定位:libobs/util 中的 Memory Management
OBS Studio 的 libobs 基础库提供了跨平台的工具层(util),其 API 参考文档以 docs/sphinx/reference-libobs-util.rst 为入口,其中 Memory Management(内存管理)对应 docs/sphinx/reference-libobs-util-bmem.rst。该模块的接口声明位于 libobs/util/bmem.h,实现位于 libobs/util/bmem.c,使用方只需包含一个头文件:
#include <util/bmem.h>
之所以不直接使用 C 标准库的 malloc/realloc/free,源码中的注释给出了直接原因(见 libobs/util/bmem.c):项目借鉴了 FFmpeg 的内存对齐技巧,让每次分配返回 32 字节对齐的内存,以满足 SIMD 指令集等对齐要求;同时因为类 Unix/POSIX 系统没有"带对齐保证的 realloc",直接改用 posix_memalign() 会破坏 realloc 场景下的对齐不变式,因此作者选择了一套跨平台统一的对齐实现。
核心函数逐一解析
文档中列出的函数签名如下(源自 reference-libobs-util-bmem.rst):
| 函数 | 原型 | 说明 |
|---|---|---|
| 分配 | void *bmalloc(size_t size) |
分配内存并使泄漏计数器 +1 |
| 重分配 | void *brealloc(void *ptr, size_t size) |
重分配,仅可用于 bmalloc 分配的内存 |
| 释放 | void bfree(void *ptr) |
释放 bmalloc 分配的内存 |
| 计数 | long bnum_allocs(void) |
返回当前存活的分配次数(active allocations) |
| 复制 | void *bmemdup(const void *ptr, size_t size) |
复制一段任意内存 |
| 零化分配 | void *bzalloc(size_t size) |
内联函数,分配并清零 |
| 限长字符串复制 | char *bstrdup_n(const char *str, size_t n) / wchar_t *bwstrdup_n(const wchar_t *str, size_t n) |
复制 n 字节字符串并自动补结尾零 |
| 字符串复制 | char *bstrdup(const char *str) / wchar_t *bwstrdup(const wchar_t *str) |
完整字符串复制 |
bmalloc:拒绝 0 字节分配 + 分配计数
实现见 libobs/util/bmem.c:
void *bmalloc(size_t size)
{
if (!size) {
os_breakpoint();
bcrash("bmalloc: Allocating 0 bytes is broken behavior, please fix your code!");
}
void *ptr = a_malloc(size);
if (!ptr) {
os_oom();
bcrash("Out of memory while trying to allocate %lu bytes", (unsigned long)size);
}
os_atomic_inc_long(&num_allocs);
return ptr;
}
三个要点:
size == 0直接触发崩溃断言。文档只说"分配内存并增加泄漏计数",但源码明确把 0 字节分配视为必须修复的编码错误,并调用os_breakpoint()进入调试器断点、随后bcrash记录错误。这是有意为之:bmalloc(0)往往意味着调用方存在size计算错误,宁可早期崩溃暴露,也不返回看似合法的空指针。- 分配失败同样直接崩溃(
os_oom()+bcrash),而不是返回 NULL 让调用方逐处判空。 os_atomic_inc_long(&num_allocs)使用原子操作维护全局计数器num_allocs,因此多线程插件并发分配也是安全的。
brealloc:计数只在新分配时增加
brealloc 的实现见 libobs/util/bmem.c。一个容易被忽略的细节:
void *brealloc(void *ptr, size_t size)
{
if (!ptr)
os_atomic_inc_long(&num_allocs);
...
}
只有当传入 ptr 为 NULL(即等价于首次分配)时才增加计数;对已有块的扩容不改变计数。配合 bfree 中"非空才递减"的逻辑(libobs/util/bmem.c),bnum_allocs() 始终等于"未释放的分配块数量"。文档强调"仅能用于 bmalloc() 分配的内存",从源码结构看原因很直接:对齐 hack 把分配偏移量 diff 写在了返回指针的前一个字节里(((char *)ptr)[-1]),a_realloc/a_free 依赖这一私有约定还原原始 malloc 指针;用 realloc 或第三方分配的指针传入会破坏该约定导致越界写。
平台差异:Windows 的 _aligned_malloc 与非 Windows 的对齐 hack
这是 bmem 模块最有价值的实现细节,见 libobs/util/bmem.c:
#define ALIGNMENT 32
#if defined(_WIN32)
#define ALIGNED_MALLOC 1 // 使用 _aligned_malloc / _aligned_realloc / _aligned_free
#else
#define ALIGNMENT_HACK 1 // 多申请 32 字节,返回一个 32 字节对齐的偏移指针
#endif
- Windows:直接映射到 CRT 的
_aligned_malloc(size, 32)、_aligned_realloc、_aligned_free。 - 其他平台(ALIGNMENT_HACK):
a_malloc多申请size + ALIGNMENT字节,计算diff = ((~(long)ptr) & (ALIGNMENT - 1)) + 1使返回地址 32 字节对齐,并把diff存入返回指针前一个字节;a_realloc与a_free读出diff还原原始块指针后再调用标准库。
该对齐值对外可通过 base_get_alignment() 查询(声明于 libobs/util/bmem.h,返回 ALIGNMENT,实现见 libobs/util/bmem.c),源码注释解释了为何不直接换用 posix_memalign():POSIX 没有对齐版 realloc,混用会破坏重分配后内存的对齐保证。
bmemdup 与内联字符串工具
bmemdup 语义是"分配 + memcpy":
void *bmemdup(const void *ptr, size_t size)
{
void *out = bmalloc(size);
if (size)
memcpy(out, ptr, size);
return out;
}
注意它返回的是 bmalloc 分配的可对齐内存,因此释放时必须配对 bfree。
bzalloc、bstrdup_n、bwstrdup_n、bstrdup、bwstrdup 均为头文件中的 static inline 函数(libobs/util/bmem.h),核心是"分配 + 内容填充"的组合:
bzalloc(size):bmalloc后memset(mem, 0, size);bstrdup_n(str, n):bmemdup(str, n + 1)后强制dup[n] = 0,即只取前 n 字节并保证终止零——这正是"n 字节 + 自动补零"语义的来源,适合处理长度已知但结尾未必终止的缓冲区;bwstrdup_n同理,按(n + 1) * sizeof(wchar_t)计算字节数;bstrdup/bwstrdup分别以strlen/wcslen作为 n 调用上一组函数。
所有字符串函数对 NULL 输入均返回 NULL,不抛错。这套工具在 libobs 内部被大量使用,例如 libobs/obs-hotkey.c、libobs/graphics/effect-parser.c、libobs/obs-canvas.c 等核心文件都有 bstrdup/bstrdup_n 的调用,是 OBS 各模块复制名称、路径、配置字符串时的统一入口。
bnum_allocs:把泄漏检测做成常驻能力
bnum_allocs() 返回全局 num_allocs 的当前值(libobs/util/bmem.c)。OBS 前端把它用作退出时的泄漏自检:程序收尾处会打印一行日志
blog(LOG_INFO, "Number of memory leaks: %ld", bnum_allocs());
见 frontend/obs-main.cpp。测试程序同样如此,Windows 测试在 test/win/test.cpp 打印该值,macOS 测试在 test/osx/test.mm 使用 NSLog(@"Number of memory leaks: %lu", bnum_allocs())。这意味着每次运行 OBS Studio 或其后端测试,日志尾部都会出现 "Number of memory leaks: N";N 应为 0,若大于 0 即提示存在未配对的 bmalloc/brealloc。对插件开发者而言,这是排查泄漏最直接的验证手段:在本地跑一遍录制/推流场景,观察日志尾部的该计数即可判断插件自身的分配是否全部释放。
使用约定与注意事项
- 配对原则:
bmalloc/brealloc/bmemdup/bzalloc/bstrdup*分配的内存一律用bfree释放;brealloc只接受 bmem 家族的指针,不要混用标准库分配函数。 - size 为 0 是错误:
bmalloc(0)与brealloc(ptr, 0)会触发断点并崩溃,属于"broken behavior",代码评审中应直接修正 size 计算逻辑。 - 失败即崩溃:分配失败不会返回 NULL,而是进入
os_oom()+bcrash流程,调用方无需(也无法)做空指针恢复。 - 对齐保证:
bmalloc返回 32 字节对齐地址,可用base_get_alignment()查询对齐值;依赖 SIMD 的自定义插件可以安全地对该内存做对齐指针类型转换。 - 线程安全:计数使用原子自增/自减,多线程并发分配不会污染
bnum_allocs的准确性。 - 扩展点:头文件中还定义了
struct base_allocator(含 malloc/realloc/free 三个函数指针,见 libobs/util/bmem.h);从源码结构看,当前 libobs/util/bmem.c 的bmalloc/brealloc/bfree内部直接调用静态的a_malloc/a_realloc/a_free,该结构体在当前仓库中未见其他引用,可理解为为将来可替换分配器预留的接口形态。
小结
bmem 模块用一组语义清晰的小函数(bmalloc/brealloc/bfree/bmemdup/bzalloc/bstrdup*)替换裸标准库分配,换来三件对长期运行的直播/录制软件至关重要的事:统一的 32 字节对齐、常驻且线程安全的泄漏计数、以及对编码错误(0 字节分配、分配失败)的早期崩溃暴露。配合退出时 frontend/obs-main.cpp 输出的 "Number of memory leaks" 日志,它为 OBS 核心与第三方插件建立了一条可验证的内存纪律。编写插件时遵循"bmalloc 配 bfree、size 不为 0、退出前计数归零"三条约定,即可与该模块的设计目标保持一致。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00