本节摘要:Blender 插件的内存问题横跨三个互不兼容的内存域——解释器堆(引用计数加周期回收)、内核内存池(手动分配释放)、GPU 显存(延迟销毁)。三域缺乏统一生命周期协议,泄漏常以诡异方式显现:Python 对象已删而 C 结构仍在、纹理已释放而包装器还在访问。本节讲 Python 域的弱引用契约、C 域的显式释放、GPU 域的"创建-使用-释放"铁律,以及对应的三套诊断手段。
阅读完本节,你应当能够:
del 一个数据块引用为什么不会释放内存;bpy 中的每个数据块有两重身份:Python 层是类型实例,C 层是结构体。关键认知:Python 对象的生命周期不等于 C 数据块的生命周期。
mesh = bpy.data.meshes['Cube'] del mesh # Python 引用消失了 但数据块仍在 集合里还能取到
del 只减少引用计数,而主数据集合本身持有一个强引用。真正的释放需要两个条件同时满足:无任何引用指向它,且显式调用了移除方法。否则 C 结构持续占用内存直到退出。
比"删不掉"更危险的是幽灵引用:插件常缓存 context.object 供后续操作,用户切换或重载文件后,原对象的 Python 变量还在,C 指针可能已被内核回收——此时访问其名称触发"结构已被移除"的引用错误。这不是 bug,是 Blender 防止悬垂指针的主动防护。
破解之道是拥抱弱引用契约:对主数据集合里的对象,优先用名称字符串索引(按名取、判空再用),而非长期持有对象;必须缓存时用弱引用并在每次使用前检查是否仍存活;订阅移除事件及时清理关联缓存。
import weakref class ObjectCache: def __init__(self): self._refs = {} def remember(self, key, obj): self._refs[key] = weakref.ref(obj) def fetch(self, key): ref = self._refs.get(key) obj = ref() if ref else None if obj is None or obj.name not in bpy.data.objects: self._refs.pop(key, None) # 已死 引连同缓存一起清 return None return obj
💡 关键直觉:把对象引用当"借来的书"——用完就还(用名字重新借),别把书藏在抽屉里(长期持有),图书馆可能已经把它下架了(数据块被删)。
内核大量使用内存池管理小对象(顶点、边、面)。网格编辑模块是典型:创建的编辑会话内存不属于解释器堆,而是内核的内存池。**删掉 Python 侧的变量不会释放这些内存,必须显式调用释放方法。**忘记配对,表现为进程内存稳步攀升,重启才恢复——这类"温水式泄漏"比崩溃更难排查,因为它不留下任何堆栈。
实践统计里,超半数插件内存问题源于三类未配对操作:网格编辑会话建了没放、某类笔刷数据加了没删、集合属性添了条目没清。防御手段是模式化写法——凡获取需释放的资源,一律用 try/finally 或上下文管理包住:
import bmesh def edit_and_release(obj): bm = bmesh.new() try: bm.from_mesh(obj.data) bmesh.ops.triangulate(bm, faces=bm.faces) bm.to_mesh(obj.data) finally: bm.free() # 铁律:创建与释放配对 不依赖回收器
诊断工具用内核的内存调试启动参数:它启用内置分配跟踪,能输出精确到调用栈的分配清单。对照"申请了没释放"的条目,泄漏源头一目了然。
GPU 纹理与缓冲是稀缺资源,且有延迟销毁特性:Python 侧删除纹理对象,底层图形接口的纹理标识未必立即释放——要等 GPU 空闲。循环里快速创建销毁纹理,极易触发显存耗尽错误。
正确姿势是显式资源管理协议:创建、使用、释放三段明确,绝不依赖回收器:
import gpu def render_pass(size): tex = gpu.types.GPUTexture(size, format='RGBA8') try: # 使用纹理 执行绘制与采样 use(tex) finally: tex.free() # 立即归还显存
着色器同理:内置着色器绑定后要配对解绑。这个协议的深层意义在于把资源"主权"明确归还开发者,迫使架构设计之初就回答"谁创建、谁销毁、何时销毁"——这恰是专业插件与脚本插件的分水岭。
三域对照与各自诊断:
| 内存域 | 管理机制 | 典型泄漏 | 诊断工具 |
|---|---|---|---|
| 解释器堆 | 引用计数加周期回收 | 全局容器只增不减 | 标准库内存追踪 |
| 内核内存池 | 手动分配释放 | 编辑会话未释放 | 内存调试启动参数 |
| GPU 显存 | 延迟销毁 | 纹理循环创建 | 帧捕获分析 |
三个域的共性教训可以浓缩成一条架构原则:与运行时建立明确的状态契约——Python 对象不长期持有主数据引用;GPU 资源必配对释放;网格编辑会话必 try/finally 包裹;依赖图更新只在必要时显式触发。契约即责任,责任即确定性。
再看两个综合场景巩固理解。
**场景一:插件缓存场景对象列表。**为了"下次快一点",把当前所有选中对象存进模块级列表。用户删除其中若干对象后,列表里一半是幽灵——下次遍历崩在访问已删对象。修复:存名字不存对象,或用前文弱引用缓存。
**场景二:插件每帧生成预览纹理。**在帧回调里创建纹理、绘制、忘了释放。几分钟后显存告警、视口花屏。修复:纹理提升为持久资源,尺寸变化时才重建;或严格用 try/finally 释放。
⚠️ 常见坑:把"进程内存不再上涨"当成无泄漏的证据。显存泄漏不体现在进程内存上,界面表现是视口逐渐花屏或纹理丢失——GPU 域要专门用帧工具查。
内存管理的最后一环是验证。建议给插件加一个自检操作符:执行 N 轮典型工作流,每轮后采样进程内存与已注册资源计数,输出趋势。内存曲线平稳、资源计数回到基线,才算通过。这个自检本身可以进第 6 章的测试金字塔——资源泄漏不是"高级问题",是任何时候都该被自动拦截的缺陷。
import tracemalloc def workflow_leak_check(rounds=50): tracemalloc.start() baseline = None for i in range(rounds): run_typical_workflow() current, peak = tracemalloc.get_traced_memory() if baseline is None: baseline = current if i % 10 == 0: print(f"round {i}: {current/1024/1024:.1f} MB " f"(delta {(current-baseline)/1024:.0f} KB)") # delta 持续线性增长 = 有泄漏
跑三轮压力自检:反复执行典型工作流若干次,观察进程内存曲线。健康曲线是"上升后走平"(首轮分配的缓存复用),病态曲线是"线性爬升"(每次执行都在泄漏)。再开内存追踪看解释器侧、用分配清单看内核侧、用帧工具看显存侧——三域各查一遍,五分钟就能给插件的健康状况出一份初诊报告。
以显存预算倒推:集显两 GB、独显八 GB 是常见区间,单张纹理超过显存的一成就值得警惕(一张四千乘四千的 RGBA 纹理约六十四 MB)。释放时机遵循作用域:帧内用的帧末释放、会话内用的插件禁用时释放、跨会话的(如用户自定义预览)持久持有并登记总量。持久的要有总量上限与最久未用淘汰——无限增长的持久缓存就是换了个名字的泄漏。
影响可忽略,收益却很大。弱引用的创建与解引用是轻量操作,且通常只在"缓存命中检查"时发生一次;与之相比,一次幽灵引用引发的崩溃排查动辄半天。性能敏感的热路径里本来就不该有缓存检查(直接现算),弱引用只在低频路径使用——两者互不干扰。
这就是 unregister 的资源清单职责:持久资源在注销时逐项释放(纹理、编辑会话、后台线程、总线订阅),登记进"待清理列表"的东西一个不落。写注销函数时的自检方法:列出插件生命周期里所有"创建后不自动消失"的东西,对照注销代码逐一勾销。勾不完的,要么补释放,要么重新设计它的所有权。
把三域的纪律收束成三个可复用的架构模式,供设计新模块时直接套用。
**模式一:资源句柄对象。**凡是要跨函数、跨帧持有的资源(编辑会话、纹理、线程),包装成句柄对象:构造时获取、析构时释放、持有期间提供有效性查询。调用方拿到句柄而不是裸引用,悬垂与泄漏在类型层面被约束。
**模式二:资源登记表。**模块级的登记表记录所有活跃资源,注销函数遍历表逐项释放。表的存在让"禁用插件时还剩什么"这个问题有了单一答案,也顺便提供了资源计数的观测点。
**模式三:作用域化的批量操作。**批处理开始前登记"将要创建的资源清单",全部成功才提交,失败则按清单回滚。这把 4.2 节落库层的事务思想延伸到了资源层面——网格、材质、纹理的创建同样可以有"要么全有、要么全无"的保证。
三个模式的共同精神只有一个:资源的一生必须写在学习题里,而不是留给运行时去即兴发挥。架构对资源的回答越明确,诡异崩溃越少。
架构模式之外,资源管理更多靠日常卫生习惯维持。三个一分钟级的习惯:写完一个获取资源的调用,立刻写下它的释放(成对写,不欠账);提交代码前扫一眼新增的创建调用,确认每个都有对应清理路径;每周跑一次压力自检曲线,截图存档。第三个习惯最容易被"反正也没出过事"淘汰——但曲线的意义正在于出事之前看见趋势,等用户替你发现内存上涨时,定位成本已是自检的一百倍。资源管理没有高深技巧,全是朴素纪律,纪律的全部价值由时间兑现。
三域管理的全部内容,压缩成一句话就是:每个资源从出生到死亡都有名有姓、有据可查。做到这一点用什么手段都是对的——句柄、登记表、作用域批量、还是最朴素的成对编写;做不到,再先进的模式也只是装饰。
性能、调度、资源三关已过。最后一章:把它变成别人敢用、能装、愿意付费的作品。