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                │
          └─────────────────────────────────────────┘

三种方式对比:

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

二、核心概念

理解这几个名词,就理解了 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通用
AssetReferenceGameObjectGameObject/Prefab
AssetReferenceSpriteSprite
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 的区别?

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

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 设计