Featured image of post Addressables的认识与使用

Addressables的认识与使用

Unity Addressables 动态资源管理系统从零入门:核心概念、加载 API、资源组、远程热更新与最佳实践

什么是 Addressables

Addressables 是 Unity 官方推出的动态资源管理系统,它允许你通过一个简单的"地址"字符串来加载资源,无论该资源是打包在应用本地,还是存放在远程服务器上。

一句话概括:Addressables = 官方在 AssetBundle 之上封装的一套"按地址加载资源"的完整方案,帮你自动处理打包、依赖、加载、卸载、远程更新等繁琐细节。

下面从"为什么需要它"讲起,逐步深入到能真正上手开发。


一、为什么需要 Addressables

在 Addressables 出现之前,Unity 加载资源主要有两种方式,它们各有明显缺陷。

1.1 Resources 文件夹

把资源放进 Assets/Resources,然后用 Resources.Load 加载。

1
2
var prefab = Resources.Load<GameObject>("Prefabs/Player");
Instantiate(prefab);

优点:简单,不用打包配置。

缺点(也是它被官方劝退的原因):

问题 说明
全部打进包 Resources 里所有资源都会被无条件打进安装包,导致包体臃肿
无法热更新 资源在构建后就固定了,不能在线替换
启动变慢 游戏启动时要加载整个 Resources 索引,文件夹越大越慢
内存僵硬 缺少引用计数,加载后难以精确卸载
路径字符串硬编码 拼错只有运行时才报错,难以维护

Unity 官方文档明确建议:新项目不要使用 Resources 文件夹,改用 Addressables。

1.2 AssetBundle(AB 包)

AssetBundle 是 Unity 底层的资源打包格式,功能强大但非常"原始"。

痛点:

  • 要自己规划哪些资源打进哪个包
  • 要自己处理依赖关系(A 包依赖 B 包,必须先加载 B)
  • 要自己管理加载 / 卸载和引用计数
  • 要自己实现版本号、Catalog、下载、校验
  • 极易出现"资源重复打包"“内存泄漏"“依赖丢失"等问题

1.3 Addressables 的定位

Addressables 把上面这些脏活累活封装起来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
          ┌─────────────────────────────────────────┐
          │           你的游戏代码                    │
          │   Addressables.LoadAssetAsync<T>("地址")  │
          └───────────────────┬─────────────────────┘
                              │
          ┌───────────────────▼─────────────────────┐
          │           Addressables 系统              │
          │  · 地址解析   · 依赖分析   · 引用计数      │
          │  · 分组打包   · 远程下载   · 目录管理      │
          └───────────────────┬─────────────────────┘
                              │
          ┌───────────────────▼─────────────────────┐
          │              AssetBundle                │
          └─────────────────────────────────────────┘

三种方式对比:

维度 Resources AssetBundle Addressables
上手难度 极低 高 中低
按需加载 差 好 好
依赖管理 自动但僵硬 手动 自动
热更新 不支持 支持(自己实现) 支持(内置)
引用计数 无 手动 内置
包体控制 差 好 好
官方推荐 否 底层可选 是

二、核心概念

理解这几个名词,就理解了 Addressables 的骨架。

概念 英文 说明
地址 Address 加载资源用的字符串 key,如 "Player"、"UI/Login"
资源条目 Asset Entry 标记为 Addressable 的一个资源
组 Group 资源的集合,一个组会打成一个或多个 Bundle
标签 Label 给资源打的标记,可按标签批量加载
配置档案 Profile 一组路径变量(本地/远程构建与加载路径)
目录 Catalog 记录所有地址、依赖、Bundle 位置的清单文件
加载句柄 AsyncOperationHandle 异步操作的句柄,用于获取结果和释放
资源引用 AssetReference 可序列化的资源引用,避免直接硬引用

它们的关系:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Address "Player"
      │ 属于
      ▼
   Asset Entry ──标记──> Label "character"
      │ 归属
      ▼
    Group "Characters" ──打包──> Bundle (.bundle)
      │ 记录在
      ▼
   Catalog (catalog.json)
      │ 存放位置由
      ▼
   Profile 变量(本地 / 远程)

关键点:地址(Address)不是资源路径。 它是一个逻辑名字,你完全可以把 Assets/Prefabs/Player.prefab 的地址设为 "Player"。地址可以任意修改,而不用改代码里的资源位置。


三、安装与初始化

3.1 安装包

  1. 打开 Window > Package Manager
  2. 选择 Unity Registry
  3. 搜索 Addressables,点击 Install

或直接编辑 Packages/manifest.json:

1
2
3
4
5
{
  "dependencies": {
    "com.unity.addressables": "1.21.21"
  }
}

不同 Unity 版本对应的 Addressables 版本不同,2021 LTS 常用 1.19~1.21,Unity 6 开始有 2.x。以包管理器推荐的版本为准。

3.2 初始化设置

安装后打开 Window > Asset Management > Addressables > Groups。

第一次打开会提示创建 Addressable 设置文件,它会生成:

1
2
3
4
5
Assets/AddressableAssetsData/
├── AddressableAssetSettings.asset   # 系统总配置
├── AssetGroups/                     # 默认资源组
├── DataBuilders/                    # 打包脚本
└── ...

这个文件夹必须提交到版本控制(它是配置,不是产物)。

3.3 Play Mode 脚本

Groups 窗口顶部有一个 Play Mode Script 下拉框,决定编辑器里运行时的加载方式:

模式 作用 使用场景
Use Asset Database (fastest) 直接读 Asset 数据库,不打包 日常开发,迭代最快
Simulate Groups (advanced) 模拟打包,但资源仍从 Asset 读 测试打包布局和依赖
Use Existing Build (requires built groups) 用上次真实构建的 Bundle 测试真机行为、远程下载

新手最常见的困惑:“我刚标记了资源,运行时却加载不到”——多半是这里选了 Use Existing Build 但没重新 Build。开发期请用 Use Asset Database。


四、快速上手

4.1 把一个资源标记为 Addressable

两种方式:

  1. 选中资源,在 Inspector 顶部勾选 Addressable
  2. 直接把它拖进 Addressables Groups 窗口

标记后,资源会出现在 Groups 窗口中,并自动生成一个地址(默认就是资源路径)。

建议:手动把地址改成语义化的短名字,例如 "Player" 而不是 "Assets/Prefabs/Player.prefab"。

4.2 加载资源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class LoadExample : MonoBehaviour
{
    async void Start()
    {
        // 异步加载(推荐):await 等待完成,拿到资源
        AsyncOperationHandle<GameObject> handle =
            Addressables.LoadAssetAsync<GameObject>("Player");

        await handle.Task;

        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            GameObject prefab = handle.Result;
            Instantiate(prefab);
        }

        // 用完释放(重要!)
        Addressables.Release(handle);
    }
}

也可以不用 async/await,用回调:

1
2
3
4
5
6
var handle = Addressables.LoadAssetAsync<GameObject>("Player");
handle.Completed += op =>
{
    if (op.Status == AsyncOperationStatus.Succeeded)
        Instantiate(op.Result);
};

4.3 直接实例化

如果只是想生成一个 Prefab 实例,用 InstantiateAsync 更省事(它内部帮你加载 + 实例化):

1
2
3
4
5
6
7
8
9
AsyncOperationHandle<GameObject> handle =
    Addressables.InstantiateAsync("Player", parent: transform);

await handle.Task;

GameObject go = handle.Result;

// 销毁实例并释放引用
Addressables.ReleaseInstance(go);

ReleaseInstance 会销毁这个由 Addressables 实例化的对象,并递减引用计数。不要用普通的 Destroy,否则引用计数不会释放,造成内存泄漏。

4.4 加载图标 / 文本等非 GameObject 资源

1
2
3
4
5
6
7
8
9
// 加载 Sprite
var spriteHandle = Addressables.LoadAssetAsync<Sprite>("Icon_Coin");
await spriteHandle.Task;
image.sprite = spriteHandle.Result;

// 加载 TextAsset(配置文本)
var textHandle = Addressables.LoadAssetAsync<TextAsset>("config_level");
await textHandle.Task;
string json = textHandle.Result.text;

五、加载 API 详解

5.1 主要加载接口

API 说明
LoadAssetAsync<T>(key) 加载单个资源,返回 AsyncOperationHandle<T>
LoadAssetsAsync<T>(key, callback, mergeMode) 按 key 或标签批量加载
InstantiateAsync(key, parent, ...) 加载并实例化 Prefab
LoadSceneAsync(key, loadMode) 加载场景
DownloadDependenciesAsync(key) 只下载依赖(远程资源)
GetDownloadSizeAsync(key) 查询需要下载的大小

5.2 AsyncOperationHandle

AsyncOperationHandle<T> 是 Addressables 异步操作的核心对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var handle = Addressables.LoadAssetAsync<GameObject>("Player");

// 1. 检查状态
handle.Status;              // None / Succeeded / Failed
handle.IsValid();           // 句柄是否有效
handle.IsDone;              // 是否完成

// 2. 获取进度
handle.PercentComplete;     // 0.0 ~ 1.0

// 3. 取结果
handle.Result;

// 4. 注册回调(可注册多个)
handle.Completed += OnLoaded;

// 5. 同步等待(阻塞当前线程,谨慎使用!)
handle.WaitForCompletion();

// 6. 释放
Addressables.Release(handle);

WaitForCompletion() 会卡住主线程直到加载完成,会造成掉帧。只在初始化等确实需要同步的场景使用,普通游戏逻辑一律用异步。

5.3 批量加载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 方式一:多个地址一次加载
var keys = new List<string> { "Icon_Coin", "Icon_Gem", "Icon_Heart" };
var handle = Addressables.LoadAssetsAsync<Sprite>(keys, null, MergeMode.Union);
await handle.Task;
foreach (var sprite in handle.Result) { /* ... */ }
Addressables.Release(handle);

// 方式二:按标签批量加载
var labelHandle = Addressables.LoadAssetsAsync<Sprite>("ui_icon", null);
await labelHandle.Task;
Addressables.Release(labelHandle);

MergeMode(合并模式)用于多个 key 的结果合并方式:

模式 含义
UseFirst 使用第一个 key 的结果
Union 并集(默认),所有 key 的结果都包含
Intersection 交集,只包含被所有 key 命中的资源

5.4 引用计数与释放

Addressables 用引用计数管理资源生命周期,这是最容易出错的地方。

规则:

  • 每 Load 一次,引用计数 +1
  • 每 Release 一次,引用计数 -1
  • 引用计数归零时,资源才真正从内存卸载
1
2
3
4
5
6
7
var h1 = Addressables.LoadAssetAsync<GameObject>("Player");  // 计数 = 1
var h2 = Addressables.LoadAssetAsync<GameObject>("Player");  // 计数 = 2

Addressables.Release(h1);   // 计数 = 1,资源仍在内存
Addressables.Release(h2);   // 计数 = 0,资源被卸载

// 如果忘记 Release,资源会一直留在内存中 —— 内存泄漏!

实例化的释放规则不同:

1
2
3
4
5
6
var go = await Addressables.InstantiateAsync("Enemy").Task;
// 用 ReleaseInstance 销毁实例并释放
Addressables.ReleaseInstance(go);

// 如果第二个参数 trackHandle 传 false,则 InstantiateAsync 不跟踪句柄,
// 此时用普通的 Destroy(go) 即可,但要自己确保资源加载句柄被释放

口诀:Load 配对 Release;InstantiateAsync 配对 ReleaseInstance。


六、资源组与打包策略

6.1 Group 的关键设置

在 Groups 窗口选中一个组,Inspector 里可配置:

设置 说明
Include In Build 是否参与构建(远程组本地测试时可能临时关掉)
Bundle Mode 一个组打成几个 Bundle
Compression 压缩方式(Uncompressed / LZ4 / LZMA)
Content Update Restriction 内容更新限制策略
Asset Load Mode 加载粒度(按需 / 整包)

6.2 Bundle Mode(打包方式)

模式 说明 适用
Pack Together 整个组打成一个 Bundle 资源少、总是一起用
Pack Separately 每个资源单独一个 Bundle 大资源、需要独立更新
Pack Together By Label 按标签分 Bundle 按用途分类

6.3 Compression(压缩方式)

方式 特点 场景
Uncompressed 不压缩,加载最快,包最大 本地频繁加载
LZ4 分块压缩,可随机读取,速度快 本地资源首选
LZMA 压缩率最高,需整包解压,加载慢 远程下载首选(省流量)

经验:本地组用 LZ4,远程组用 LZMA。远程包下载到本地缓存后会解压。

6.4 打包策略建议

  • 按使用场景分组:把总是一起出现的资源放同一组(如"登录界面"“新手村”)
  • 控制粒度:Bundle 太多会导致 IO 次数多、Catalog 大;太少会导致加载冗余、更新不灵活
  • 大资源拆开:视频、大图单独成包,避免牵连无关资源重新下载
  • 避免重复:多个 Bundle 依赖同一资源时,Addressables 会自动把公共依赖提到共享 Bundle,但要检查 Check Duplicate Bundle Dependencies

七、远程资源与热更新

这是 Addressables 最有价值的部分,也是游戏上线后运营的核心能力。

7.1 Profile 与路径变量

打开 Window > Asset Management > Addressables > Profiles,可以看到路径变量。

内置变量(不同版本略有差异):

变量 含义
Local.BuildPath 本地资源的构建输出目录
Local.LoadPath 运行时本地资源的读取路径
Remote.BuildPath 远程资源的构建输出目录
Remote.LoadPath 运行时远程资源的下载地址(如 CDN)

默认值类似:

1
2
3
4
Local.BuildPath  = [UnityEngine.AddressableAssets.Addressables.BuildPath]/[BuildTarget]
Local.LoadPath   = {UnityEngine.AddressableAssets.Addressables.RuntimePath}/[BuildTarget]
Remote.BuildPath = ServerData/[BuildTarget]
Remote.LoadPath  = http://localhost/[BuildTarget]

7.2 把组切到远程

选中 Group,在 Content Packing & Loading > Build & Load Paths 里把:

  • Build Path 设为 Remote
  • Load Path 设为 Remote

这样该组就会打包到远程目录,运行时从 Remote.LoadPath 下载。

7.3 构建与部署

  1. Window > Asset Management > Addressables > Groups
  2. 菜单 Build > New Build > Default Build Script
  3. 产物在 ServerData/[BuildTarget]/ 下:
1
2
3
4
5
ServerData/StandaloneWindows64/
├── catalog.json                    # 目录清单
├── catalog_xxx.hash                # 目录哈希
├── catalog_xxx.bundle
└── *.bundle                        # 资源包
  1. 把 ServerData 里的内容上传到 CDN / 服务器,与 Remote.LoadPath 对应。

7.4 运行时检查与下载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

async void CheckUpdate()
{
    // 1. 检查 Catalog 是否有更新
    AsyncOperationHandle<List<string>> checkHandle =
        Addressables.CheckForCatalogUpdates();
    await checkHandle.Task;
    List<string> catalogs = checkHandle.Result;

    if (catalogs != null && catalogs.Count > 0)
    {
        // 2. 更新本地 Catalog
        AsyncOperationHandle<List<IResourceLocator>> updateHandle =
            Addressables.UpdateCatalogs(catalogs);
        await updateHandle.Task;
    }

    // 3. 查询某个资源需要下载多少字节
    long size = await Addressables.GetDownloadSizeAsync("Chapter_02").Task;
    Debug.Log($"需要下载 {size / 1024f / 1024f:F2} MB");

    if (size > 0)
    {
        // 4. 下载依赖
        var downloadHandle =
            Addressables.DownloadDependenciesAsync("Chapter_02");
        while (!downloadHandle.IsDone)
        {
            Debug.Log($"下载进度:{downloadHandle.PercentComplete:P0}");
            await System.Threading.Tasks.Task.Yield();
        }
        Addressables.Release(downloadHandle);
    }
}

7.5 热更新的原理与限制

  • 本地 Catalog 更新:游戏自带的 Catalog 版本旧了,启动时从服务器拉最新 Catalog,就能知道新资源在哪
  • 资源内容更新:只需上传改动过的 Bundle + 新 Catalog,玩家按需下载
  • 不能更新代码:Addressables 只能热更资源。逻辑代码的热更需要 Lua(xLua/ToLua)、ILRuntime 等方案配合

Content Update 工作流(增量更新):

  1. 首次发布:Build > New Build
  2. 内容改动后:先把现有构建结果设为基线 Build > Update a Previous Build
  3. 选择上一次构建的 addressables_content_state.bin
  4. 生成增量 Bundle

这个 addressables_content_state.bin 文件必须妥善保存,每次增量更新都基于它,丢了就只能全量重发。


八、依赖与引用

8.1 自动依赖分析

Addressables 会自动分析资源依赖,并把公共依赖提取到共享 Bundle。

例如 Player.prefab 引用了 Weapon.mat,若把 Player.prefab 设为 Addressable,Weapon.mat 会被自动包含进依赖 Bundle。

重要副作用:如果你只想加载 Weapon.mat,它可能因为依赖关系被连带打包。此时应显式把 Weapon.mat 也标记为 Addressable,并设置好地址,避免加载 Player 才能拿到材质。

8.2 用 AssetReference 替代硬引用

问题:如果某个 MonoBehaviour 脚本里直接 public GameObject prefab; 并用 Inspector 拖拽,那么该资源会被硬引用,无法动态加载,也会被强制打进包。

解决:用 AssetReference。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;
using UnityEngine.AddressableAssets;

public class Spawner : MonoBehaviour
{
    // 在 Inspector 中通过 AssetReference 选择一个 Addressable 资源
    public AssetReferenceGameObject enemyRef;

    async void Spawn()
    {
        // 通过引用加载,而非硬引用
        AsyncOperationHandle<GameObject> handle = enemyRef.InstantiateAsync();
        await handle.Task;
        // ...
    }

    void OnDestroy()
    {
        enemyRef.ReleaseInstance(); // 释放该引用实例化的对象
    }
}

AssetReference 家族:

类型 用途
AssetReference 通用
AssetReferenceGameObject GameObject/Prefab
AssetReferenceSprite Sprite
AssetReferenceAtlasedSprite 图集 Sprite
AssetReferenceT<T> 泛型自定义

好处:

  • 不产生硬引用,资源可动态加载
  • Inspector 中只能选择 Addressable 资源,避免误拖
  • 可序列化,支持 ID 引用,资源改名不影响

九、标签(Label)的使用

标签用于给资源分类,便于批量加载和更新管理。

添加标签:在 Groups 窗口中选中资源条目,Inspector 里 Labels 一栏添加,如 character、ui_icon。

按标签加载:

1
2
3
4
5
var handle = Addressables.LoadAssetsAsync<GameObject>("character", null);
await handle.Task;
foreach (var go in handle.Result)
    Instantiate(go);
Addressables.Release(handle);

典型用途:

  • 一次性加载某类资源(所有语言文本、所有 UI 图标)
  • 按标签打包(Pack Together By Label)
  • 资源更新时按标签定位

标签是字符串,没有命名空间强制约束,建议团队约定命名规范,如 ui_、char_、audio_ 前缀。


十、场景加载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;

// 加载场景(单场景模式,替换当前场景)
AsyncOperationHandle<SceneInstance> handle =
    Addressables.LoadSceneAsync("Level_01", LoadSceneMode.Single);
await handle.Task;

// 叠加加载(Additive)
var addHandle = Addressables.LoadSceneAsync("UI_HUD", LoadSceneMode.Additive);
await addHandle.Task;

// 卸载场景
await Addressables.UnloadSceneAsync(addHandle).Task;

要点:

  • 场景也要先标记为 Addressable 才能加载
  • 单场景模式会卸载当前场景再加载新场景
  • 叠加模式适合把 UI、加载页作为独立场景
  • 卸载用 UnloadSceneAsync,不要用普通 SceneManager.UnloadSceneAsync 混用,否则引用计数对不上

十一、常见问题与陷阱

11.1 内存泄漏

症状:反复进出某界面,内存持续上涨。

原因:加载了资源却忘记 Release / ReleaseInstance。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 错误:加载后没有释放
void ShowIcon()
{
    Addressables.LoadAssetAsync<Sprite>("Icon").Completed += op =>
        img.sprite = op.Result;
}

// 正确:保存句柄,关闭界面时释放
AsyncOperationHandle<Sprite> _iconHandle;

async void ShowIcon()
{
    _iconHandle = Addressables.LoadAssetAsync<Sprite>("Icon");
    await _iconHandle.Task;
    img.sprite = _iconHandle.Result;
}

void OnDestroy()
{
    if (_iconHandle.IsValid())
        Addressables.Release(_iconHandle);
}

11.2 编辑器能跑,真机报错

原因:Play Mode Script 选了 Use Asset Database,编辑器不打包;真机需要真实 Bundle。

解决:发布前务必用 Build > New Build 构建,并用 Use Existing Build 验证一次。

11.3 地址拼错

地址是字符串,拼错只有运行时才报 Failed。

缓解:

  • 地址集中定义为常量或配置
  • 使用 AssetReference 代替字符串
  • 统一命名规范
1
2
3
4
5
public static class Addresses
{
    public const string Player = "Player";
    public const string LoginUI = "UI/Login";
}

11.4 资源被重复打包 / 包体异常大

排查:Window > Asset Management > Addressables > Analyze,运行以下分析:

  • Check Duplicate Bundle Dependencies:检查重复依赖
  • Check Bundle Duplicate Assets:检查 Bundle 中的重复资源

11.5 Destroy 与 ReleaseInstance 混用

InstantiateAsync 创建的对象若用 Destroy 销毁,引用计数不归零 → 泄漏。反之,若 trackHandle: false,则只能用 Destroy。要么统一用 Addressables 管理,要么统一手动管理,不要混。

11.6 远程资源加载失败

常见原因:Remote.LoadPath 地址错误、服务器未开启跨域(CORS)、HTTPS 证书问题、Catalog 未更新。先在浏览器直接访问 .bundle 地址验证。


十二、性能与最佳实践

加载性能

  • 优先异步,避免 WaitForCompletion 卡主线程
  • 预加载:在加载界面提前 DownloadDependenciesAsync / LoadAssetAsync
  • 使用 DownloadDependenciesAsync 提前把远程包下到本地缓存

内存

  • 严格的 Load/Release 配对,界面关闭时清理
  • 用 Profiler 的 Addressables 模块查看引用计数
  • 大资源用完立即释放,不要长时间驻留

打包

  • 本地 LZ4,远程 LZMA
  • 按使用场景分组,避免大杂烩组
  • 定期跑 Analyze 检查重复依赖
  • 固定 Catalog 命名,做好版本管理

工程规范

  • 地址用常量管理,不散落魔法字符串
  • 优先 AssetReference 而非硬引用
  • 封装一个加载管理器,统一处理句柄生命周期

一个简单的加载管理器示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public static class ResLoader
{
    // 记录句柄,统一在合适时机释放
    static readonly Dictionary<string, AsyncOperationHandle> _handles = new();

    public static async System.Threading.Tasks.Task<T> LoadAsync<T>(string address)
    {
        if (_handles.TryGetValue(address, out var cached) && cached.IsValid())
            return (T)cached.Result;

        var handle = Addressables.LoadAssetAsync<T>(address);
        await handle.Task;

        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            _handles[address] = handle;
            return handle.Result;
        }

        Debug.LogError($"加载失败:{address}");
        Addressables.Release(handle);
        return default;
    }

    public static void Release(string address)
    {
        if (_handles.TryGetValue(address, out var handle))
        {
            if (handle.IsValid()) Addressables.Release(handle);
            _handles.Remove(address);
        }
    }

    public static void ReleaseAll()
    {
        foreach (var kv in _handles)
            if (kv.Value.IsValid()) Addressables.Release(kv.Value);
        _handles.Clear();
    }
}

十三、完整实战流程

从零到跑通一个加载的完整步骤:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
1. 安装 Addressables 包
        │
2. Window > Asset Management > Addressables > Groups
        │
3. 创建分组:Local / Remote
        │
4. 拖入资源,设置语义化地址(如 "Player")
        │
5. 配置 Play Mode Script = Use Asset Database(开发期)
        │
6. 代码加载:
     Addressables.InstantiateAsync("Player", parent);
        │
7. 用完 ReleaseInstance / Release
        │
8. 发布前 Build > New Build,切 Use Existing Build 验证
        │
9. 上传 ServerData 到 CDN,配置 Remote.LoadPath
        │
10. 运行时 CheckForCatalogUpdates → DownloadDependencies → 加载

最小可运行示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class AddressablesDemo : MonoBehaviour
{
    public string address = "Player";
    GameObject _instance;
    AsyncOperationHandle<GameObject> _handle;

    async void Start()
    {
        // 加载并实例化
        _handle = Addressables.InstantiateAsync(address, transform);
        await _handle.Task;

        if (_handle.Status == AsyncOperationStatus.Succeeded)
        {
            _instance = _handle.Result;
            Debug.Log("加载成功:" + address);
        }
        else
        {
            Debug.LogError($"加载失败:{address},{_handle.OperationException}");
        }
    }

    void OnDestroy()
    {
        // 销毁实例并释放
        if (_instance != null)
            Addressables.ReleaseInstance(_instance);
    }
}

十四、面试高频问答

14.1 Addressables 和 AssetBundle 的关系?

Addressables 是构建在 AssetBundle 之上的更高层抽象。底层仍然打包成 Bundle,但 Addressables 帮你自动完成依赖分析、引用计数、Catalog 管理、远程下载、版本更新等。你可以把它理解成"官方维护的、带引用计数的 AssetBundle 管理器”。

14.2 Addressables 和 Resources 的区别?

Resources Addressables
打包 全部无条件打进包 按组按需打包
热更新 不支持 支持
卸载 无引用计数 引用计数精确卸载
路径 固定文件夹 任意地址,可远程
官方推荐 否 是

14.3 为什么加载了资源一定要 Release?

Addressables 用引用计数管理内存。Load/Instantiate 使计数 +1,Release/ReleaseInstance 使计数 -1,只有归零时资源才卸载。不 Release 会导致计数永远大于 0,资源常驻内存,即内存泄漏。

14.4 Bundle Mode 有哪几种?

  • Pack Together:整组一个 Bundle
  • Pack Separately:每个资源一个 Bundle
  • Pack Together By Label:按标签分组打包

14.5 三种压缩方式的区别?

  • Uncompressed:不压缩,加载最快,体积最大
  • LZ4:分块压缩,支持随机读取,速度与体积平衡,适合本地
  • LZMA:压缩率最高,需整包解压,加载慢但省流量,适合远程下载

14.6 Play Mode Script 三种模式的区别?

  • Use Asset Database:直接从编辑器资源数据库加载,不打包,最快
  • Simulate Groups:模拟打包,仍从数据库读,用于测试打包布局
  • Use Existing Build:使用真实构建的 Bundle,最接近真机

14.7 如何实现资源热更新?

  1. 启动时 CheckForCatalogUpdates 检查 Catalog 更新
  2. UpdateCatalogs 拉取新 Catalog
  3. GetDownloadSizeAsync 查询下载大小
  4. DownloadDependenciesAsync 下载依赖
  5. 正常加载资源

资源改动后只需上传增量 Bundle + 新 Catalog。注意:Addressables 只能热更资源,代码热更需要 Lua / ILRuntime 等。

14.8 什么是 Catalog?

Catalog 是 Addressables 生成的清单文件(catalog.json),记录了所有地址到资源、依赖关系、Bundle 位置和哈希的映射。运行时通过它把"地址"解析为"具体要加载的 Bundle 和资源”。它是远程更新能否生效的关键。

14.9 为什么建议用 AssetReference 而不是直接引用?

直接用 public GameObject 会产生硬引用,导致资源被强制打进包且无法动态卸载。AssetReference 通过 ID 序列化引用,不产生硬引用,资源可动态加载/释放,且只能选择 Addressable 资源,更安全。

14.10 如何避免 Bundle 重复依赖?

  • 用 Analyze > Check Duplicate Bundle Dependencies 检测
  • 把公共依赖单独成组
  • 合理划分 Group,避免多个组独立引用同一大资源
  • 关注 Include in Build 与依赖提取设置

十五、学习路线建议

阶段 目标 重点内容
入门 能加载一个 Prefab 安装、标记地址、InstantiateAsync、ReleaseInstance
熟练 掌握异步与内存 句柄、引用计数、批量加载、标签
进阶 远程与分组 Profile、Group 配置、Compression、Build
高级 热更新与工程化 Catalog 更新、增量 Build、加载管理器封装
实战 上线运营 CDN 部署、版本管理、性能分析、内存泄漏排查

推荐练习:

  1. 把一个 Resources.Load 的项目改成 Addressables
  2. 搭建本地远程服务器(http-server 或 Python http.server)测试远程加载
  3. 做一个带"检查更新 → 显示下载大小 → 下载进度 → 进入游戏"的启动流程
  4. 用 Unity Profiler 观察 Release 前后的内存变化

本文系统梳理了 Unity Addressables 的核心概念与实际使用,从 Resources/AssetBundle 的痛点讲起,覆盖加载 API、资源组、远程热更新、依赖管理与最佳实践。示例基于 Unity 2021 LTS + Addressables 1.21 编写,不同版本 API 基本兼容。学习时建议边看边在项目中动手实验,尤其要亲手验证"引用计数与释放"这一最容易踩坑的部分。

不嘻嘻
使用 Hugo 构建
主题 Stack 由 Jimmy 设计