← ALL ARTICLES

对象生命周期

全库目录221
本文目录7

场景里的加载对象已经消失,解码任务却还在写像素,事件发布者也还保存着它的回调。此时把包装对象交给GC,既不能说明任务结束,也不能说明原生缓冲可以释放。对象生命周期要把拥有关系、可达关系和实际使用期间分别跟踪,再确定每份资源的回收条件。

拥有关系与借用期间

以纹理加载器为例:加载器创建文件流和像素缓冲,启动解码任务,并把上传区间提交给GPU。文件流可在读取完成后关闭;像素缓冲要等解码写入和后续上传读取都结束;上传区间则要等GPU完成对应提交。这些条件不一定同时满足,因此用一个“加载器已销毁”的布尔值无法代替它们。

所有者负责最终释放,借用者只在规定期间访问。C++中,unique_ptr通过移动交接唯一所有权;借出的裸指针不会延长寿命,所有者必须保证借用结束前不销毁对象。shared_ptr则让多个所有者共享控制块,最后一个强引用离开时销毁对象。复制强引用保住的是对象寿命,不会自动保护对象内部并发读写。

假设A、B各保存对方的强引用。外部不再持有它们以后,两边引用计数仍为1,谁都不会销毁。把非拥有的一条边改为weak_ptr,即可让计数归零;使用前通过lock()临时取得强引用,失败表示对象已经结束。关键在于先决定哪条关系表示拥有,不能只把所有指针都换成弱引用。

可达性与托管回收

托管GC从根引用出发追踪对象。场景列表删除加载器以后,长寿命事件发布者仍可能保存委托,委托又引用加载器,于是存在“根 → 发布者 → 委托 → 加载器 → 像素包装对象”的链。加载器仍可达,GC保留它符合规则;问题在于订阅关系超出了预定寿命。

解除订阅切断这条链,但正在派发的回调可能已被复制到订阅快照中。因此先让回调入口识别停止状态,再按派发器契约等待在途回调结束,才能释放回调访问的缓冲。仅从订阅表删除条目,不能撤回已经开始执行的访问。

对象不可达以后也不会保证立刻回收。不同.NET与Unity运行时可能使用不同代际、增量和移动策略。终结器的执行时刻不确定,文件、Socket和原生句柄因此应通过Dispose、using或对应引擎接口确定性释放。以Unity的RenderTexture为例,Release释放硬件资源,纹理对象本身仍然存在;包装对象与底层资源有各自的寿命。

停止与回收协议

加载器可用Active、Stopping、Released三个状态表达关闭过程。Active允许提交解码;进入Stopping后拒绝新任务并使迟到回调失效,通知旧任务取消,然后等待这些任务的终态。取消只是请求,任务可能正在完成一次写入,所以等待终态发生在释放缓冲之前。

text
Active → Stopping
停止新任务与新借用,注销订阅并使在途回调失效
请求取消;等待解码任务和在途回调结束
等待引用上传区间的GPU完成值
关闭文件流,释放原生缓冲与上传区间
清除托管引用 → Released

这是机制协议。若关闭发生在执行任务的同一线程,直接等待自己会死锁,需要由外层调度者完成收尾。异常路径也走同一协议:读取失败并不允许忽略已经启动的解码或上传。重复关闭看到Released时返回;Stopping中的另一次关闭等待同一完成结果,不能再次释放资源。GPU完成值的具体管理见 GPU资源生命周期。

原生容器同样遵守这条使用边界。Unity NativeArray、NativeList等要求显式释放,分配器标签表达预期范围,不能让仍运行的任务引用已经Dispose的容器。互操作期间固定托管地址只阻止对象被移动,不代表原生使用者已经停止;长期大量固定还可能影响碎片。固定区间也必须覆盖实际借用期间。

分配与驻留诊断

每帧字符串、装箱、集合和捕获闭包可能产生分配,具体行为取决于API、编译器和执行路径。先记录分配栈与字节数,再沿对象是否存活分类。短命分配增加未来回收工作;长期事件引用增加存活集;集合Clear后保留容量则可能让一次峰值成为长驻大数组。这三者对应不同修法,不能只看一帧是否发生GC。

重复创建关闭加载器时,保存托管与原生快照。如果对象数随轮数增长,沿根引用寻找事件、静态缓存和任务链;如果对象数稳定而原生字节增长,检查句柄与资源释放;如果峰值后保持平台,应检查缓存容量与池的驻留策略。总内存高本身不足以判定泄漏。

实现与证据

本文提供加载器生命周期协议,解释所有权、可达链与异步完成条件;未运行内存快照、GC实验或实际加载器。验收应覆盖读取异常、取消时写入、迟到回调、重复关闭和GPU仍在使用五条路径,逐份确认资源恰好释放一次。平台与引擎对象规则由相关专篇维护。

相关主题

参考资料