本节摘要:插件不是一段被动的代码,而是一个有完整生命节律的运行时实体:被扫描导入、被启用激活、随 Blender 启停、被禁用回收。本节讲清注册三件事(类、属性、处理器)的语义与顺序约束、逆序注销的原因、热重载与模块缓存的陷阱,以及属性更新回调这类"细微生物钟"。理解生命周期,插件才不会在用户禁用、重载、升级时留下幽灵。
阅读完本节,你应当能够:
把插件的处境想成一出剧目:文件躺在插件目录里只是"剧本入库";Blender 启动时扫描目录读取元数据,是"拿到节目单";用户点启用、解释器执行主模块,是"演员候场";register() 被调用,是"登台";禁用或退出时 unregister() 执行,是"谢幕回收道具"。每一阶段系统对代码的期待不同,错配阶段做错事,就是一半诡异 bug 的来源。
注意两个细节。第一,导入(模块代码被执行)与激活(register 被调用)是两个时刻,中间可以隔很久——用户禁用又启用插件时,模块不一定重新导入,但 register 一定会再跑。第二,register 失败(比如类名冲突)时系统会中止并报错,不会留下半注册状态——这是对架构稳定性的承诺,也要求你把注册写成"要么全成、要么全无"的原子操作。
bpy.utils.register_class 向类型系统注入 Operator、Panel、Menu、PropertyGroup 等类。注册后,类不再只是 Python 类,而是被 RNA 识别并赋予元数据的"可序列化类型":Operator 进入全局操作符字典(可以被 F3 搜到、被快捷键绑定),Panel 进入界面注册表(参与每次重绘的调度)。类名冲突会直接报错,所以命名规范(类别前缀)不只是审美,是注册层面的必需。
向内置数据块动态挂自定义属性,例如把一个属性组挂到 Scene 上。挂载之后,这些属性获得三样东西:随文件保存加载的序列化能力、进入撤销栈的事务能力、可被界面与驱动系统引用的元数据。反过来说,用模块级全局变量存插件状态,这三样全没有——重启丢、多文件污染、不能动画驱动。第 4 章会把持久化展开讲,这里先立住结论:需要跨会话的状态,必须走属性注册。
把回调函数挂到事件钩子上:文件加载后、保存前、帧变更前后、依赖图更新后。这些钩子是插件感知外部世界的神经末梢。要注意它们是全局列表——你的函数 append 进去后,如果 unregister 时忘了 remove,函数会在插件禁用后继续被调用,而它引用的模块可能已经过期,崩溃就近在眼前。
⚠️ 常见坑:handler 列表越挂越多。每次启用插件都 append 一个同函数,禁用不移除,列表里就堆满死引用。正确姿势是注册时先检查列表中是否已有自己的函数,注销时显式移除。
unregister 必须严格逆序执行 register 的操作:先摘处理器,再删属性,最后注销类。原因在于依赖方向:处理器可能引用属性与类实例;属性的类型可能是某个已注册的 PropertyGroup 类。如果先注销类再删属性,RNA 在清理属性时仍会尝试访问已失效的类引用,引发崩溃。对称写法很简单——注册顺序列表反转即注销顺序:
_classes = (MySettings, MyOperator, MyPanel) def register(): for cls in _classes: bpy.utils.register_class(cls) bpy.types.Scene.my_tool = bpy.props.PointerProperty(type=MySettings) bpy.app.handlers.load_post.append(on_file_loaded) def unregister(): bpy.app.handlers.load_post.remove(on_file_loaded) del bpy.types.Scene.my_tool for cls in reversed(_classes): bpy.utils.unregister_class(cls)
这个模板值得抄进每个项目:处理器最先摘、属性其次删、类最后注销,与注册顺序完全镜像。属性定义所在类(MySettings)排在注册列表最前,正是为了让它先于引用它的挂载可用。
开发期最方便的功能是"重新加载脚本":改完代码点一下,插件重跑。但热重载也是新手最困惑的崩溃来源。机制上,重载会让解释器重新执行模块,产生一批新的类对象;而全局状态(比如你缓存在某个模块变量里的数据、挂过的 handler)不会自动清掉旧引用。于是出现两类典型症状:一是"注册了两个同名类"报错——旧类还挂在系统里;二是 handler 调用了旧模块的函数,而旧模块引用的资源已失效。
防御性写法有三条。第一,注册前先做清理钩子:现代 API 提供"若插件已注册则先整体注销"的机制,确保重载从干净状态开始。第二,全局可变状态(缓存、单例)要么避免,要么放在能被 unregister 清理的地方。第三,模块间杜绝循环导入——A 导入 B、B 又导入 A,在首次导入时或许侥幸成立,重载时序一变就爆"无法导入名称"。
💡 关键直觉:把每次热重载都想成"旧插件谢幕不彻底、新插件抢台"。你的 unregister 写得越对称,谢幕越干净,重载越稳。
生命周期里还有一个微缩版本:属性更新。给属性声明 update 回调,它会在值被修改的瞬间触发(无论来自界面滑块还是脚本赋值),拿到属性宿主与上下文:
def on_threshold_change(self, context): obj = context.active_object if obj and obj.type == 'MESH': # 立即按新阈值刷新显示,无需用户点应用 refresh_preview(obj, self.threshold) class MyToolSettings(bpy.types.PropertyGroup): threshold: bpy.props.FloatProperty( name="阈值", default=0.5, min=0.0, max=1.0, update=on_threshold_change, )
这是"所见即所得"的技术支点:拖滑块、立即见。但注意回调在主线程同步执行,回调里做重活会拖慢每一次拖动——重计算要推迟到帧事件或后台任务(第 5 章的模态与定时器正是为此准备的)。
| 生命周期环节 | 触发时机 | 你该做什么 | 你不该做什么 |
|---|---|---|---|
| 模块导入 | 启用或扫描时执行主模块 | 定义类与常量、解析 bl_info | 不做任何注册动作 |
| register | 启用确认后调用 | 注册类、挂属性、挂处理器 | 不做耗时初始化、不访问场景数据 |
| 运行期 | 插件激活期间 | 响应事件、按需读写数据 | 不缓存过期引用、不泄漏句柄 |
| unregister | 禁用或退出前调用 | 逆序清理一切 | 不留全局残迹 |
| 热重载 | 开发者手动触发 | 保证注销对称、无循环导入 | 依赖模块级缓存放心复用 |

把四个高频故障与排查路径列成手册,供开发期随时对照。
**故障一:启用插件即报"无法注册"。**先看报错里的类名,九成是类名冲突或标识格式问题。若报"已注册",说明上次注销不彻底——检查注销函数是否真的被调用过、处理器列表里是否残留你的函数。应急办法是先执行一遍完整注销再注册;根治办法是让注册入口自带"若已注册先注销"的清理钩子。
**故障二:禁用后界面还有残留面板。**面板类没被注销。常见原因是注册顺序列表里漏了它,或者用了动态生成的类没进列表。养成"所有类进同一个元组"的习惯,注册与注销都遍历它,残留自然绝迹。
**故障三:重载后插件行为错乱。**症状是功能"半新半旧"——新代码的一部分生效,另一部分还在跑旧逻辑。根因通常是模块级缓存:旧模块的缓存对象被新代码读到。解法是清空可变全局状态、或把它们挪到能被注销清理的宿主上;根根治法是遵循 6.1 节的依赖注入结构,让状态有唯一的家。
**故障四:文件加载后处理器没反应。**你把初始化写在了模块导入时,而处理器注册发生在启用时——两者都被满足时才工作,但用户"加载文件"这个动作只触发事件不重新执行模块。确认处理器在注册时已挂好、函数内不依赖早已失效的闭包变量,是定位这类"时灵时不灵"问题的两步。
把生命周期知识与 1.2 节的调试工具连起来,你得到一套"注册问题定位法"。
启用插件失败的报错栈会指向注册过程中的具体一行——多数是类定义或属性声明问题。用远程断点在注册函数入口停住,单步观察哪一步抛错,比对着报错猜快得多。禁用后界面残留的问题,在注销函数里打断点确认它真的被调用:如果没被调用,说明宿主认为你的插件未正确注册(早前的失败留下了半状态);如果被调用但仍残留,检查注销顺序列表是否漏项。
热重载错乱的问题用模块检查法:在可疑模块的顶层放一行输出(打印模块名与时间),重载两次看输出几次——输出超过一次说明旧模块被重新执行,全局状态必然重复累积。这个一分钟的实验,能把你从"玄学重载"的恐惧里捞出来。生命周期调试的要诀就一句:让每一步注册与注销都可见,黑盒变白盒,问题自现。
给出可直接套用的注册模块完整模板,作为本节收束——它综合了前述全部纪律:统一类列表、逆序注销、处理器先查重、属性挂载与删除配对、可选的清理钩子。使用方法:把全部类按"被依赖者在前"的顺序填进列表,属性挂载与处理器注册填进对应段落,其余原样保留。模板的价值不在省打字,在于把纪律固化成结构——用了它,漏注销、顺序错、重复挂载这三类事故在结构上就不可能发生。团队协作时它还有教学功能:新人改模板比读规范快十倍,而模板的正确性由结构保证,不依赖新人的记性。
第 1 章到此收束。你已经能搭环境、写插件、管生命周期。第 2 章潜入 bpy 深水区:数据、上下文、操作域三套律令。