7.ILRuntime实践项目总结

7.总结


7.1 知识点

学习的主要内容

实践思考


7.2 核心要点速览

工程拼装与资源路径习惯

  • 包管理:ILRuntime 包走作用域 com.ourpalm,在同一套 Scoped Registry 里填 OpenUPM 地址 https://package.openupm.com,并在 Player 设置里打开 Allow unsafe code,否则 ILRuntime 相关代码容易编不过。
  • 周边模块:顺带接入 AB 构建工具、项目里已有的基础框架,以及 AB 对比与上传下载管线;Demo 里暂时用不到的脚本可直接删掉,仅剩报错处用注释压住也行,后面真要扩展至少有骨架可对。
  • 加载顺序AssetBundleManager 里凡是从磁盘拉 AB,一律先问 persistentDataPath 里有没有同名文件,没有再退回 StreamingAssetsPath。主包名按平台切换(IOS / Android / PC),读出 AssetBundleManifest 后再按依赖把依赖包和目标包 LoadFromFile 进来。热更成功后玩家本地目录会覆盖安装包内资源,这条分支就是热更生效的关键。
场景 行为
主 manifest 或普通 AB File.Exists(持久化路径 + 包名) 为真则用持久化,否则用 Streaming
依赖包 同样在字典里按包名拼路径,避免只更主包不更依赖

热更 DLL 如何进包、如何被 ILRuntime 吃掉

  • 热更工程编出来的 DLL、PDB 要打进 AB;正文里把扩展名改成 .txt 再当 TextAsset 打 AB,是让资源按 Unity 侧的识别规则进包(本质是当作二进制文本资源塞进 AB)。
  • 运行时侧:AB 包名约定为 dll_res,里头两个 TextAsset 名字仍是 HotFix_Project.dll / HotFix_Project.pdb,与资源在工程里的命名一致即可。
  • 启动链路:new AppDomain(ILRuntimeJITFlags.JITOnDemand),两次 LoadAssetBundleResourceAsync<TextAsset> 串起来,new MemoryStream(bytes)appDomain.LoadAssembly(dllStream, pdbStream, new PdbReaderProvider())。PDB 挂上后调试栈更像普通 C# 工程。
// 骨架:先 DLL 再 PDB,再 LoadAssembly
appDomain = new AppDomain(ILRuntimeJITFlags.JITOnDemand);
// ... 异步拿到 dll、pdb 两个 TextAsset ...
dllStream = new MemoryStream(dll.bytes);
pdbStream = new MemoryStream(pdb.bytes);
appDomain.LoadAssembly(dllStream, pdbStream, new PdbReaderProvider());
  • InitILRuntime 里给 appDomain.UnityMainThreadID 赋当前托管线程 ID,方便 Unity 侧调试与性能视图正确关联主线程。若开调试模式,协程里先等到 DebugService.IsDebuggerAttached 再执行外部回调。
  • StopILRuntime:关闭两个 MemoryStreamAppDomain 置空,isStart 复位,避免重复 Start 时状态错乱。

Loading 界面:先更 AB,再起脚本域

  • 界面素材:Loading 预制体里放背景、进度条 Image、说明 Text;脚本在 Start 里把进度条宽度先收到 0,文案写「资源加载中」。
  • 主流程BeginUpdateAssetBundleUpdateManager.CheckUpdate,三个委托分别处理:更新是否结束、日志刷到 infoTxt、用 (current, max) 驱动进度条宽度(示例里用 nowNum / maxNum * 1600 作条宽)。
  • 衔接 ILRuntime:仅当 CheckUpdate 成功回调为 true 时才 StartILRuntime;失败则提示网络或服务端。StartILRuntime 除完成回调外再带 infoCallBack,把拉 dll、pdb、初始化、等调试器等步骤打到 UI 上,排障不用猜卡在哪一步。

热更启动链速查

按时间顺序扫一眼,细节仍以各 ### 小节为准。

顺序 模块 在干什么
1 Main 异步加载 loading_ui,挂 LoadingPanelClearAssetBundleBeginUpdate
2 AssetBundleUpdateManager 下对比文件、比对 md5、删旧包、拉缺包,落地最新 ABCompareInfo.txt
3 ILRuntimeManager 从 AB 拉 dll、pdb,LoadAssembly 起热更域
4 热更 DLL InvokeILRuntimeMain,后续逻辑跑在热更侧

建议实现顺序

  1. 确认 AssetBundleManager 持久化优先逻辑与主包平台名一致,能本地模拟「Streaming 一份、持久化覆盖一份」。
  2. 热更 DLL 改后缀、打进 dll_res,用编辑器与真机各打一轮 AB,确认 TextAsset.bytes 体积合理。
  3. ILRuntimeManager 双异步回调链,非调试路径跑通后再加等待调试器逻辑。
  4. 最后做 LoadingPanel:先把 CheckUpdate 三个委托绑到 UI,再并入 StartILRuntime 的文本回调。
  5. 本机搭 FTP:Serv-U 中建域、建可上传下载的账号;在 AssetBundleUpdateManagerABTools 里把 ftp 地址、用户名、密码改成与服务器一致,用资源管理器访问 ftp://本机IP/ 先自测。
  6. 资源侧:ArtRes 目录按工具约定建好;热更 DLL、PDB 的生成路径不要落在待打进默认包的 StreamingAssets 里,生成后改 .txt 再参与 AB;打 AB 或打 Player 前若工程里写了 UnityMainThreadID,先整行注释,构建结束后再取消注释。
  7. 上架:FTP 上建好 AB/平台/ 层级;用工具生成 ABCompareInfo.txt 并与各 AB 一并上传;默认资源里带上 LoadingPanel 与主包,保证首包冷启动有界面和 manifest。
  8. 场景里挂 Main:异步从 loading_ui 实例化 LoadingPanelCanvasClearAssetBundle 后走 CheckUpdate → StartILRuntime → appDomain.Invoke(...) 进入热更程序集。
  9. 迭代热更:热更工程改动后重编 DLL、改 txt、重打 dll_res、重传对比文件;用已发布的空壳包验证「只更服务端、客户端能拉新」。

本机 FTP 与客户端地址

  • Serv-U 装好后按向导建域即可,FTP 账号要同时具备读和写,否则上传工具与运行时下拉都会对不上。
  • 工程里 FtpWebRequest 的 URI 一般是 ftp://IP/AB/平台名/文件名,与你在资源服务器磁盘上建的文件夹一一对应;改完代码务必和本机实际 IP、账号密码同步,否则只能在本机浏览器里打开 FTP、客户端却仍 530/路径错。
要对齐的项 说明
账号口令 AssetBundleUpdateManager、编辑器上传工具与 Serv-U 账号一致
目录层级 至少 AB + 平台子目录,和下载代码里拼的路径相同
冒烟方式 资源管理器 ftp://IP/ 能列目录,再跑 Unity 下载对比文件

AB 产出、对比文件与默认包

  • ArtRes:正文里 AB 工具把输出根写死在 ArtRes 下,先建文件夹再打,避免打空或打到意料外的路径。
  • 热更程序集:DLL、PDB 不要和「要写进 StreamingAssets 的默认包」混路径;打完改成 .txtTextAsset 打 AB,和前面 Loading → ILRuntime 链一致。
  • UnityMainThreadID 与打包:这行只在真机跑 ILRuntime 时需要;Editor 里打 AB、打 Player 安装包前都应先注释,构建完成后再取消注释,否则可能报错;避免把构建问题误判成资源依赖。
  • 对比文件ABCompareInfo.txt 描述远端每个 AB 名、版本或 md5 等信息,客户端 CheckUpdate 靠它和本地比对后再决定下哪些、删哪些;上传工具里要和 AB 一起推到 FTP。
  • 默认资源LoadingPanel 不进默认包则首包黑屏无提示;主 manifest 所在包也要勾选进首包,否则依赖链拉不起来。

场景入口与热更调用链

  • Main 职责:首场景只负责把 UI 架子搭起来——异步加载 loading_ui 包里的 LoadingPanel 预制体,挂到场景已有 Canvas 下,拿到 LoadingPanel 组件后调用 BeginUpdate。不在主工程里写玩法,避免和 DLL 里逻辑分叉。
  • 为何要 ClearAssetBundle:实例化 Loading 时可能已经通过 AB 管理器加载过包内资源;在开始 CheckUpdate、重新从持久化目录拉包之前,清空字典与主 manifest 引用,减少「旧句柄仍占着、新文件已到磁盘却仍当没更新」一类状态。教学工程里文案强调「主包已有记录」也是同一层担心。
  • 热更入口StartILRuntime 的完成回调里用 appDomain.Invoke("HotFix_Project.ILRuntimeMain", "StartILRuntime", null, null) 调热更程序集里的静态入口,之后 UI 销毁、重新异步拉界面、LoadAssetBundleResource 拖资源等都可以写在 ILRuntimeMain 里验证「逻辑真在 DLL 里跑」。
  • 验收方式:Editor 直连本机 FTP 能看到下载日志与进度;打出空壳包后改热更 UI 或入口代码,只重传 AB 与对比文件、不动安装包,客户端应能拉到新内容。文中用 Destroy 整棵 Canvas、Primitive 创建立方体等写法偏演示,正式项目要换成明确的资源生命周期管理。

7.3 面试题精选

进阶题

1. 持久化目录优先加载 AB 对热更有何意义?

题目

AssetBundleManager 里为什么先判断 persistentDataPath 有没有包,再退回 StreamingAssetsPath?若不做这步会怎样?

深入解析
  • Streaming 一般是安装包内只读资源,首装或清数据后从这里读到底包 AB。
  • Persistent 是可写目录,下载或替换后的 AB 落在这里;同名文件存在说明玩家已经拉过新资源。
  • 若只读 Streaming,热更下来的 AB 不会被用到;若只读 Persistent,首装无网时可能拿不到底包。正文写法是「有则更用新的,没有再读包内」,兼顾冷启动与热更。
答题示例

持久化路径放下载后的新 AB,Streaming 是包内默认 AB。

先判持久化有没有文件,有才用本地热更结果,没有再用包内资源,首包能玩、后续也能覆盖。

只做一边会偏:只认 Streaming 热更白下;只认 Persistent 第一次可能什么也没有。

参考文章
  • 1.相关内容导入
  • 3.默认加载界面处理

2. Loading 流程里为何把 ILRuntime 初始化放在 AB 更新完成之后?

题目

LoadingPanel 为什么要在 CheckUpdate 成功后再调用 StartILRuntime,而不是一进游戏就先起 AppDomain

深入解析
  • dll_res 本身是 AB;远端清单或 AB 未同步完时,本地可能仍是旧 DLL,或根本没有新包。
  • 先走 CheckUpdate:拉对比文件、按 md5 差量补下、删多余文件,保证磁盘上 AB 与服务器一致,再让管理器去加载 TextAsset,避免脚本域已起却读到过期或缺失程序集。
  • 进度上把「资源更新」和「脚本域初始化」拆开,问题定位更清楚。
答题示例

热更 DLL 在 AB 里,必须先保证 AB 更完,否则可能是旧 dll 或缺文件。

CheckUpdate 把清单和 AB 对齐后再 StartILRuntime,从 AB 里异步拿 dll、pdb 才一致。

前面失败多半是 FTP、对比文件或下载链路,后面卡在 ILRuntime 再查 dll_res 与 LoadAssembly。

参考文章
  • 2.ILRuntimeManager逻辑处理
  • 3.默认加载界面处理

3. 客户端 FTP 路径里为什么要带平台子目录?

题目

DownLoadFile 一类代码里为什么在 AB 下面还要拼 IOS / Android / PC?只放一个扁平的 AB 目录行不行?

深入解析
  • 正文约定上传端按平台分子文件夹,文件名在各平台可以同名(例如都叫某个 bundle 名),互不覆盖。
  • 客户端用 #if UNITY_IOS 等宏选子路径,保证打出来的包装到真机上时拉的是本平台那份 AB,不会把 PC 包下到手机里。
  • 若强行扁平,要么所有平台文件名全局唯一(维护成本高),要么容易覆盖、串包;分子目录是最省事的隔离方式。
答题示例

因为三个平台的 AB 二进制不通用,同名文件可以各放一份。

代码里在 AB 后面再拼平台文件夹,下载时自动选对那一支,避免混用资源。

扁平目录取决于你是否能保证全局唯一命名;教学项目里用子目录最简单。

参考文章
  • 4.Ftp服务器搭建
  • 5.相关AB包生成上传

4. 打 AB 或打 Player 时为什么要临时注释 UnityMainThreadID

题目

正文里在生成 AB、以及打可执行安装包时,appDomain.UnityMainThreadID = … 都可能触发构建期报错,需要先注释再构建、构建完再还原。原因是什么?

深入解析
  • 这行依赖运行中的 ILRuntime AppDomain 与当前线程;Editor 里走 BuildPipeline / 打 AB / 打 Player 时,不一定存在与真机一致的 ILRuntime 环境,脚本若在构建期被加载执行,容易空引用或与打包管线冲突。
  • 运行时调试与 Profiler 对齐主线程有意义,对纯构建无意义,构建阶段关掉最省事。
  • AB 与安装包打完都要记得取消注释,否则真机跑热更时缺主线程绑定,调试或行为异常。
答题示例

那是运行时给 ILRuntime 用的,打 AB、打 Player 是 Editor 构建流程,执行到那行容易和当前环境对不上就报错。

所以对构建先注释,构建完再打开,避免构建失败,也不误伤真机上的赋值。

参考文章
  • 5.相关AB包生成上传
  • 6.逻辑串联

深度题

1. ILRuntime 用 MemoryStream 加载程序集时要注意什么?

题目

正文为什么把 TextAsset.bytes 放进 MemoryStream 再交给 LoadAssembly,而不是对磁盘路径做 Assembly.LoadFromStopILRuntime 里为何要关流?

深入解析
  • ILRuntime 的 AppDomain.LoadAssembly 面向 字节流 + 符号读取器,数据来自 AB 解出的 TextAsset,用流最直接。
  • Assembly.LoadFrom 走 CLR 默认加载上下文,与 ILRuntime 内热更域不是一条路。
  • 不关流会长期占缓冲;Stop 时关流并置空 ILRuntime 侧 AppDomain,才能释放托管资源,并让 isStart 允许再次完整走一遍启动流程。若域仍占用流,重复 LoadAssembly 也容易乱。
答题示例

LoadAssembly 吃的是流和 PDB 提供器,TextAsset 只有字节数组,包成 MemoryStream 对齐接口。

LoadFrom 是另一套加载模型,不适合这里的热更域。

Stop 里关流、清 domain,释放内存并允许下次重新 Start。

参考文章
  • 2.ILRuntimeManager逻辑处理

2. Main 里为什么在 CheckUpdate 前要 Clear?热更入口为什么用 Invoke?

题目

Main 在调用 BeginUpdate 之前为什么要 AssetBundleManager.ClearAssetBundle()StartILRuntime 成功后为什么不直接在主工程里写 ILRuntimeMain.StartILRuntime(),而用 appDomain.Invoke

深入解析
  • Clear:首包从 Streaming 加载过 manifest 或其它 AB 后,管理器内部字典和非空的主包引用还在;接下来 CheckUpdate 会从 FTP 把新 AB 落到 persistent 并可能删旧文件。若不先 Unload/Clear,后续 LoadFromFile 可能仍命中旧句柄或与磁盘不一致,调试时表现为「对比文件说更新了,加载却仍像旧的」。清空后让整条链从「当前磁盘上的包」重新建状态更干净。
  • InvokeILRuntimeMain 在热更 DLL 里,由 ILRuntime 的 AppDomain 加载。主工程不引用热更程序集时,不能直接写 ILRuntimeMain.StartILRuntime() 通过编译;Invoke("命名空间.类名", "方法名", null, null) 在热更域里做反射式调用,是桥接主工程与热更域的薄封装。后两个 null 对应示例里的无参静态方法(无实例、无额外参数)。
  • 工程上也可做 Adapter + 工程引用 + CLR 绑定 显式调热更类型,样板更多;本实践刻意保持主工程与热更 DLL 解耦,故用字符串 Invoke 最少依赖。
答题示例

Clear 是为了清掉上一阶段加载留下的 AB 记录,避免和下载后的磁盘状态打架,尤其 manifest 已经加载过时更要重置。

热更类在主工程里没有引用,不能直接 C# 调用,要用 ILRuntime 的 appDomain.Invoke 在热更域里起入口。

参考文章
  • 6.逻辑串联


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏