当前位置: 首页 > news >正文

MyFramework:Unity ListScope 如何减少临时 List 的 GC

项目地址:

https://github.com/ZHOURUIH/MyFramework

Unity 项目里,List<T>是使用频率非常高的容器。

很多时候,我们只是临时需要一个列表:

临时收集对象 临时保存回调 临时保存路径 临时过滤结果 临时做一次遍历缓存

这种列表生命周期很短。

函数执行完就不再需要。

如果每次都直接写:

List<int> tempList = new();

在高频逻辑里就会不断产生临时 GC。

MyFramework 里的ListScope<T>解决的就是这个问题。

它让临时List<T>ListPool中申请,并在using结束时自动归还。


一、代码

ListScope<T>的代码如下:

using System; using System.Collections.Generic; using static FrameBaseHotFix; using static FrameUtility; using static StringUtility; // 用于自动从对象池中获取一个List<T>,不再使用时会自动释放,需要搭配using来使用,比如using(new ListScope<T>(out var list)) public struct ListScope<T> : IDisposable { private List<T> mList; // 分配的对象 public ListScope(out List<T> list) { if (GameEntryBase.getInstance() == null || mListPool == null) { list = new(); mList = null; return; } string stackTrace = GameEntryBase.getInstance().mFrameworkParam.mEnablePoolStackTrace ? getStackTrace() : EMPTY; list = mListPool.newList(typeof(T), typeof(List<T>), stackTrace, true) as List<T>; mList = list; } public ListScope(out List<T> list, List<T> initList) { if (GameEntryBase.getInstance() == null || mListPool == null) { list = new(); mList = null; return; } string stackTrace = GameEntryBase.getInstance().mFrameworkParam.mEnablePoolStackTrace ? getStackTrace() : EMPTY; list = mListPool.newList(typeof(T), typeof(List<T>), stackTrace, true) as List<T>; mList = list; mList.addRange(initList); } public ListScope(out List<T> list, T[] initList) { if (GameEntryBase.getInstance() == null || mListPool == null) { list = new(); mList = null; return; } string stackTrace = GameEntryBase.getInstance().mFrameworkParam.mEnablePoolStackTrace ? getStackTrace() : EMPTY; list = mListPool.newList(typeof(T), typeof(List<T>), stackTrace, true) as List<T>; mList = list; mList.addRange(initList); } public void Dispose() { mListPool?.destroyList(ref mList, typeof(T)); } }

核心逻辑很简单:

构造函数里从 ListPool 申请 List<T> Dispose 里把 List<T> 归还给 ListPool

使用方式:

using var a = new ListScope<int>(out var tempList); tempList.Add(1); tempList.Add(2); tempList.Add(3);

离开作用域后,Dispose()自动执行。


二、为什么不用 new List

临时List<T>看起来很便宜。

但在框架代码里,它可能出现在很多高频位置:

每帧 Update 事件分发 资源回调 UI 刷新 对象过滤 临时排序 路径收集

如果这些地方不断new List<T>(),就会产生大量短生命周期对象。

这些对象本身可能不大,但数量多了以后,会增加 GC 压力。

ListScope<T>的思路是:

不要每次创建新的 List<T> 而是从 ListPool 取一个空列表 用完 Clear 后放回池中

这样可以减少临时列表反复分配。


三、using 管生命周期

ListScope<T>是结构体,并实现了IDisposable

所以它可以用using管理生命周期:

using var a = new ListScope<string>(out var paths); // 使用 paths

编译后,作用域结束时会调用:

a.Dispose();

Dispose()中执行:

mListPool?.destroyList(ref mList, typeof(T));

也就是说,临时列表不需要手动归还。

只要写在using作用域里,结束时就会自动还回去。

这和ClassScope<T>的思路一致:

ClassScope<T> 管临时 ClassObject ListScope<T> 管临时 List<T>

四、ListPool

ListScope<T>背后使用的是ListPool

申请列表时:

list = mListPool.newList(typeof(T), typeof(List<T>), stackTrace, true) as List<T>;

ListPool内部有几个核心容器:

protected Dictionary<Type, HashSet<IList>> mPersistentInuseList = new(); // 持久使用的列表对象,为了提高运行时效率,仅在编辑器下使用 protected Dictionary<Type, HashSet<IList>> mInusedList = new(); // 仅当前栈帧中使用的列表对象,为了提高运行时效率,仅在编辑器下使用 protected Dictionary<Type, Queue<IList>> mUnusedList = new(); // 未使用列表 protected Dictionary<IList, string> mObjectStack = new(); // 对象分配的堆栈信息列表

其中最关键的是:

mUnusedList 已经回收、可以复用的 List mInusedList 当前正在使用的临时 List mPersistentInuseList 当前正在使用的持久 List

ListScope<T>申请的是临时列表,所以传入:

true

也就是onlyOnce = true


五、申请流程

ListPool.newList()的逻辑是:

public IList newList(Type elementType, Type listType, string stackTrace, bool onlyOnce = true) { if (mHasDestroy) { return null; } if (isEditor() && !isMainThread()) { Debug.LogError("只能在主线程使用ListPool,子线程请使用ListPoolThread代替"); return null; } bool isNew = false; IList list; // 先从未使用的列表中查找是否有可用的对象 if (mUnusedList.TryGetValue(elementType, out var valueList) && valueList.Count > 0) { list = valueList.Dequeue(); } // 未使用列表中没有,创建一个新的 else { list = createInstance<IList>(listType); isNew = true; } if (isEditor()) { // 标记为已使用 mObjectStack.Add(list, stackTrace); addInuse(list, elementType, onlyOnce); if (isNew) { int totalCount = 0; totalCount += mInusedList.get(elementType)?.Count ?? 0; totalCount += mPersistentInuseList.get(elementType)?.Count ?? 0; if (totalCount % 1000 == 0) { Debug.Log("创建的List总数量已经达到:" + totalCount + "个,type:" + elementType); } } } return list; }

优先从mUnusedList取。

池里没有时才创建新的List<T>

所以第一次可能会创建。

后面重复使用时,就可以复用池里的列表。


六、回收流程

ListScope<T>结束时调用:

mListPool?.destroyList(ref mList, typeof(T));

destroyList()的逻辑是:

public void destroyList<T>(ref List<T> list, Type elementType) { if (mHasDestroy || list == null) { return; } if (isEditor() && !isMainThread()) { Debug.LogError("只能在主线程使用ListPool,子线程请使用ListPoolThread代替"); return; } list.Clear(); addUnuse(list, elementType); if (isEditor()) { removeInuse(list, elementType); mObjectStack.Remove(list); } list = null; }

这里做了几件事:

清空 List 加入未使用队列 从使用列表移除 移除堆栈记录 外部引用置空

回收时会先Clear()

所以下次从池中取出来时,是一个空列表。

这一步很重要。

临时列表不能把上一次的数据带到下一次使用。


七、编辑器泄漏检查

ListScope<T>的一个重要价值是配合ListPool的泄漏检查。

ListPool.update()中会检查临时列表是否还在使用:

public override void update(float elapsedTime) { base.update(elapsedTime); if (isEditor()) { foreach (var item in mInusedList) { foreach (IList itemList in item.Value) { string stack = mObjectStack.get(itemList); if (stack.isEmpty()) { stack = "当前未开启对象池的堆栈追踪,可在对象分配前使用F4键开启堆栈追踪,然后就可以在此错误提示中看到对象分配时所在的堆栈\n"; } else { stack = "create stack:\n" + stack + "\n"; } logError("有临时对象正在使用中,是否在申请后忘记回收到池中! \n" + stack); break; } } } }

ListScope<T>申请列表时传入onlyOnce = true

所以这个列表会被记录到mInusedList

如果它没有在当前使用周期内归还,编辑器下就会报错。

这能提前发现:

申请了临时 List 但没有释放

使用using后,这种问题会少很多。


八、堆栈追踪

ListScope<T>申请列表时会记录堆栈:

string stackTrace = GameEntryBase.getInstance().mFrameworkParam.mEnablePoolStackTrace ? getStackTrace() : EMPTY;

是否记录由参数控制:

mEnablePoolStackTrace

开启后,如果某个临时列表忘记归还,报错里可以看到创建堆栈。

这对排查对象池泄漏很有用。

不开启时,也会提示:

当前未开启对象池的堆栈追踪 可在对象分配前使用 F4 键开启堆栈追踪

这说明ListPool不只是一个复用容器。

它也承担了运行时检查工具的职责。


九、初始化列表

ListScope<T>还有两个构造函数。

可以从已有列表初始化:

public ListScope(out List<T> list, List<T> initList) { if (GameEntryBase.getInstance() == null || mListPool == null) { list = new(); mList = null; return; } string stackTrace = GameEntryBase.getInstance().mFrameworkParam.mEnablePoolStackTrace ? getStackTrace() : EMPTY; list = mListPool.newList(typeof(T), typeof(List<T>), stackTrace, true) as List<T>; mList = list; mList.addRange(initList); }

也可以从数组初始化:

public ListScope(out List<T> list, T[] initList) { if (GameEntryBase.getInstance() == null || mListPool == null) { list = new(); mList = null; return; } string stackTrace = GameEntryBase.getInstance().mFrameworkParam.mEnablePoolStackTrace ? getStackTrace() : EMPTY; list = mListPool.newList(typeof(T), typeof(List<T>), stackTrace, true) as List<T>; mList = list; mList.addRange(initList); }

也就是:

using var a = new ListScope<int>(out var tempList, sourceList);

或者:

using var a = new ListScope<int>(out var tempList, sourceArray);

这样可以在不分配新List<T>的情况下,得到一个临时副本。


十、多个列表

MyFramework 里还有ListScope2<T>

public struct ListScope2<T> : IDisposable { private List<T> mList0; // 分配的对象 private List<T> mList1; // 分配的对象 public ListScope2(out List<T> list0, out List<T> list1) { if (GameEntryBase.getInstance() == null || mListPool == null) { list0 = new(); list1 = new(); mList0 = null; mList1 = null; return; } string stackTrace = GameEntryBase.getInstance().mFrameworkParam.mEnablePoolStackTrace ? getStackTrace() : EMPTY; list0 = mListPool.newList(typeof(T), typeof(List<T>), stackTrace, true) as List<T>; list1 = mListPool.newList(typeof(T), typeof(List<T>), stackTrace, true) as List<T>; mList0 = list0; mList1 = list1; } public void Dispose() { Type type = typeof(T); mListPool?.destroyList(ref mList0, type); mListPool?.destroyList(ref mList1, type); } }

它一次申请两个同类型列表。

还有ListScope2T<T0, T1>

public struct ListScope2T<T0, T1> : IDisposable { private List<T0> mList0; // 分配的对象 private List<T1> mList1; // 分配的对象 public ListScope2T(out List<T0> list0, out List<T1> list1) { if (GameEntryBase.getInstance() == null || mListPool == null) { list0 = new(); list1 = new(); mList0 = null; mList1 = null; return; } string stackTrace = GameEntryBase.getInstance().mFrameworkParam.mEnablePoolStackTrace ? getStackTrace() : EMPTY; list0 = mListPool.newList(typeof(T0), typeof(List<T0>), stackTrace, true) as List<T0>; list1 = mListPool.newList(typeof(T1), typeof(List<T1>), stackTrace, true) as List<T1>; mList0 = list0; mList1 = list1; } public void Dispose() { mListPool?.destroyList(ref mList0, typeof(T0)); mListPool?.destroyList(ref mList1, typeof(T1)); } }

它一次申请两个不同类型的列表。

例如资源回调中就会用到类似结构:

using var a = new ListScope2T<AssetLoadCallback, string>(out var callbacks, out var paths);

一个列表保存回调。

一个列表保存路径。

两者生命周期一致,所以可以用同一个 scope 管理。


十一、为什么需要 ListScope2T

有些逻辑不是只需要一个临时列表。

例如异步加载回调执行前,需要把两组数据同时转移出来:

回调列表 加载路径列表

它们必须保持下标对应关系。

如果分别申请两个ListScope

using var a = new ListScope<AssetLoadCallback>(out var callbacks); using var b = new ListScope<string>(out var paths);

也可以工作。

但写法更长。

ListScope2T<T0, T1>把这件事收成一个作用域:

using var a = new ListScope2T<AssetLoadCallback, string>(out var callbacks, out var paths);

两个临时列表一起申请。

作用域结束后一起归还。


十二、主线程限制

ListPool是主线程列表池。

代码里有明确检查:

if (isEditor() && !isMainThread()) { Debug.LogError("只能在主线程使用ListPool,子线程请使用ListPoolThread代替"); return null; }

这说明ListScope<T>适合主线程逻辑。

子线程不能使用普通ListPool

子线程需要使用ListPoolThread

这个边界很重要。

因为ListPool内部没有为了多线程访问而加锁。

它的设计目标是 Unity 主线程运行时系统。


十三、和 safe() 的区别

safe()也和列表有关。

但它们解决的问题不同。

safe()解决的是:

遍历时原列表可能是 null

例如:

foreach (var item in list.safe()) { }

它返回一个共享空列表,避免空判断。

ListScope<T>解决的是:

我需要一个临时 List 但不想频繁 new 也不想手动回收

例如:

using var a = new ListScope<int>(out var tempList);

一个偏读取安全。

一个偏临时容器复用。


十四、和 SafeList 的区别

SafeList<T>解决的是遍历中修改:

正在遍历 同时可能新增或删除元素

所以它内部维护:

mMainList mUpdateList mModifyList

ListScope<T>没有这种复杂逻辑。

它只是一个临时列表生命周期工具。

ListScope<T> 从池中取一个 List 用完自动还回去 SafeList<T> 管理一个长期存在的安全列表

它们名字相似,但目的不同。


十五、使用边界

ListScope<T>只适合临时列表。

不适合把列表长期保存。

下面这种写法是错误的:

using var a = new ListScope<int>(out var tempList); mCacheList = tempList;

using结束后,tempList会被清空并回收到池中。

mCacheList持有的就是一个已经失效的列表。

所以使用边界很明确:

只能在当前作用域内使用 不能保存到成员变量 不能跨帧使用 不能交给异步回调后继续使用 不能在子线程使用普通 ListScope

长期列表应该自己持有。

临时列表才应该使用ListScope<T>


十六、设计价值

ListScope<T>的价值不是代码复杂。

它的价值在于把高频临时列表的生命周期固定下来:

using 开始 从 ListPool 取列表 using 结束 Clear 回到 ListPool

它带来的效果是:

减少临时 List 分配 减少忘记回收 编辑器下能检查泄漏 支持堆栈追踪 支持多个临时列表一起管理

对于 Unity 框架来说,这种小工具非常实用。

因为临时列表使用太频繁了。

如果每个地方都手动管理,代码会变啰嗦,也容易漏。


总结

ListScope<T>本质上是一个列表作用域管理工具。

它通过usingIDisposable,把临时List<T>的生命周期限制在当前作用域内。

using var a = new ListScope<int>(out var tempList);

构造时从ListPool取列表。

结束时自动调用:

mListPool?.destroyList(ref mList, typeof(T));

destroyList()会清空列表、放回未使用队列,并在编辑器下移除使用记录。

这样可以减少临时List<T>带来的 GC,也能避免忘记手动归还。

在 MyFramework 中,ListScope<T>ListScope2<T>ListScope2T<T0, T1>共同承担了临时列表生命周期管理的职责。

它们和ClassScope<T>一样,都是把“手动申请、手动释放”的对象池使用方式,收敛成更稳定的作用域生命周期。

http://www.gsyq.cn/news/1592022.html

相关文章:

  • SU(3)群特征标的点态与Lp范数估计:从Weyl公式到工程应用
  • 2026年苏州厂家用了这款8寸晶圆专用衬纸,良率提升0.5%!
  • 35+运维转行网络安全:告别内卷越老越吃香,附实战经验建议收藏
  • OpCore Simplify:重构黑苹果配置的技术框架与智能解决方案
  • 计算机毕业设计之jsp基于SSM的问卷调查平台的设计与实现
  • 计算机毕业设计之基于SSM的锦州风味美食推广系统设计与实现
  • 关于激光管安装的相关事宜
  • AI真能替代安全专家吗?聊聊AI技术在入侵检测系统中的作用与挑战
  • 亲子娱乐想让家庭再来,不能只让孩子玩、大人等
  • GHelper终极指南:华硕笔记本性能调校的完整解决方案
  • Windows 7 SP2完整方案深度解析:让经典系统在现代硬件上重获新生
  • python网络爬虫学习
  • Scale-Across场景与技术方案初探
  • DVWA文件包含漏洞实战:从原理到高级利用与防御
  • 高考热门专业十年大洗牌:你爸妈推荐的那些,大学自己都不开了
  • 第8篇:初遇Power Query——一键清洗大区报表
  • 兰亭妙微原创作品 | 智能制造驾驶舱UI设计
  • 银行项目网上支付接口调用测试实例
  • 深度解析猫抓浏览器扩展:从M3U8嗅探到资源捕获的完整技术架构
  • 从理论物理视角构建凯瑟琳轮模型:几何、拓扑与数值模拟
  • Kinetis Expert Pins工具全平台安装与配置实战指南
  • 深度评测 | 从剂型创新到材料突破:2026年肛肠外科护理产品的技术迭代与选购逻辑
  • 纯RGB视觉基于神经网络的点云重建SLAM3R(对白墙等弱纹理环境友好)
  • G2-Laplacian流与超辛流的降维演化:连接七维与四维几何结构
  • 大模型小白必看:Transformer位置编码全解析(收藏版)
  • vLLM部署下一代大模型:PagedAttention与动态上下文实战指南
  • 小程序毕设选题推荐:智慧农业背景下菇房环境物联网监测管理系统的设计与实现(小程序端)基于物联网技术的菇房环境智能管控小程序平台设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】
  • 腔体滤波器设计制造全流程:从电磁仿真到装配调试实战解析
  • 通过MANUS Metagloves Pro Haptic将人类手部动作映射到23自由度灵巧手
  • 【课程设计/毕业设计】基于物联网感知的菇房智能环境管理小程序应用设计与实现 SpringBoot后端驱动的微信小程序菇房物联网管控系统设计与实现【附源码、数据库、万字文档】