当前位置: 首页 > 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.jsqmd.com/news/1068887/

相关文章:

  • 为什么你的RAG+CoT系统上线即崩?3个被92%团队忽略的时序一致性陷阱与实时修复补丁
  • 2026年薪酬设计:这3招让企业员工都满意
  • Spring AI MCP 工具调用测试文章
  • 深圳企业家怎么做个人IP?别再跟风唱跳,这套“工厂思维”才是爆款底层密码
  • 奈飞Netflix高级会员解锁版破解版 全网同步 终身免费使用观看
  • DSPE-PEG2000-NGR 靶向磷脂结构与两亲特性
  • 路侧单元被劫持,交叉路口的车全部收到了假信号——V2X路侧安全该怎么做?
  • AI芯漫平台本金减损措施正式出台,您可以申请本金核定
  • 哪些AI短剧的工具好用?2026全品类AI短剧创作工具盘点
  • AI原生状态管理不是框架选择题,而是数学建模题(2026奇点大会论文集第8章精要速读版)
  • 【AI原生跨模态工程化终极指南】:SITS 2026视觉语言模型落地的7大避坑法则与3个已验证生产级Pipeline
  • Linux占用CPU脚本
  • 别再用传统SOA审计AI系统!奇点大会权威发布《AI原生审计成熟度模型》——仅开放前500份评估工具包
  • AI原生预训练模型选型避坑手册(SITS 2026实测版):5个被厂商隐瞒的关键衰减指标曝光
  • 别再堆模型了!SITS 2026定义的“最小可行融合单元”是什么?——1个架构图+4个验证checklist
  • (毕业必看)亲测好用的一键生成论文工具,毕业党收藏备用
  • Hermes Agent 技能进化系统拆解:Skill 的元数据结构、自注册加载与退化机制 [07]
  • 2026年腾讯地图LBS:社交地产出行AR三维地图技术方案
  • 工程企业怎么进行数字化管理,都有哪些工程项目管理系统?
  • 为什么92%的LLM部署在2026年将因XAI不达标被拒入金融/医疗场景?——奇点大会首曝监管沙盒准入白皮书
  • 为什么你的MoCo在SITS 2026测试集上AUC暴跌?20年CV老兵拆解:时序负样本采样偏差的3层因果链与实时校准工具包
  • 六张网基建全面落地:十万亿级地下管网赛道,谁能抓住核心红利?
  • 2026串口屏行业观察
  • Mac 连接火山引擎 ECS:SSH 密钥配置与文件互传完整教程
  • 【紧急预警】SITS 2026将于Q3强制启用新注意力校验协议:3类旧版可视化脚本将在2026.09.30自动失效
  • 从本地到云端,ROCm 7.x 环境迁移的差异化配置要点
  • 使用Gemini显示“出了点问题”又或者“Somethingwent wrong”出错?
  • 2026思明区培育钻怎么挑?内行人的避坑指南
  • 软件许可证总是不够用,问题到底出在哪
  • 2026 年 6 月密封圈定制亲测分享