一旦你将 Addressable assets 组织到 groups 并构建到 AssetBundles 中,就需要在运行时加载、实例化和释放它们。
Addressables 使用引用计数系统来确保 assets 只在需要时保留在内存中。
Addressables 初始化
Addressables 系统在运行时第一次加载 Addressable 或进行其他 Addressable API 调用时会初始化自己。调用 Addressables.InitializeAsync
可以更早地初始化 Addressables。如果已经完成初始化,则此方法不执行任何操作。
初始化任务
初始化操作执行以下任务:
- 设置
[ResourceManager](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/api/UnityEngine.ResourceManagement.ResourceManager.html)
和[ResourceLocators](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/api/UnityEngine.AddressableAssets.ResourceLocators.html)
。 - 加载 Addressables 从
StreamingAssets
创建的配置数据。 - 执行任何 initialization object 操作。
- 加载
content catalog
。默认情况下,Addressables 首先检查content catalog
是否有更新,如果有,则下载新的 catalog。
以下 Addressables 设置可以更改初始化行为:
- Only update catalogs manually:Addressables 不会自动检查更新的 catalog。有关手动更新 catalogs 的信息,请参见 Updating catalogs。
- Build Remote Catalog:没有 remote catalog,Addressables 不会尝试加载 remote 内容。
- Custom certificate handler:如果需要访问 remote asset hosting 服务,请指定一个自定义证书处理程序。
- Initialization object list:将
[IObjectInitializationDataProvider](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/api/UnityEngine.ResourceManagement.Util.IObjectInitializationDataProvider.html)
ScriptableObject 添加到你的应用程序中,Addressables 在初始化操作期间调用它。
在初始化操作开始之前设置以下运行时属性:
- Custom URL transform function。
[ResourceManager exception handler](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/api/UnityEngine.ResourceManagement.ResourceManager.ExceptionHandler.html#UnityEngine_ResourceManagement_ResourceManager_ExceptionHandler)
。- 用于任何 Profile variables 中自定义运行时占位符(placeholders)的静态属性。
Initialization Objects
你可以将对象附加到 Addressable Assets 设置,并在运行时将它们传递到初始化过程中。例如,你可以创建一个 [CacheInitializationSettings](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/api/UnityEditor.AddressableAssets.Settings.CacheInitializationSettings.html)
对象来在运行时初始化 Unity 的 [Cache](https://docs.unity3d.com/2023.1/Documentation/ScriptReference/Cache.html)
设置。
要创建自己的初始化对象类型,请创建一个实现 [IObjectInitializationDataProvider](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/api/UnityEngine.ResourceManagement.Util.IObjectInitializationDataProvider.html)
接口的 ScriptableObject。使用此对象创建 [ObjectInitializationData](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/api/UnityEngine.ResourceManagement.Util.ObjectInitializationData.html)
asset,Addressables 在运行时数据中包含它。
Cache Initialization Objects
使用[CacheInitializationSettings](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/api/UnityEditor.AddressableAssets.Settings.CacheInitializationSettings.html)
对象在运行时初始化 Unity 的[Cache](https://docs.unity3d.com/2023.1/Documentation/ScriptReference/Cache.html)
设置。
要指定 Addressables 系统使用的 cache 初始化设置:
-
创建一个
CacheInitializationSettings
asset(菜单:Assets > Addressables > Initialization > Cache Initialization Settings
)。 -
在 Project 面板中选择新 asset 文件以在 Inspector 中查看设置。
-
根据需要调整设置。
-
打开 Addressables 设置 Inspector(菜单:
Window > Asset Management > Addressables > Settings
)。 -
在 Inspector 的
Initialization Objects
部分中,单击+
按钮将新对象添加到列表中。 -
在文件对话框中选择
CacheInitializationSettings
asset 并单击Open
。 -
缓存设置对象已添加到列表中。
当 Addressables 在运行时初始化时,它将这些设置应用于默认的 Unity Cache
。设置适用于默认缓存(default cache)中的所有 AssetBundles,而不仅仅是 Addressables 系统下载的那些。有关 Unity cache 系统的更多信息,请参见 Caching 。
内存管理概述
Addressables 系统通过对其加载的每个项进行引用计数来管理加载 assets 和 bundles 所使用的内存。
当 Unity 加载一个 Addressable 时,系统会增加引用计数。当 Unity 释放该 asset 时,系统会减少引用计数。当 Addressable 的引用计数归零时,它可以被卸载。当你显式加载一个 Addressable asset 时,也必须在使用完后释放该 asset。
内存泄漏
为了避免内存泄漏,即在不再需要时 assets 仍然保留在内存中,应将每次加载方法的调用与释放方法的调用进行匹配。你可以通过对 asset 实例本身的引用或通过原始加载操作返回的结果 handle 来释放 asset 。
但是,Unity 并不会立即从内存中卸载已释放的 assets ,因为只有当它们所属的 AssetBundle 也被卸载时,内存才会被释放。
AssetBundles 有自己的引用计数,系统将它们视为包含 assets 依赖项的 Addressables。当你从 bundle 加载一个 asset 时,bundle 的引用计数会增加;当你释放该 asset 时,bundle 的引用计数会减少。当 bundle 的引用计数归零时,这意味着 bundle 中包含的所有 assets 都不再使用。此时,Unity 会从内存中卸载该 bundle 及其包含的所有 assets。
使用 Profiler 模块来监控已加载的内容。该模块会显示 assets 及其依赖项何时加载和卸载。
内存清理(Memory Clearance)
如果一个 asset 不再被引用,即 Profiler 模块中显示为已释放状态且文本被禁用,这并不意味着 Unity 已经卸载了该 asset。常见的适用场景包括一个 AssetBundle 中的多个 assets。例如:
- 你在一个 AssetBundle(stuff)中有三个 assets(tree、tank 和 cow)。
- 当加载 tree 时,Profiler 显示 tree 和 stuff 各有一个引用计数。
- 之后,当加载 tank 时,Profiler 显示 tree 和 tank 各有一个引用计数,stuff AssetBundle 有两个引用计数。
- 如果你释放 tree,它的引用计数变为零,蓝色条消失。
在这个例子中,tree asset 此时并未被卸载。你可以加载一个 AssetBundle 或其部分内容,但不能卸载 AssetBundle 的部分内容。直到整个 AssetBundle 被卸载,stuff 中的 assets 才会被卸载。
避免 asset churn
如果你释放了一个是 AssetBundle 中最后一个项目的对象,并立即重新加载该 asset 或 bundle 中的另一个 asset,则会发生 asset churn。
例如,如果你有两个材质(boat 和 plane)共享一个纹理 cammo,cammo 位于其自己的 AssetBundle 中。Level 1 使用 boat,Level 2 使用 plane。当你退出 Level 1 时,Unity 释放 boat,并立即加载 plane。当 Unity 释放 boat 时,Addressables 会卸载纹理 cammo。然后,当 Unity 加载 plane 时,Addressables 会立即重新加载 cammo。
你可以使用 Profiler 模块帮助检测 asset churn,通过监控 asset 的加载和卸载来实现。
AssetBundle Memory Overhead
当你加载一个 AssetBundle 时,Unity 会分配内存来存储该 bundle 的内部数据以及该 bundle 中包含的 assets。加载的 AssetBundle 的主要内部数据类型包括:
- 加载缓存:存储最近访问的 AssetBundle 文件页面。使用
[AssetBundle.memoryBudgetKB](https://docs.unity3d.com/2023.1/Documentation/ScriptReference/AssetBundle-memoryBudgetKB.html)
控制其大小。 - TypeTrees:定义对象的序列化布局。
- Table of contents:列出 bundle 中的 assets 。
- Preload table:列出每个 asset 的依赖项。
在组织 Addressable 组和 AssetBundles 时,你需要在创建和加载的 AssetBundles 数量和大小之间进行权衡。较少且较大的 bundles 可以最小化总内存使用量。然而,使用多个小 bundles 可以最小化峰值内存使用量(peak memory usage),因为 Unity 可以更轻松地卸载 assets 和 AssetBundles 。
磁盘上的 AssetBundle 大小与运行时大小并不相同。然而,你可以使用磁盘大小作为构建中 AssetBundles 内存开销的指南。你可以从 Build Layout Report 中获取 bundle 大小和其他信息来帮助分析 AssetBundles。
TypeTrees
TypeTree 描述了项目中数据类型的字段布局(field layout)。
每个序列化文件在 AssetBundle 中都有一个TypeTree
,用于文件中的每种对象类型。你可以使用 TypeTree 信息加载与序列化方式略有不同的对象。TypeTree 信息不会在 AssetBundles 之间共享,每个 bundle 都有其包含对象的完整 TypeTrees 集合。
Unity 在加载 AssetBundle 时会加载所有 TypeTrees,并在 AssetBundle 的生命周期内将其保存在内存中。与 TypeTrees 相关的内存开销与序列化文件中唯一类型的数量和这些类型的复杂性成正比。
减少 TypeTree 内存
你可以通过以下方式减少 AssetBundle TypeTrees 的内存需求:
- 将相同类型的 assets 放在同一个 bundle 中。
- 禁用 TypeTrees,这会从 bundle 中排除 TypeTree 信息,使 AssetBundles 更小。然而,没有 TypeTree 信息,当你用更新版本的 Unity 加载旧的 bundles 或在项目中进行脚本更改时,可能会出现序列化错误或未定义行为。
- 使用简单的数据类型来减少 TypeTree 的复杂性。
为了测试 TypeTrees 对 AssetBundles 大小的影响,构建启用和禁用 TypeTrees 的 bundles 并比较它们的大小。使用 BuildAssetBundleOptions.DisableWriteTypeTree
在你的 AssetBundles 中禁用 TypeTrees。
一些平台需要 TypeTrees 并忽略
DisableWriteTypeTree
设置。此外,并非所有平台都支持 TypeTrees。
如果在项目中禁用 TypeTrees,请在构建新的 player 之前始终重新构建本地 Addressable 组。如果你的项目通过远程分发内容,请使用与生成 player 相同版本(包括补丁号)的 Unity 并且不要进行哪怕小的代码更改。如果你使用多个 player 版本、更新和 Unity 版本,禁用 TypeTrees 带来的内存节省可能不值得麻烦。
Table of contents
Table of contents 是 bundle 中的一个 map ,你可以使用它按名称查找每个显式包含的 asset。它的大小与 assets 的数量和字符串名称的长度成线性比例。
Table of contents 数据的大小基于 assets 的总数量。为了最小化用于保存目录数据的内存,请最小化同时加载的 AssetBundles 数量。
预加载表(Preload table)
Preload table 是一个列表,列出了一个 asset 引用的所有其他对象。当你从 AssetBundle 加载一个 asset 时,Unity 使用预加载表来加载这些引用的对象。
例如,一个 prefab 对其每个组件以及它可能引用的任何其他 assets(如材质或纹理)都有一个预加载条目。每个预加载条目为 64 位,可以引用其他 AssetBundles 中的对象。
当一个 asset 引用另一个 asset,而另一个 asset 又引用其他 assets 时,预加载表可能会变大,因为它包含加载这两个 assets 所需的条目。如果两个 assets 都引用第三个 asset,那么两个 assets 的预加载表都包含加载第三个 asset 的条目,无论引用的 asset 是 Addressable 还是在同一个 AssetBundle 中。
例如,一个项目有两个 assets(PrefabA 和 PrefabB)在一个 AssetBundle 中,它们都引用第三个 prefab(PrefabC),PrefabC 很大并且有几个组件和对其他 assets 的引用。这个 AssetBundle 有两个预加载表,一个是 PrefabA 的,一个是 PrefabB 的。这些表包含各自 prefab 的所有对象条目,以及 PrefabC 和 PrefabC 引用的任何对象的条目。加载 PrefabC 所需的信息会在 PrefabA 和 PrefabB 中重复出现。这种情况发生在 PrefabC 是否显式添加到一个 AssetBundle 中。
根据你组织项目中 assets 的方式,AssetBundles 中的预加载表可能会很大并包含许多重复的条目。如果你决定预加载表带来的内存开销是个问题,你可以重新结构化项目中的可加载 assets,使它们具有较少的复杂加载依赖项。
加载 AssetBundle 依赖项
加载一个 Addressable asset 也会加载包含其依赖项的所有 AssetBundles。当一个 bundle 中的 asset 引用另一个 bundle 中的 asset 时,就会发生 AssetBundle 依赖。例如,当一个材质引用一个纹理时。有关更多信息,请参阅 Asset and AssetBundle dependencies 。
Addressables 在 bundle 级别计算 bundle 之间的依赖关系。如果一个 asset 引用另一个 bundle 中的对象,那么整个 bundle 就依赖于那个 bundle。这意味着即使你加载的第一个 bundle 中的 asset 没有自己的依赖项,第二个 AssetBundle 仍会被加载到内存中。
例如,BundleA
包含 Addressable assets RootAsset1
和 RootAsset2
。RootAsset2
引用 DependencyAsset3
,它在 BundleB
中。即使 RootAsset1
没有引用 BundleB
,BundleB
仍然是 RootAsset1
的依赖,因为 RootAsset1
在 BundleA
中,而 BundleA
引用了 BundleB
。
为了避免加载超过所需数量的 bundles,请尽量简化 AssetBundles 之间的依赖关系。你可以使用 Build Layout Report 检查依赖关系。
管理运行时的 Catalogs
默认情况下,Addressables 系统在运行时自动管理 catalog 。如果你使用了 Remote Catalog 构建应用程序,Addressables 系统会自动检查新目录,下载新版本并加载到内存中。
你可以在运行时加载额外的目录。例如,你可以加载由另一个兼容项目生成的目录,以加载该项目生成的 Addressable 资产。有关更多信息,请参阅 Loading content from multiple 。
如果你想更改 Addressables 系统的默认目录更新行为,可以禁用自动检查,并手动检查更新。有关更多信息,请参阅 Updating catalogs 。
加载额外的目录
使用 Addressables.LoadContentCatalogAsync
加载额外的内容目录,可以从托管服务或本地文件系统加载。你需要提供要加载的目录的位置。目录加载操作完成后,你可以使用新目录中的键调用任何 Addressables 加载函数。
如果你在与目录相同的 URL 上提供目录哈希文件,Addressables 会缓存二级目录。当客户端应用程序加载目录时,只有哈希更改时才会下载新版本的目录。
哈希文件需要与目录位于同一位置并具有相同的名称。路径的唯一区别应该是扩展名。
LoadContentCatalogAsync
带有一个参数 autoReleaseHandle
。为了使系统下载新的 Remote Catalog ,指向你要加载的目录的任何先前调用LoadContentCatalogAsync
的操作需要被释放。否则,系统会从操作缓存中获取内容目录加载操作。如果从缓存中获取到操作,则不会下载新的 Remote Catalog 。设置autoReleaseHandle
为true
可以确保操作在完成后不会保留在操作缓存中。
一旦加载目录,就无法卸载它。然而,你可以更新已加载的目录。在更新目录之前,必须释放加载目录的操作句柄。有关更多信息,请参阅 Updating catalogs 。
通常,在加载目录后没有理由保留操作句柄。你可以在加载目录时通过将 autoReleaseHandle
参数设置为 true 来自动释放它,如下例所示:
public IEnumerator Start()
{// 加载目录并自动释放操作句柄。AsyncOperationHandle<IResourceLocator> handle= Addressables.LoadContentCatalogAsync("path_to_secondary_catalog", true);yield return handle;// ...
}
在 Addressables 设置中使用
Catalog Download Timeout
属性指定下载目录的超时时间。
更新目录
如果提供了目录哈希文件,Addressables 会在加载目录时检查哈希,以确定提供的 URL 版本是否比缓存的目录版本更新。你可以禁用默认目录检查,并在需要更新目录时调用 Addressables.UpdateCatalogs
方法。如果你使用 LoadContentCatalogAsync
手动加载目录,则在更新目录之前必须释放操作句柄。
调用 UpdateCatalog
方法时,Unity 会阻止所有其他 Addressable 请求,直到操作完成。你可以在操作完成后立即释放 UpdateCatalog
返回的操作句柄,或者将 autoRelease
参数设置为true
。
如果在不提供目录列表的情况下调用UpdateCatalog
,Addressables 会检查所有已加载的目录是否有更新。
IEnumerator UpdateCatalogs()
{AsyncOperationHandle<List<IResourceLocator>> updateHandle= Addressables.UpdateCatalogs();yield return updateHandle;updateHandle.Release();
}
你也可以直接调用Addressables.CheckForCatalogUpdates
获取有更新的目录列表,然后执行更新:
IEnumerator CheckCatalogs()
{List<string> catalogsToUpdate = new List<string>();AsyncOperationHandle<List<string>> checkForUpdateHandle= Addressables.CheckForCatalogUpdates();checkForUpdateHandle.Completed += op => { catalogsToUpdate.AddRange(op.Result); };yield return checkForUpdateHandle;if (catalogsToUpdate.Count > 0){AsyncOperationHandle<List<IResourceLocator>> updateHandle= Addressables.UpdateCatalogs(catalogsToUpdate);yield return updateHandle;updateHandle.Release();}checkForUpdateHandle.Release();
}
运行时获取地址
默认情况下,Addressables 使用你分配给资源的地址作为其[IResourceLocation](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/api/UnityEngine.ResourceManagement.ResourceLocations.IResourceLocation.html)
实例的[PrimaryKey](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/api/UnityEngine.ResourceManagement.ResourceLocations.IResourceLocation.PrimaryKey.html#UnityEngine_ResourceManagement_ResourceLocations_IResourceLocation_PrimaryKey)
值。
如果禁用了资源所属的 Addressables 组的[Include Addresses in Catalog](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/manual/ContentPackingAndLoadingSchema.html)
选项,PrimaryKey
可以是 GUID、Label 或空字符串。如果你想获取使用 AssetReference
或标签加载的资源地址,可以加载资源的位置,如Load assets by location 所述。然后,你可以使用 IResourceLocation
实例来访问 PrimaryKey
值并加载资源。
以下示例获取分配给名为 MyRef1
的 [AssetReference](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/api/UnityEngine.AddressableAssets.AssetReference.html)
对象的资源地址:
var opHandle = Addressables.LoadResourceLocationsAsync(MyRef1);
yield return opHandle;if (opHandle.Status == AsyncOperationStatus.Succeeded &&opHandle.Result != null &&opHandle.Result.Count > 0)
{Debug.Log("address is: " + opHandle.Result[0].PrimaryKey);
}
Labels 通常引用多个资源。以下示例演示了如何加载多个预制件资源并使用其主键值将它们添加到字典中:
Dictionary<string, GameObject> _preloadedObjects= new Dictionary<string, GameObject>();private IEnumerator PreloadHazards()
{// 查找所有带有标签 "SpaceHazards" 的位置var loadResourceLocationsHandle= Addressables.LoadResourceLocationsAsync("SpaceHazards", typeof(GameObject));if (!loadResourceLocationsHandle.IsDone)yield return loadResourceLocationsHandle;// 开始加载每个位置的资源List<AsyncOperationHandle> opList = new List<AsyncOperationHandle>();foreach (IResourceLocation location in loadResourceLocationsHandle.Result){AsyncOperationHandle<GameObject> loadAssetHandle= Addressables.LoadAssetAsync<GameObject>(location);loadAssetHandle.Completed +=obj => { _preloadedObjects.Add(location.PrimaryKey, obj.Result); };opList.Add(loadAssetHandle);}// 创建一个 GroupOperation 一次性等待所有上述加载操作完成var groupOp = Addressables.ResourceManager.CreateGenericGroupOperation(opList);if (!groupOp.IsDone)yield return groupOp;loadResourceLocationsHandle.Release();// 查看我们的结果foreach (var item in _preloadedObjects){Debug.Log(item.Key + " - " + item.Value.name);}
}
此代码段展示了如何加载带有特定标签的资源,并将加载的资源添加到字典中以便后续使用。
修改事件(Modification events)
你可以使用修改事件向 Addressables 系统的某些部分发出信号,当某些数据被操作时,例如添加或移除 AddressableAssetGroup
或 AddressableAssetEntry
时。
修改事件作为 SetDirty
调用的一部分被触发。SetDirty
用于指示需要由 AssetDatabase
重新序列化的资源。作为 SetDirty
的一部分,可以触发两个修改事件回调:
public static event Action<AddressableAssetSettings, ModificationEvent, object> OnModificationGlobal
public Action<AddressableAssetSettings, ModificationEvent, object> OnModification { get; set; }
这些回调分别通过静态或实例访问器在[AddressableAssetSettings](https://docs.unity3d.com/Packages/com.unity.addressables@2.2/api/UnityEditor.AddressableAssets.Settings.AddressableAssetSettings.html)
中找到。
修改事件示例
AddressableAssetSettings.OnModificationGlobal += (settings, modificationEvent, data) =>
{if(modificationEvent == AddressableAssetSettings.ModificationEvent.EntryAdded){// 执行工作}
};
AddressableAssetSettingsDefaultObject.Settings.OnModification += (settings, modificationEvent, data) =>
{if (modificationEvent == AddressableAssetSettings.ModificationEvent.EntryAdded){// 执行工作}
};
修改事件传递一个通用对象,用于与事件关联的数据。下表列出了修改事件及其传递的数据类型。