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

MyFramework:异步加载回调为什么要先转移再执行

项目地址:

GitHub - ZHOURUIH/MyFramework: Unity 商用级别开发框架,经过了多年经验沉淀.一个在unity上使用的网络游戏客户端开发框架,为unity所有使用方式提供完善的封装和管理,只需要专注于游戏逻辑的编写 · GitHub

ResourceManager的异步资源加载里,有一个很小的细节:

callbackAll()

它没有直接遍历原始回调列表,而是先把回调列表转移到临时列表,再统一执行。

这个设计处理的是异步回调中的列表修改问题。


一、回调列表

AssetInfo中保存了两个列表:

protected List<AssetLoadCallback> mCallback = new(); // 异步加载回调列表 protected List<string> mLoadPath = new(); // 加载资源时使用的路径

mCallback保存回调函数。

mLoadPath保存每个回调对应的加载路径。

添加回调时,两个列表同步追加:

public void addCallback(AssetLoadCallback callback, string loadPath) { if (callback == null) { return; } mCallback.Add(callback); mLoadPath.Add(loadPath); }

这里没有创建单独的结构体保存回调和路径,而是用两个并行列表。

执行时按相同下标读取。


二、普通写法的问题

最直接的写法是这样:

foreach (AssetLoadCallback callback in mCallback) { callback(asset, subAssets, bytes, loadPath); } mCallback.Clear(); mLoadPath.Clear();

这种写法有风险。

回调执行过程中,业务逻辑可能再次发起同一个资源的异步加载。

这时又会调用:

addCallback(callback, loadPath);

也就是在遍历mCallback的过程中修改mCallback

结果可能是:

foreach 报错 新回调被本轮错误执行 Clear 时把新回调也清掉 回调顺序混乱

异步资源加载里,回调内部再次请求资源是很常见的情况。

所以不能直接遍历原始列表。


三、callbackAll

MyFramework 的实现是:

public void callbackAll() { // 复制一份列表,避免回调中再次修改回调列表而报错 using var a = new ListScope2T<AssetLoadCallback, string>(out var callbacks, out var paths); mCallback.moveTo(callbacks); mLoadPath.moveTo(paths); int callbackCount = callbacks.Count; for (int i = 0; i < callbackCount; ++i) { callbacks[i](mSubAssets.get(0), mSubAssets, null, paths[i]); } }

它分三步。

1. 从 ListPool 中申请两个临时列表 2. 把原始列表内容转移到临时列表 3. 遍历临时列表执行回调

原始列表在回调执行前已经清空。

所以回调执行过程中,即使再次调用addCallback(),新增回调也只会进入新的原始列表,不会影响当前这一轮。


四、moveTo

列表转移使用的是moveTo()扩展函数:

// 将sourceList中的所有元素添加到targetList中,并清空sourceList,返回targetList public static List<T> moveTo<T>(this List<T> sourceList, List<T> targetList) { if (sourceList.isEmpty()) { return targetList; } targetList.AddRange(sourceList); sourceList.Clear(); return targetList; }

它不是复制后保留原列表。

它是转移:

sourceList -> targetList sourceList.Clear()

转移完成后:

mCallback 变为空列表 callbacks 保存本轮需要执行的回调

这样当前回调和新加入的回调被分成了两批。


五、ListScope2T

临时列表通过ListScope2T获取:

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

ListScope2T的作用是一次申请两个临时 List:

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结束时,两个临时列表自动归还到对象池。

这避免了每次资源回调都创建新的List


六、并行列表

callbackAll()中必须同时转移两个列表:

mCallback.moveTo(callbacks); mLoadPath.moveTo(paths);

然后通过相同下标执行:

callbacks[i](mSubAssets.get(0), mSubAssets, null, paths[i]);

这要求两个列表始终数量一致。

添加回调时:

mCallback.Add(callback); mLoadPath.Add(loadPath);

执行回调时:

callbacks[i] paths[i]

这种写法比创建一个临时结构对象更省。

代价是必须保证两个列表同步维护。

在这个场景中,addCallback()是唯一入口,所以同步关系比较容易保证。


七、本轮和下一轮

这个设计最关键的是区分两类回调:

本轮已经准备执行的回调 回调执行过程中新增的回调

转移前:

mCallback = [A, B, C]

转移后:

callbacks = [A, B, C] mCallback = []

执行A时,如果又添加了D

callbacks = [A, B, C] mCallback = [D]

D不会插入当前遍历。

D会留给下一次加载流程处理。

这样回调执行顺序更稳定。


八、避免 Clear 误删

如果不使用转移,而是遍历后清空:

foreach (...) { callback(); } mCallback.Clear();

回调中新增的内容也可能被最后的Clear()清掉。

这类 Bug 很隐蔽。

表现是:

回调已经注册 资源也加载完成 但回调没有执行

moveTo()先清空原列表,可以避免这个问题。

当前批次和新批次不会混在一起。


九、适用位置

这种写法适合所有“执行回调时可能再次修改回调列表”的场景。

例如:

资源异步加载 AssetBundle 异步加载 事件分发 命令完成回调 网络消息回调 UI 动画完成回调

条件是:

当前批次执行期间 允许产生下一批回调 但不希望下一批影响当前批次

这类场景都可以使用“转移列表再执行”的方式。


十、和 SafeList 的区别

SafeList适合遍历中允许增删列表。

callbackAll()的需求不同。

它不需要让新增回调参与当前遍历。

它需要把当前批次固定下来。

所以这里没有用 SafeList,而是使用:

moveTo + 临时列表

这比 SafeList 更直接。

当前批次被完整保存。

原列表立即空出来。

新增内容自然进入下一批。


十一、设计价值

这个函数的价值不在复杂。

它解决的是一个高频细节:

回调执行时,回调列表可能被再次修改

MyFramework 的处理方式是:

先转移 再执行 执行期间允许原列表继续接收新回调 临时列表用完自动归还对象池

这让异步资源回调更稳定。


总结

callbackAll()的核心逻辑很短:

using var a = new ListScope2T<AssetLoadCallback, string>(out var callbacks, out var paths); mCallback.moveTo(callbacks); mLoadPath.moveTo(paths); for (int i = 0; i < callbacks.Count; ++i) { callbacks[i](mSubAssets.get(0), mSubAssets, null, paths[i]); }

它做了三件事:

固定当前回调批次 避免遍历中修改原列表 避免回调中新增内容被 Clear 误删

再配合ListScope2T,临时列表也不会频繁产生 GC。

这是 MyFramework 中一个很小但实用的设计点。

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

相关文章:

  • Mistral AI:企业控制 AI 层的新希望,能否在巨头林立的市场突围?
  • Spring AI MCP 工具调用测试文章
  • 奈飞Netflix高级会员解锁版破解版 全网同步 终身免费使用观看
  • 路侧单元被劫持,交叉路口的车全部收到了假信号——V2X路侧安全该怎么做?
  • AI原生状态管理不是框架选择题,而是数学建模题(2026奇点大会论文集第8章精要速读版)
  • AI原生预训练模型选型避坑手册(SITS 2026实测版):5个被厂商隐瞒的关键衰减指标曝光
  • 别再堆模型了!SITS 2026定义的“最小可行融合单元”是什么?——1个架构图+4个验证checklist
  • Hermes Agent 技能进化系统拆解:Skill 的元数据结构、自注册加载与退化机制 [07]
  • 为什么你的MoCo在SITS 2026测试集上AUC暴跌?20年CV老兵拆解:时序负样本采样偏差的3层因果链与实时校准工具包
  • 2026串口屏行业观察
  • 软件许可证总是不够用,问题到底出在哪
  • 从本地到云端,ROCm 7.x 环境迁移的差异化配置要点
  • # 传统土建危废间难适配数字化监管,越华环保集团智能存储方案能补齐技术短板吗?
  • 【AI原生模型审计黄金标准】:2026奇点大会首次公开的7步闭环审计流程(含GDPR/ISO/MLSec合规映射表)
  • 2026年在惠州寻找靠谱的产品故事片影视制作服务商哪家更靠谱
  • 大模型调试不再靠猜(SITS 2026注意力异常检测引擎内测版限时开放,仅剩最后112个企业席位)
  • IDEA搭建SpringBoot+Elasticsearch6.8完整流程
  • Litefuse 开源发布:一行命令部署 Agent 可观测与评估平台,单机版比 Langfuse 快 5.5 倍
  • 【JAVA毕设源码分享】基于springboot高校教学质量评估系统(程序+文档+代码讲解+一条龙定制)
  • 杂乱文件太多处理不过来?这套ETL方案专治各种“不服”(选做实验1)
  • 为什么92%的SITS 2026部署环境未通过对抗压力测试?3个被忽视的架构漏洞与修复优先级清单
  • 性能碾压!RustFS 100KiB以下小文件场景全面超越MinIO,实测数据曝光
  • 蛋仔网:CSDN技术文章怎么写,讲清低负载看板和安全记录
  • 编写网络管理
  • 警惕“伪DPO陷阱”:2026奇点大会权威认证的5项AI原生偏好对齐黄金指标(含ROC-AUC@Preference阈值校验表)
  • AI偏见检测工具选型终极指南(SITS 2026深度测评版):对比TensorFlow Fairness、AIF360与Hugging Face Bias Toolkit的5项硬指标
  • 2B参数Spatial-TTT入选ECCV 2026,长视频处理与空间推理能力领先,节省超40%显存与计算
  • AI手势识别+手势控制系统 OpenCV+Python(源码和教程)
  • AI Agent 智能体是什么
  • 从机器翻译到智驾:规则派的黄昏与数据革命的终局