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:关闭两个MemoryStream,AppDomain置空,isStart复位,避免重复Start时状态错乱。
Loading 界面:先更 AB,再起脚本域
- 界面素材:Loading 预制体里放背景、进度条
Image、说明Text;脚本在Start里把进度条宽度先收到 0,文案写「资源加载中」。 - 主流程:
BeginUpdate调AssetBundleUpdateManager.CheckUpdate,三个委托分别处理:更新是否结束、日志刷到infoTxt、用(current, max)驱动进度条宽度(示例里用nowNum / maxNum * 1600作条宽)。 - 衔接 ILRuntime:仅当
CheckUpdate成功回调为true时才StartILRuntime;失败则提示网络或服务端。StartILRuntime除完成回调外再带infoCallBack,把拉 dll、pdb、初始化、等调试器等步骤打到 UI 上,排障不用猜卡在哪一步。
热更启动链速查
按时间顺序扫一眼,细节仍以各 ### 小节为准。
| 顺序 | 模块 | 在干什么 |
|---|---|---|
| 1 | Main |
异步加载 loading_ui,挂 LoadingPanel,ClearAssetBundle 后 BeginUpdate |
| 2 | AssetBundleUpdateManager |
下对比文件、比对 md5、删旧包、拉缺包,落地最新 ABCompareInfo.txt |
| 3 | ILRuntimeManager |
从 AB 拉 dll、pdb,LoadAssembly 起热更域 |
| 4 | 热更 DLL | Invoke 进 ILRuntimeMain,后续逻辑跑在热更侧 |
建议实现顺序
- 确认
AssetBundleManager持久化优先逻辑与主包平台名一致,能本地模拟「Streaming 一份、持久化覆盖一份」。 - 热更 DLL 改后缀、打进
dll_res,用编辑器与真机各打一轮 AB,确认TextAsset.bytes体积合理。 - 写
ILRuntimeManager双异步回调链,非调试路径跑通后再加等待调试器逻辑。 - 最后做
LoadingPanel:先把CheckUpdate三个委托绑到 UI,再并入StartILRuntime的文本回调。 - 本机搭 FTP:Serv-U 中建域、建可上传下载的账号;在
AssetBundleUpdateManager、ABTools里把ftp地址、用户名、密码改成与服务器一致,用资源管理器访问ftp://本机IP/先自测。 - 资源侧:
ArtRes目录按工具约定建好;热更 DLL、PDB 的生成路径不要落在待打进默认包的 StreamingAssets 里,生成后改.txt再参与 AB;打 AB 或打 Player 前若工程里写了UnityMainThreadID,先整行注释,构建结束后再取消注释。 - 上架:FTP 上建好
AB/平台/层级;用工具生成ABCompareInfo.txt并与各 AB 一并上传;默认资源里带上LoadingPanel与主包,保证首包冷启动有界面和 manifest。 - 场景里挂
Main:异步从loading_ui实例化LoadingPanel到Canvas,ClearAssetBundle后走CheckUpdate → StartILRuntime → appDomain.Invoke(...)进入热更程序集。 - 迭代热更:热更工程改动后重编 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 的默认包」混路径;打完改成
.txt当TextAsset打 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.LoadFrom?StopILRuntime 里为何要关流?
深入解析
- 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可能仍命中旧句柄或与磁盘不一致,调试时表现为「对比文件说更新了,加载却仍像旧的」。清空后让整条链从「当前磁盘上的包」重新建状态更干净。 - Invoke:
ILRuntimeMain在热更 DLL 里,由 ILRuntime 的AppDomain加载。主工程不引用热更程序集时,不能直接写ILRuntimeMain.StartILRuntime()通过编译;Invoke("命名空间.类名", "方法名", null, null)在热更域里做反射式调用,是桥接主工程与热更域的薄封装。后两个null对应示例里的无参静态方法(无实例、无额外参数)。 - 工程上也可做 Adapter + 工程引用 + CLR 绑定 显式调热更类型,样板更多;本实践刻意保持主工程与热更 DLL 解耦,故用字符串
Invoke最少依赖。
答题示例
Clear 是为了清掉上一阶段加载留下的 AB 记录,避免和下载后的磁盘状态打架,尤其 manifest 已经加载过时更要重置。
热更类在主工程里没有引用,不能直接 C# 调用,要用 ILRuntime 的
appDomain.Invoke在热更域里起入口。
参考文章
- 6.逻辑串联
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com