跳到正文
Qiita · Unity 开发社区· 来源· hiyoko-cake·· 4 小时前AI 评分50

【Unity】曾为 Addressables 的 handle 管理和忘记释放而烦恼,于是试着做了一个管理模块

【Unity】Addressablesのhandle管理と解放忘れに悩んだので管理モジュールを作ってみた

正文 · AI 翻译

前言

我制作了一个统一管理 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 的使用方法。

準備

  1. 通过 Create > MAH > Addressable > RuntimeSettings 创建设置资源
  2. 在启动场景的 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 中。
因此,如果它被多个组使用,构建时可能会被重复包含。

  1. 打开窗口并按下 Scan
  2. 未注册的资源会逐行显示(名称、类型、大小、正在使用它的组数量等)
  3. 被两个以上组使用的资源会被强调显示。展开后,可以看到使用它的父资源与组
  4. 单击某一行,会在 Project 窗口中选中该资源

想重新审视组的划分方式时,可以方便地使用。

自动注册(Auto Registration)

将资源注册到 Addressables 的工作(放入组、决定地址、添加 Label)会按照规则自动化。规则是 ScriptableObject,按目标文件夹分别创建。

項目 内容
目标文件夹 目标文件夹(以 GUID 保存,因此移动或重命名也不会失效)
扩展名 要注册的扩展名
地址模板 地址的添加方式。可使用 {Asset.Name}、{Folder.Name} 等宏
目标组 注册目标组。若不存在则自动创建
标签 要附加的 Label。可使用宏
  • 在创建规则之前不会发生任何事,因此仅安装包并不会改写组
  • 如果资产和规则相同,结果始终相同。在团队所有成员的环境中,都会得到相同的注册结果
  • 当多个规则适用于同一个资源时,会作为冲突报错,而不是注册。
  • 对已有资源,可以通过窗口中的重新应用(Reapply)来应用。写入之前,可以在列表中确认变更内容。

手动批量注册(Register to Addressables)

这是用于不值得创建规则的场合,或想自己决定注册时机的场合的工具。

  1. 在 Project 窗口中右键单击文件夹,选择 MAH → Register to Addressables...
  2. 其下的资源会出现在列表中(已注册的除外)
  3. 用扩展名和复选框进行筛选,选择要注册到的分组和 Label
  4. 按下 注册。在确认对话框中确认件数和新分组后再执行注册

其他

  • 带有 [AddressablePath] / [AddressableLabel] 属性的 string 字段,可以在 Inspector 中从下拉菜单选择地址和 Label(防止拼写错误)
  • Editor 工具的显示可以在日语和英语之间切换(Edit > Preferences > MAH Addressable > Language)。只要准备好 JSON 文件,还可以添加其他语言

结语

如果能为那些苦恼于 Addressables 的 handle 管理或忘记释放的人提供参考,我会很高兴。
详细用法已整理在 GitHub 的 README 和 Docs/usage.md(英语)中。如有缺陷或需求,欢迎通过 Issue 提出。

来源:Qiita · Unity 开发社区 · qiita.com