【Unity】曾为 Addressables 的 handle 管理和忘记释放而烦恼,于是试着做了一个管理模块
【Unity】Addressablesのhandle管理と解放忘れに悩んだので管理モジュールを作ってみた
前言
我制作了一个统一管理 Unity Addressables 从加载到释放的模块 MAH-AddressableManager,并已在 GitHub 上公开。
可以通过 UPM(git URL)导入。
本文将介绍制作理由、使用方法以及内部实现中下功夫的地方。
動作環境
- Unity 6000.0 及以上
- Addressables 2.9.0 及以上
可以做的事
-
只需用
LoadAsync<T>和Release即可处理(handle 和引用计数由模块管理) - 同一个资产无论 Load 多少次,实际加载只进行一次
- 忘记释放的部分,会在 Load 时指定的寿命(Manual / Scene / Owner)结束时自动回收
- 指定 Label,即可将类型不同的资产批量加载、批量释放
- 更新数据的确认(数量・容量)与下载(带进度・重试)
-
通过
[AddressablePath]/[AddressableLabel]属性,可从 Inspector 的下拉菜单中选择地址・Label - 附带可确认泄漏和引用计数的调试窗口等 Editor 工具
使用方法
这里进行简单说明。
详情请参阅 GitHub 的使用方法。
準備
-
通过
Create > MAH > Addressable > RuntimeSettings创建设置资源 - 在启动场景的 GameObject 上添加
AddressableManager,并分配设置资源
Manager 通过 DontDestroyOnLoad 常驻,启动时自动开始 Addressables 的初始化(也可通过设置改为手动)。
加载与释放
操作入口只有 AddressableManager.AccInstance.AccHandler 这一个。
using System;
using MAH.AddressableManager;
using MAH.AddressableManager.Attribute;
using UnityEngine;
public sealed class EnemyView : MonoBehaviour
{
// Inspector のプルダウンからアドレスを選ぶ
[SerializeField, AddressablePath] private string _spriteAddress;
private AssetTicket<Sprite> _ticket;
private bool _hasTicket;
private async void Start()
{
var handler = AddressableManager.AccInstance.AccHandler;
try
{
// 読み込んだアセット本体と、解放用のチケットが返る
_ticket = await handler.LoadAsync<Sprite>(
AssetKey.FromPath(_spriteAddress), AssetLifetime.Manual, destroyCancellationToken);
_hasTicket = true;
GetComponent<SpriteRenderer>().sprite = _ticket.AccAsset;
}
catch (OperationCanceledException)
{
// 読み込み中に破棄された。チケットは発行されないので解放は不要
}
}
private void OnDestroy()
{
// 使い終わったらチケットを返して解放する
if (_hasTicket) AddressableManager.AccInstance?.AccHandler.Release(_ticket);
}
}
向 Release 传入的不是 handle,而是票券。即使从多个位置 Load 同一个资产,只要各自返回自己的票券即可,因此无需考虑“需要 Release 多少次”。即使对同一张票券执行两次 Release,也只会发出警告,不会成为异常。
通过生命周期防范忘记释放
如果在 Load 时指定生命周期,那么未被 Release 的部分会在该生命周期结束时自动回收。
| 寿命 | 自动回收的时机 |
|---|---|
AssetLifetime.Manual |
不会自动回收 |
AssetLifetime.Scene() |
调用时所在的场景被卸载时 |
AssetLifetime.Owner(this) |
指定的 GameObject 被销毁时 |
// この GameObject が破棄されたら、Release を忘れていても回収される
var ticket = await handler.LoadAsync<Sprite>(
AssetKey.FromPath(_address), AssetLifetime.Owner(this), ct);
自动回收是安全网,基本前提是调用 Release。发生自动回收时,Editor 会给出带有分配位置堆栈跟踪的警告,因此可以立刻找到忘记释放的位置。
用 Label 批量读取
Load 时不指定类型,在取出时指定类型。
var group = await handler.LoadByLabelAsync(_label, AssetLifetime.Owner(this), ct);
if (group.TryGetAsset<Sprite>("icon_sword", out var sprite))
_image.sprite = sprite;
var clips = group.GetAssets<AudioClip>();
for (int i = 0; i < clips.Count; i++)
Register(clips[i]);
// まとめて解放
handler.Release(group);
下载更新数据
可以先获取要显示在确认对话框中的件数与容量,获得同意后再下载。
var result = await handler.CheckUpdateAsync(ct);
if (result.AccHasDownload && await ShowConfirmDialogAsync(result.AccBundleCount, result.AccTotalBytes))
{
try
{
await handler.DownloadAsync(Progress.Create<DownloadStatus>(OnProgress), ct);
}
catch (AddressableDownloadException ex)
{
ShowRetryDialog(ex); // ex.AccPhase で失敗した段階が分かる
}
}
Editor 工具
为了解决“不知道现在正在加载什么”“向 Addressables 注册很麻烦”这些问题,我也制作了几个 Editor 工具。
| 工具 | 打开方法 | 可以做的事 |
|---|---|---|
| 调试窗口 | MAH/Addressable/Debug Window |
确认 Play 过程中的加载状况、泄漏以及 Owner 的回收情况 |
| 依赖检查 | MAH/Addressable/Dependency Check |
无需进行 Play 或构建,即可列出仍以未注册状态使用的资源 |
| 自動登録 | MAH/Addressable/Auto Registration |
按照规则,在导入时自动注册到 Addressables |
| 手動一括登録 | 在 Project 窗口中右键 → MAH → Register to Addressables...
|
将所选文件夹中的内容批量注册 |
调试窗口与 Dependency Check 均为只读。会改写 Addressables 设置的只有两种注册操作(自动注册与手动批量注册),且仅在用户主动指示时才会执行。
调试窗口
在 Play 过程中,可通过切换标签页来查看 AddressableManager 的状态。
- 运行时状态:以列表形式显示已加载的资源。除资源名、引用计数、加载状态(加载中/完成/失败)之外,还能查看按生命周期区分的分配数量(Manual / Scene / Owner),以及分配位置的堆栈跟踪。点击堆栈跟踪中的行,即可跳转到对应的代码
-
泄漏:统计未
Release、在生命周期结束时被自动回收的对象,并按分配位置汇总。Console 日志会不断滚动,容易看漏,因此可以在这里集中确认需要修复的位置。还可以输出到文本文件。 - Owner:确认 Owner 生命周期的监视与回收是否正常工作。
被记录为泄漏的是:在 Scene / Owner 生命周期中分配、虽然收到了 ticket 却没有Release的对象。该功能仅在 Editor 中可用,在构建后的应用中不会记录。
依赖检查
列出已注册到 Addressables 的资产所使用的未注册资产。未注册资产会被隐式包含在使用它的已注册资产的 Bundle 中。
因此,如果它被多个组使用,构建时可能会被重复包含。
- 打开窗口并按下 Scan
- 未注册的资源会逐行显示(名称、类型、大小、正在使用它的组数量等)
- 被两个以上组使用的资源会被强调显示。展开后,可以看到使用它的父资源与组
- 单击某一行,会在 Project 窗口中选中该资源
想重新审视组的划分方式时,可以方便地使用。
自动注册(Auto Registration)
将资源注册到 Addressables 的工作(放入组、决定地址、添加 Label)会按照规则自动化。规则是 ScriptableObject,按目标文件夹分别创建。
| 項目 | 内容 |
|---|---|
| 目标文件夹 | 目标文件夹(以 GUID 保存,因此移动或重命名也不会失效) |
| 扩展名 | 要注册的扩展名 |
| 地址模板 | 地址的添加方式。可使用 {Asset.Name}、{Folder.Name} 等宏 |
| 目标组 | 注册目标组。若不存在则自动创建 |
| 标签 | 要附加的 Label。可使用宏 |
- 在创建规则之前不会发生任何事,因此仅安装包并不会改写组
- 如果资产和规则相同,结果始终相同。在团队所有成员的环境中,都会得到相同的注册结果
- 当多个规则适用于同一个资源时,会作为冲突报错,而不是注册。
- 对已有资源,可以通过窗口中的重新应用(Reapply)来应用。写入之前,可以在列表中确认变更内容。
手动批量注册(Register to Addressables)
这是用于不值得创建规则的场合,或想自己决定注册时机的场合的工具。
- 在 Project 窗口中右键单击文件夹,选择
MAH→Register to Addressables... - 其下的资源会出现在列表中(已注册的除外)
- 用扩展名和复选框进行筛选,选择要注册到的分组和 Label
- 按下 注册。在确认对话框中确认件数和新分组后再执行注册
其他
-
带有
[AddressablePath]/[AddressableLabel]属性的 string 字段,可以在 Inspector 中从下拉菜单选择地址和 Label(防止拼写错误) - Editor 工具的显示可以在日语和英语之间切换(
Edit > Preferences > MAH Addressable > Language)。只要准备好 JSON 文件,还可以添加其他语言
结语
如果能为那些苦恼于 Addressables 的 handle 管理或忘记释放的人提供参考,我会很高兴。
详细用法已整理在 GitHub 的 README 和 Docs/usage.md(英语)中。如有缺陷或需求,欢迎通过 Issue 提出。
来源:Qiita · Unity 开发社区 · qiita.com