首页
/ OBS Studio 内存管理 API 详解:bmem 模块(libobs/util)的分配、对齐与泄漏计数

OBS Studio 内存管理 API 详解:bmem 模块(libobs/util)的分配、对齐与泄漏计数

2026-09-05 16:46:41作者:毕习沙Eudora

本篇围绕 OBS Studio 文档中的 libobs/util 内存管理模块(bmem)展开,系统讲解 bmallocbreallocbfree 等分配函数的语义与设计动机,并结合 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;
}

三个要点:

  1. size == 0 直接触发崩溃断言。文档只说"分配内存并增加泄漏计数",但源码明确把 0 字节分配视为必须修复的编码错误,并调用 os_breakpoint() 进入调试器断点、随后 bcrash 记录错误。这是有意为之:bmalloc(0) 往往意味着调用方存在 size 计算错误,宁可早期崩溃暴露,也不返回看似合法的空指针。
  2. 分配失败同样直接崩溃os_oom() + bcrash),而不是返回 NULL 让调用方逐处判空。
  3. 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_realloca_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

bzallocbstrdup_nbwstrdup_nbstrdupbwstrdup 均为头文件中的 static inline 函数(libobs/util/bmem.h),核心是"分配 + 内容填充"的组合:

  • bzalloc(size)bmallocmemset(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.clibobs/graphics/effect-parser.clibobs/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。对插件开发者而言,这是排查泄漏最直接的验证手段:在本地跑一遍录制/推流场景,观察日志尾部的该计数即可判断插件自身的分配是否全部释放。

使用约定与注意事项

  1. 配对原则bmalloc/brealloc/bmemdup/bzalloc/bstrdup* 分配的内存一律用 bfree 释放;brealloc 只接受 bmem 家族的指针,不要混用标准库分配函数。
  2. size 为 0 是错误bmalloc(0)brealloc(ptr, 0) 会触发断点并崩溃,属于"broken behavior",代码评审中应直接修正 size 计算逻辑。
  3. 失败即崩溃:分配失败不会返回 NULL,而是进入 os_oom() + bcrash 流程,调用方无需(也无法)做空指针恢复。
  4. 对齐保证bmalloc 返回 32 字节对齐地址,可用 base_get_alignment() 查询对齐值;依赖 SIMD 的自定义插件可以安全地对该内存做对齐指针类型转换。
  5. 线程安全:计数使用原子自增/自减,多线程并发分配不会污染 bnum_allocs 的准确性。
  6. 扩展点:头文件中还定义了 struct base_allocator(含 malloc/realloc/free 三个函数指针,见 libobs/util/bmem.h);从源码结构看,当前 libobs/util/bmem.cbmalloc/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、退出前计数归零"三条约定,即可与该模块的设计目标保持一致。

登录后查看全文
热门项目推荐
相关项目推荐