安装Addressable Assets
在Package Manager中安装
设置资源为Addressable
- 在资源的Inspector中
- 在Addressables Groups窗口中
指定资源的索引标识(address)
项目中一个资源的默认索引标识(address)通常都是它的所在位置路径(例如:Assets/images/myImage.png)。你可以在Addressables Groups窗口中,选中一个资源,然后在右键菜单中选择Change Address来修改。也可以在选中资源之后,再单击一次选中的资源触发修改。
使用Addressables时,系统会把编辑时和运行时的相关配置和数据都会保存到Assets/AddressableAssetsData目录下的文件中。所以你需要把这个目录和目录下的文件都提交到你的版本管理中。
构建资源
- 如果是使用Unity编辑器,首先打开Addressables Groups窗口,然后选择菜单Build > New Build > Default Build Script。
- 如果是使用API,可以在你的构建代码中调用AddressableAssetSettings.BuildPlayerContent()
使用Addressable Assets
通过address加载或实例化
在代码中加载资源,你需要申明命名空间UnityEngine.AddressableAssets
,然后调用如下方法:
// This loads the asset with the specified address
Addressables.LoadAssetAsync<GameObject>("AssetAddress");
// This instantiates the asset with the specified address into your Scene.
Addressables.InstantiateAsync("AssetAddress");
using System.Collections;
using System.Collections.Generic;
using UnityEngine.AddressableAssets;
using UnityEngine;
public class AddressablesExample : MonoBehaviour {
GameObject myGameObject;
...
Addressables.LoadAssetAsync<GameObject>("AssetAddress").Completed += OnLoadDone;
}
private void OnLoadDone(UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle<GameObject> obj)
{
// In a production environment, you should add exception handling to catch scenarios such as a null result.
myGameObject = obj.Result;
}
}
加载子资源
例如加载图集中的一张图片
// To load all sub-objects in an asset
Addressables.LoadAssetAsync<IList<Sprite>>("MySpriteSheetAddress");
// To load a single sub-object in an asset
Addressables.LoadAssetAsync<Sprite>("MySpriteSheetAddress[MySpriteName]");
使用AssetReference
可以在Component中定义AssetReference类型变量,然后可以选择一个Addressable资源绑定到这个变量上。
可以通过如下方法加载和实例化一个资源
AssetRefMember.LoadAssetAsync<GameObject>();
AssetRefMember.InstantiateAsync(pos, rot);
构建注意事项
StreamingAssets中的本地数据
Addressable Asset System在运行时,需要通过一些必要的文件才能知道资源加载时,需要从哪些资源文件中和如何加载。在构建Addressables时,这些文件会生成在StreamingAssets目录下。这个特殊的目录下所有的文件都会打包到APP中。构建资源时,系统会把这些文件缓存在Library目录下。然后在构建APP时,再把这些文件拷贝覆盖到StreamingAssets目录下,然后删除Library目录下的文件。所以当APP发布到不同的平台时,它们只会包含跟各自平台相关的数据。
除了特定的Addressables数据之外,每个资源组构建后的数据都会缓存在所设定平台特有的位置。为了确保有效,需要在Profile属性中设置构建路径(以[UnityEngine.AddressableAssets.Addressables.BuildPath]
开头)和加载路径(以{UnityEngine.AddressableAssets.Addressables.RuntimePath}
开头)。你可以在AddressableAssetSettings(默认该文件是在Assets/AddressableAssetsData目录下)的Profile属性中管理这些配置。
预下载
可以调用Addressables.DownloadDependenciesAsync()
,并指定address或label下载相关联的依赖资源。通常下载下来的是asset bundle文件。
在接口返回的AsyncOperationHandle
结构中包含了属性PercentComplete
,你可以监听这个值来显示下载进度。你也可以让你的APP一直等待,直到所有文件都下载完毕。
关于PercentComplete
PercentComplete
只是考虑了单个AsyncOperationHandle
一些内部处理情况。所以在某些情况下,它并不是线性的,也有可能是线性的。这都是由哪些快速操作或者那些同样需要长时间处理的操作的权重决定的。
例如,你想从远端下载一个资源,可能需要花费大量的时间,但是它的依赖资源已经存在于本地,所以你会发现在开始下载时,PercentComplete
就跳到了50%。这是因为资源在本地加载要比从远程加载快得多,而且这一步操作只需要两步就完成了。
如果你希望在下载前得到用户的同意,请使用Addressables.GetDownloadSize()
来返回从给定address或label下载内容所需的空间。注意,这将考虑到所有以前下载的仍然在Unity的资产包缓存中的包。
虽然预下载是有好处的,但是在某些情况下,或许不是一个好的选择,例如:
- 如果你的APP有大量的在线资源,但是你通常只期望用户只与其中一少部分有交互。
- 你的APP必须联网,而且你的资源都是非常小的包,最好是按需下载。
比起只是使用一个百分比数字来等待下载完成,最好是使用一个预加载功能来展示下载开始和下载进度。这需要你来实现一个加载或等待界面,显示在资源还未被加载完成的时候。
Content Catalog加载
Content Catalog存储了Addressables查找资源实际位置相关数据。默认下,Addressables会为本地的资源组生成catalog。如果在AddressableAssetSettings
中的Build Remote Catalogs启用了,系统会为远程的资源组生成一个额外的catalog。Addressables只会使用其中一个catalog,如果远程有一个catalog和本地的hash串有差异,那么远程的catalog会被下载,并缓存下来代替本地的catalog。
但是,如果可能的话,你期望加载指定的catalog,可能会有不同的原因需要加载额外的catalog,比如构建一个只有美术资源的项目提供给其他项目使用。
如果找到一个适合你的catalog,可以使用LoadContentCatalogAsync
来加载它。
只要知道catalog的位置,就可以使用LoadContentCatalogAsync
来加载。但是,这种做法并没有使用catalog缓存。所以需要注意的是,想要加载一个远程的catalog,那么就需要每次都通过WebRequest
来加载那个需要的catalog。
为了避免每次都需要下载远程的catalog,需要提供一个.hash
文件和你的catalog一起被加载。AAS可以利用它来缓存catalog。
注意:这个hash文件需要和catalog在同一个远程位置,然后文件名要和catalog一致。他们的路径只有文件后缀不一样。
还需要注意的是,你会注意到这个方法有一个参数autoReleaseHandle
。在下载一个新的远程catalog之前,任何之前调用LoadContentCatalogAsync
加载的catalog都必须要释放。否则,系统会选择这个已经被加载的catalog作为缓存。如果系统选择缓存的catalog,那么远程的catalog就不会被下载。如果autoReleaseHandle
设置为true,就可以确保加载的catalog完成后不会保持在缓存中。
Build Scripts
Build scripts是一种ScriptableObject资源,它实现了IDataBuilder接口。用户可以创建自己特定的脚本,然后再Inspector窗口中添加到AddressableAssetsSettings
的属性Build and Play Mode Scripts中。在Addressables Groups窗口(Window > Asset Management > Addressables > Groups)中通过选择Play Mode Script,并从下拉框中选择一项来应用build script。当前有三个脚本可以完全支持资源构建,还有三个用于编辑器下开发迭代的播放脚本。
Play mode scripts
Addressable Assets有三个构建脚本,可以创建不同运行数据来加速APP开发。
Use Asset Database(快速)
Use Asset Database模式(BuildScriptFastMode
)可以让你在完成游戏流程开发的同时也能快速运行游戏。它直接从asset database中加载资源,以便于游戏快速迭代,在这个过程中没有任何资源分析和asset bundle构建。
Simulate Groups(高级)
Simulate Groups模式(BuildScriptVirtualMode
)只会分析资源结构和依赖关系,但是不会创建asset bundle。资源是通过ResourceManager从asset database中加载而来,整个过程和从bundle中加载一样。所以可以在Addressables Event Viewer 窗口中查看资源的使用情况,以及bundle在游戏运行时的加载和卸载情况。
你可以通过Simulate Groups模式的模拟asset bundle加载来帮助你调整asset group,让整个资源结构在非常平衡状态下发布APP。
Use Existing Build(需要构建资源组)
Use Existing Build模式是非常接近发布版本加载方式。但这需要先执行资源构建。在你没有修改资源的前提下,这将是最快进入游戏的模式,因为在运行时不需要处理任何数据。所以在这个模式下运行APP前,你需要在Addressables Groups窗口(Window > Asset Management > Addressables > Groups)中,选择菜单 Build > New Build > Default Build Script或者使用APIAddressableAssetSettings.BuildPlayerContent()
来构建资源。
数据分析和调试
默认下Addressables Assets的日志只会记录警告和错误。你可以在Player settings窗口(Edit > Project Settings... > Player>)中,导航至Other Settings > Configuration,在属性Scripting Define Symbols中添加宏定义ADDRESSABLES_LOG_ALL
来开启所有详细信息日志。
你也可以在AddressableAssetSettings
的Inspector窗口中取消Log Runtime Exceptions勾选来关闭异常检测。当然,如果你有需要的话,你也可以实现 ResourceManager.ExceptionHandler属性来处理指定异常。但是这必须要在Addressables完成运行时初始化之后立即执行。
URL定制
有一些情况下,是需要为资源(一般是AssetBundle)定制一个URL。最常见的就是签名URL,另外就是动态主机路径。
下面的代码展示了组装一个查询URL:
//Implement a method to transform the internal ids of locations
string MyCustomTransform(IResourceLocation location)
{
if (location.ResourceType == typeof(IAssetBundleResource) && location.InternalId.StartsWith("http"))
return location.InternalId + "?customQueryTag=customQueryValue";
return location.InternalId;
}
//Override the Addressables transform method with your custom method. This can be set to null to revert to default behavior.
[RuntimeInitializeOnLoadMethod]
static void SetInternalIdTransform()
{
Addressables.InternalIdTransformFunc = MyCustomTransform;
}
注意:如果Addressables里打包了视频文件,并且需要在Android平台加载时,必须创建一个CacheInitializationSettings
对象,并禁用Compress Bundles属性。然后在AddressableAssetSettings
的Inspector窗口中,添加到Initialization objects属性里。
资源更新工作流
Unity推荐把游戏资源分成两部分:
Cannot Change Post Release
:静态资源,你永远都不期望更新。Can Change Post Release
:动态资源,你期望可以更新。
在这种结构下,静态资源随APP一起发布(或者安装后立即下载),它们分散在少数非常大的bundle中。动态资源放在网络上,通常都是很小的bundle,以减少每次更新数量。所以Addressable Assets System的目标之一就是,更容易使用和修改,并且不需要修改你的脚本。
当然,当需要修改静态部分资源的时候,而且还不想发布一个全新版本,Addressable Assets System同样可以满足这种需求。
工作原理
Addressables采用catalog把address和每一个资源映射起来,并指定了资源应该在何处以及如何被加载。为了使APP能够修改这些映射关系,你的发布APP必须要加载和识别一个在网络上的catalog副本。在AddressableAssetSettings
的Inspector窗口中开启Build Remote Catalog属性来开启这个功能。这样在构建时,会创建一个catalog的副本,并且可以通过指定的路径来加载它。这个路径在你的APP发布之后就不能再修改。而在之后把内容更新的过程中产生的新版本catalog(具有相同文件名)覆盖掉托管位置的旧文件即可。
在APP构建的时候会产生一个唯一的版本串,所以每一个版本的APP都知道它自己应该加载哪一份catalog。所以也可以在服务器上放置多个版本的catalog。APP在构建的时候会把必要数据都写入在addressables_content_state.bin文件中,它包含版本串,还有每一个被标记为静态资源的hash数据。默认下,这个文件在Assets/AddressableAssetsData/{platform}
目录下,{platform}
是你的目标发布平台。
addressables_content_state.bin文件包含了每一个静态资源组的hash和依赖数据。这些静态资源都会被打包到StreamingAssets
目录下。因为这种设定,那些大型远程资源组也会受益。在后面的步骤(准备内容更新)中,这些hash数据可以分辨出任何静态资源是否有修改,那些被更新过的资源将会被移动到别的资源组中。
唯一Bundle ID
Unity不能把两个具有同样名称的AssetBundle加载到内存中,这其实一定程度上限制运行时的资源更新。但是Addressables支持在初始化之后还可以更新catalog,所以即使资源已经被加载,也还是可以被更新。
要达到这个目的,有两种情况。一是在更新catalog之前把旧的Addressables数据卸载;另一种是确保被更新AssetBundle具有唯一的内部标识。这样就可以加载一个全新的AssetBundle,虽然旧的还在内存中。Addressables可以在AddressableAssetSettings
的Inspector窗口中开启Unique Bundle IDs属性来启用第二种情况。而它的缺点是需要重新构建和它有依赖关系的所有资源。这意味着,如果你修改了一个资源组里的material,默认情况下你只需要重新打包这一个资源组,但是当Unique Bundle IDs
启用后,所有依赖这个material的资源都需要重新构建。
准备更新资源
如果你修改了任何被标记为Cannot Change Post Release
资源,你需要执行Check for Content Update Restrictions,然后那些被修改的资源会从原来的静态组中移除,然后把它们放入到一个新的资源组中,步骤如下:
- 在Unity编辑器中打开Addressables Groups窗口(Window > Asset Management > Addressables > Groups)。
- 在Addressables Groups窗口中,点击Tools,在下拉菜单中选择Check for Content Update Restrictions。
- 在打开的Build Data File对话框中,选择之前构建产生的addressables_content_state.bin(默认这个文件在
Assets/AddressableAssetsData/{platform}
目录下)文件
bin文件里的数据会检测出当前哪些资源和依赖项已经被修改过。系统会自动把这些资源移动到一个新的资源组,以便资源更新构建。
注意:如果修改的资源都是非静态资源,那么以上步骤将什么都不处理。
重点:在执行更新准备之前,推荐首先在版本控制中分出一个新的分支,因为更新准备会修改和新建资源组。当你下次需要发布一个新的版本时,你仍然可以使用之前已经规划好的资源分组。
构建更新资源
构建内容更新请执行如下操作:
- 在Unity编辑器中打开Addressables Groups窗口(Window > Asset Management > Addressables > Groups)。
- 在Addressables Groups窗口中,点击Tools,在下拉菜单中选择Check for Content Update Restrictions。
- 在打开的Build Data File对话框中,选择之前构建产生的addressables_content_state.bin(默认这个文件在
Assets/AddressableAssetsData/{platform}
目录下)文件
构建完毕后会生成一个catalog,一个hash文件,和一些AssetBundles。
生成的catalog文件和之前构建产生的catalog文件同名,最新的数据会覆盖写入到旧的catalog和hash文件。APP会加载hash文件来决定是否有新的catalog需要加载。所以系统只会加载同APP一起发布的未修改过的bundle文件,以及下载下来的最新bundle文件。
系统会使用addressables_content_state.bin里的版本串和位置信息来生成AssetBundles。不需要更新的AssetBundles将会用同样的名字写入到bin文件中。如果一个AssetBundles需要更新,就会生成一个新的AssetBundles,它会有一个新的文件名,这样它就可以和原始的AssetBundles共存。所以需要把新产生的AssetBundles复制到你托管的服务器地址下以供下载。
系统也会为那些不可以修改的资源构建AssetBundles,但是不需要把他们上传到托管服务器,因为没有Addressables Asset引用他们。
需要注意的是,不要在在发布新版本和构建更新资源中间修改build script,这将会有无法预测的事情发生。
运行时检测可用更新
你可以添加一个自定义脚本来定期检查是否有新的资源需要更新,方法如下:
public static AsyncOperationHandle<List<string>> CheckForCatalogUpdates(bool autoReleaseHandle = true)
在返回结果List<string>
中包含了所有需要更新的 locator IDs,你可以从中筛选出指定的ID进行更新,或者直接全部传参给APIUpdateCatalogs
来更新所有。
如果有更新内容,你可以提供一个按钮给用户点击开始更新,或者在后台自动静默更新。需要注意的是,这需要开发人员确保旧的资源已经被释放。
UpdateCatalogs
的参数如果为空列表的话,将更新所有需要更新的内容。
public static AsyncOperationHandle<List<IResourceLocator>> UpdateCatalogs(IEnumerable<string> catalogs = null, bool autoReleaseHandle = true)
API返回一个被更新过的资源Locator列表
更新示例
在这个例子中,已发布的APP中有如下几个资源组:
Local_Static | Remote_Static | Remote_NonStatic |
---|---|---|
AssetA | AssetL | AssetX |
AssetB | AssetM | AssetY |
AssetC | AssetN | AssetZ |
Local_Static
和Remote_Static
组有被标记为 Cannot Change Post Release
。
当前版本正在运行中,用户设备上有Local_Static
资源,可能已经下载了一到两个远程包。
如果你修改每一个组的资源(比如AssetA
,AssetL
,和AssetX
),然后执行Check for Content Update Restrictions,然后资源组会变成如下:
Local_Static | Remote_Static | Remote_NonStatic | content_update_group (non-static) |
---|---|---|---|
AssetX | AssetA | ||
AssetB | AssetM | AssetY | AssetL |
AssetC | AssetN | AssetZ |
需要注意的是,预处理会修改静态资源组,这可能有些奇怪。然而系统确实是会把资源组调整为以上表格所展示的样子。并丢弃所有静态组的构建结果,因此我们从玩家的角度再来分析:
Local_Static:
Local_Static |
---|
AssetA |
AssetB |
AssetC |
Local_Static
资源已经安装在用户设备上了,并无法修改他们。只是旧版本AssetA
资源不会再被使用,变成了无效资源。
Remote_Static:
Remote_Static |
---|
AssetL |
AssetM |
AssetN |
Remote_Static
没有变化,如果这些资源还没有缓存在玩家设备上,但是在AssetM
和AssetN
被使用时,它会被下载到本地。只是其中旧版本的AssetL
就不会再被使用了。
Remote_NonStatic:
Remote_NonStatic(old) | Remote_NonStatic(new) |
---|---|
AssetX | AssetX |
AssetY | AssetY |
AssetZ | AssetZ |
旧版本是可以从服务器上删除掉,它再也不会被下载。如果玩家设备已经缓存了,它也不会被使用。 | 旧的Remote_NonStatic 被新版本替换,通过hash文件来区分。修改后的AssetX 被放在了新的bundle中 |
content_update_group:
content_update_group |
---|
AssetA |
AssetL |
content_update_group
资源包包含了所有被修改的静态资源。它会在以后的运行中被使用到。
从上面的示例中我们得出一些结论:
- 在用户设备上,任何被修改过的本地资源将再也不会被使用。
- 如果用户设备上已经缓存了非静态资源包,用户需要重新下载资源包,并且这个资源包也包含了未修改的资源(比如示例中的
AssetY
和AssetZ
)。理想的情况下,用户没有缓存这个资源包,当需要时只需要下载最新的Remote_NonStatic
资源包即可。 - 如果用户已经缓存了
Remote_Static
资源包,那只需要下载content_update_group
资源包,当然这是理想状况。如果用户没有缓存Remote_Static
资源包,那Remote_Static
资源包和content_update_group
资源包都会被下载到本地。无论初始状态如何,最后用户设备上都会拥有已失效的AssetL
,即使永远没有被使用,但它永远都在。
所以远程资源包的最佳配置,取决于你如何去使用。
Addressable Assets配置
Addressable Assets配置系统可以创建和设置一些属性来决定资源如何被打包。这些配置可以指定资源是属于本地设备的还是属于远程托管服务器上的。
对于每一个配置文件,你所修改的每一个属性,都直接影响所有构建场景下的路径。而且一次搞定。
构建配置
配置中默认有五个变量:
- BuildTarget
- LocalBuildPath
- LocalLoadPath
- RemoteBuildPath
- RemoteLoadPath
你可以随便增加或移除变量。
语法相关
所有的变量都是string类型。通常,只需要输入详细地址或值即可。但是还有两种语法格式可以用:
- 中括号
[]
。被中括号包围的变量在构建的时候会被重新赋值。它可以是其他的配置变量(比如[BuildTarget]
)或者是代码中的变量(比如[UnityEditor.EditorUserBuildSettings.activeBuildTarget]
),在构建资源时,资源组被处理完后,这些被中括号包围的变量都会被重新赋值,然后最终结果会被写入到catalog中。 - 大括号
{}
。被大括号包围的变量是会在运行时被重新赋值。通常一般都是代码中的变量(比如{UnityEngine.AddressableAssets.Addressables.RuntimePath})。
例如,有一个加载路径:{MyNamespace.MyClass.MyURL}/content/[BuildTarget]},它被设置在一个资源组上,这个资源组会生成一个名为“trees.bundle”的资源包。在构建的时候,这个bundle的加载路径会以*{MyNamespace.MyClass.MyURL}/content/Android/trees.bundle}*记录在catalog中。然后游戏运行后,catalog会被profile系统处理,MyNamespace.MyClass.MyURL最终会被替换为http://myinternet.com/content/Android/trees.bundle
。
指定打包和加载路径
一旦在配置文件中设置了必要的变量,你可以为一个资源组应用这些变量。
设置这些路径你需要这样:
- 在Project窗口中选中一个资源组。
- 在它的Inspector窗口中,在属性Content Packing & Loading > Build and Load Paths下,从下拉框中选择需要的Build Path和Load Path。
需要注意的是,不要直接输入路径,而是选择之前在Profiles窗口中定义好的变量。当你选择后,对应的详细路径会显示在下拉列表下面。
一定要小心,build path和load path必须要匹配,否则你无法从远程服务器上加载到你构建在本地路径上的资源。
一些例子
下面的例子是演示了开发阶段的内容。
Content with local and remote bundles stored locally for development.
在开发过程中,你的本地和远程bundles都会是用本地目录,看下图中“Local”的那一行。
Paths set for local development and production.
在这个例子中,事实上本地加载和远程加载都使用的本地路径。对于开发过程中来说,搭建一个远程服务器是比较麻烦的。所以当资源准备好了,你需要把bundles提交到远程托管服务器上。如下图。
Content with remote bundle moved to a server for production.
在这种情况下,你只需要在配置中选择“Production”即可。不需要修改你的资源组,就可以把所有的远程资源变成真正的远程资源。
内存管理
一一对应的加载和卸载
当使用Addressables时,正确的内存管理方式即是保证load和unload都是成对调用。具体如何实现取决于资源类型和资源加载的方式。通常情况下,release方法都是被已加载的资源,或者是加载异步处理返回的句柄来调用。例如,在场景加载(下面也有说到)过程中,加载会返回一个异步句柄AsyncOperationHandle<SceneInstance>
,你可以通过它来释放资源,或者使用handle.Result
(在本示例中即SceneInstance
)来释放。
资源加载
加载资源有两个API: Addressables.LoadAssetAsync
(加载单个)或 Addressables.LoadAssetsAsync
(加载多个)。
注意:LoadAssetAsync
是用来加载单个资源。如果你使用的address匹配到了多个资源(像使用label),这个方法会加载第一个匹配的资源。所以会加载到哪个资源不是绝对的,这跟构建顺序有关。
这些加载方法只是加载资源到内存里,没有实例化。Load方法每被调用一次,被加载的资源上就会增加一个引用计数。如果同一个address资源被LoadAssetAsync
加载三次,你会得到三个不同的异步句柄 AsyncOperationHandle
,所有的加载都会执行相同的底层操作,对应的,这个资源就会有三个引用计数。异步句柄 AsyncOperationHandle
结构会在属性.Result
中包含被加载完成的资源。你可以使用Unity内置实例化函数来实例化资源,实例化并不会增加资源的引用计数。
卸载资源使用API:Addressables.Release
。调用之后会减少资源的引用计数。当这个资源的引用计数为0时,它就会被真正卸载掉,并且它的所有依赖资源的引用计数都会减1。
注意:资源是否会被立即卸载,取决于它的依赖资源。可以从文档内存何时清除中了解更多。
场景加载
加载场景使用API:Addressables.LoadSceneAsync
。你可以通过这个方法,以Single
模式加载场景(之前被打开的场景会被关闭)或以Additive
模式加载场景(更多信息请参看Scene mode loading)。
卸载场景使用API:Addressables.UnloadSceneAsync
,或者还可以直接以Single
模式加载一个新场景。你可以使用Addressables的接口,也可以使用SceneManager
的方法:SceneManager.LoadScene
或SceneManager.LoadSceneAsync
来加载。打开新的场景就会关闭当前场景,同样也会减少引用计数。
当加载场景时,场景中所有的GameObject的依赖资源都是在场景加载操作中加载的,如果没有其它对象引用到这些AssetBundles,那么当场景被卸载后,所有的AssetBundles,包括场景的和之前需要的依赖资源的,都会被卸载。
注意:在一个通过Addressable加载的场景中,即使你把一个GameObject标记为DontDestroyOnLoad
,或者移动到另一个已经加载的场景中,当原始场景卸载掉,这些GameObject的依赖资源仍然会被卸载掉。
如果你有碰到过这样的状况,可以以下两个选择:
- 把你想要
DontDestroyOnLoad
的GameObject放在一个单独的Addressable预制体中,实例化之后然后把它设置为DontDestroyOnLoad
。 - 在卸载包含这个GameObject的场景前,把他设置为
DontDestroyOnLoad
,然后调用场景加载句柄AsyncOperationHandle.Acquire()
,这会增加场景的引用计数,保存它,然后它的依赖资源都会保留,直到这个acquired句柄的Release
被调用。
GameObject实例化
加载并实例化一个GameObject资源使用API:Addressables.InstantiateAsync
。当指定了参数location
预制体实例化之后就会出现在对应的位置。Addressables会加载这个预制体和它的所有依赖项,并且所有加载项的引用计数都会增加。
使用同一个address调用Addressables.InstantiateAsync
三次,所有相关的资源引用计数都会为增加3。但是和调用Addressables.LoadAssetAsync
三次不同,每一次调用Addressables.InstantiateAsync
返回的都是同一个AsyncOperationHandle
句柄,这是因为每一次Addressables.InstantiateAsync
的返回结果都是一个单例。Addressables.InstantiateAsync
和其他加载方式的另一个区别点在于参数trackHandle
,当它为false时,你必须把它保存下来以便于在释放实例时使用它。这是非常高效的,但是需要开发者花费更多精力来管理它。
销毁一个GameObject实例使用API:Addressables.ReleaseInstance
,或者直接关闭包含这些实例的Scene。这个Scene不管是通过Additive
或Single
模式加载,还是使用APIAddressables
或 SceneManagement
加载的都适用。根据上面提到的,如果参数trackHandle
为false时,你只能通过传入返回的异步handle作为参数来调用Addressables.ReleaseInstance
来销毁,而不能使用GameObject本身。
注意:如果你调用Addressables.ReleaseInstance
来销毁一个不是用Addressables的API创建的GameObject,或者它是在trackHandle==false
的条件下创建的,接口会返回false,表示当前操作不能释放指定的GameObject。所以在这些情况下,实例不会被销毁。
Addressables.InstantiateAsync
是有开销的。所以当你需要在一帧内实例化上百个相同物件,推荐使用Addressables的API来加载,然后使用其他方式来实例化。这种情况下,你可以调用Addressables.LoadAssetAsync
加载资源,然后保存好结果,再调用GameObject.Instantiate()
来满足你的需求。这样可以灵活地以同步方式调用实例化。缺点就是Addressables不知道你到底实例化了多少对象,如果没有合适的管理好它们,就有可能会导致内存问题。例如,一个预制体引用了一张texture,但是有可能会引用了一个无效的texture,而导致渲染问题(或更糟)。因为可能不会立即触发内存卸载,所以这些问题很难追查(请看下文清除内存部分)。
数据加载
AsyncOperationHandle.Result
不需要被释放,但是操作句柄本身是需要释放。例如Addressables.LoadResourceLocationsAsync
和Addressables.GetDownloadSizeAsync
。在操作句柄释放之前都可以访问属性值。这些释放应该使用Addressables.Release
来完成。
后台交互
当操作句柄在属性AsyncOperationHandle.Result
不包含任何返回时,有一个选项可以让操作句柄在调用完成时自动释放。当操作句柄执行完成后,你也不再需要它,可以设置参数autoReleaseHandle
为true来保证操作句柄被清除。有一种情况autoReleaseHandle
必须为false,那就是你想在句柄执行完毕后还需要检测它的Status
的时候。比如这两个接口Addressables.DownloadDependenciesAsync
和Addressables.UnloadScene
。
The Addressables Event Viewer
可以使用Addressables Event Viewer窗口来监视所有的Addressable操作的引用计数。点击菜单Window > Asset Management > Addressables> Event Viewer来打开窗口。
重点:在AddressableAssetSettings
的Inspector窗口中开启属性Send Profiler Events后,才能在Event Viewer窗口中看到数据。
在Event Viewer窗口中看到的数据有:
- 白色的垂直线表示有发生加载请求的帧。
- 蓝色背景表示当前加载的资源。
- 图表中的绿色部分表示资源当前的引用计数。
需要注意的是,Event Viewer只关心引用计数,不关心内存消耗(请查看内存何时清除了解更多)。
在资源列下面,对于每一帧都有一下数据:
- FPS:每秒运行帧率。
- MonoHeap:RAM使用情况。
- Event Counts:在这一帧里事件总数。
- Asset requests:显示一段时间内一个操作的引用计数。如果资源有依赖资源。会出现一个三角形,你可以点击它来查看以来资源的请求操作。
可以点击左右箭头来逐帧查看,点击Current跳转到最新的帧,点击**+**按钮展开一行查看更多细节数据。
在Event Viewer窗口中显示的数据和你选择build script有关联。
当使用Event Viewer时,避免使用Use Asset Database built script,因为它不会统计任何依赖资源的数据。请使用Simulate Groups script 或 Use Existing Build script,但是后者最合适,因为它提供了更准确的引用计数监控。
内存何时清除
当资源不再被引用时(profiler中蓝色区块末尾)并不意味着资源会被卸载。比如说一个asset bundle中包含了多个资源。看一个示例:
- 一个asset bundle(
stuff
)中有三个资源(tree
,tank
,和cow
)。 - 当资源
tree
加载,profiler会显示出tree
引用计数为1,同样stuff
的引用计数也为1。 - 然后,当
tank
加载后,tree
和tank
的引用计数都为1,而stuff
的引用计数为2。 - 如果卸载
tree
,它的引用计数变为0,然后profiler中的蓝色区块消失。
在上面的实例中,tree
在这个时候并没有被真正卸载。你可以加载一个asset bundle,或这个bundle的一部分资源,但是你无法只释放asset bundle的一部分。除非stuff
自身被完全卸载,否则它其中的资源都不会被释放。但是Unity的内置接口Resources.UnloadUnusedAssets
是个例外,如果在上述的示例中调用它就会导致tree
被卸载。因为Addressables无法识别这些事件,profiler里的图表仅反映Addressables的引用计数(不是真正内存占用情况)。需要注意的是,当你使用Resources.UnloadUnusedAssets
时,它是一个非常慢的操作,最好是在没有任何交互的界面下使用(比如loading界面)。
异步操作句柄
有一些Addressables的API返回AsyncOperationHandle
结构。主要目的是为了通过它来获取当前操作的status和result。在没有调用Addressables.Release
或Addressables.ReleaseInstance
之前,异步操作句柄的result都是有效的(更多内容请看内存管理)。
当一次异步操作结束后,AsyncOperationHandle.Status
的值通常都是AsyncOperationStatus.Succeeded
或AsyncOperationStatus.Failed
。如果操作成功,就可以通过AsyncOperationHandle.Result
得到想要的结果。
你可以定期检查异步操作的状态,或者通过AsyncOperationHandle.Complete
来注册一个完成事件的回调。当不再需要AsyncOperationHandle
时,就应该使用Addressables.Release
来释放它。
有类型和无类型句柄
大部分Addressables的API都可以返回泛型AsyncOperationHandle<T>
结构,它可以为AsyncOperationHandle.Complete
和AsyncOperationHandle.Result
保证类型安全。也有一个非泛型AsyncOperationHandle
结构,如果有需要的话,它们可以相互转换。
需要注意的是,如果尝试从一个非泛型句柄转换为一个错误类型的泛型句柄,会抛出一个异常。例如:
AsyncOperationHandle<Texture2D> textureHandle = Addressables.LoadAssetAsync<Texture2D>("mytexture");
// Convert the AsyncOperationHandle<Texture2D> to an AsyncOperationHandle:
AsyncOperationHandle nonGenericHandle = textureHandle;
// Convert the AsyncOperationHandle to an AsyncOperationHandle<Texture2D>:
AsyncOperationHandle<Texture2D> textureHandle2 = nonGenericHandle.Convert<Texture2D>();
// This will throw and exception because Texture2D is required:
AsyncOperationHandle<Texture> textureHandle3 = nonGenericHandle.Convert<Texture>();
AsyncOperationHandle示例
使用AsyncOperationHandle.Complete
来注册一个完成事件监听器:
private void TextureHandle_Completed(AsyncOperationHandle<Texture2D> handle) {
if (handle.Status == AsyncOperationStatus.Succeeded) {
Texture2D result = handle.Result;
// The texture is ready for use.
}
}
void Start() {
AsyncOperationHandle<Texture2D> textureHandle = Addressables.LoadAsset<Texture2D>("mytexture");
textureHandle.Completed += TextureHandle_Completed;
}
AsyncOperationHandle
实现了IEnumerator
,所以它可以在协程中调用:
public IEnumerator Start() {
AsyncOperationHandle<Texture2D> handle = Addressables.LoadAssetAsync<Texture2D>("mytexture");
yield return handle;
if (handle.Status == AsyncOperationStatus.Succeeded) {
Texture2D texture = handle.Result;
// The texture is ready for use.
// ...
// Release the asset after its use:
Addressables.Release(handle);
}
}
Addressables还可以通过AsyncOperationHandle.Task
来支持异步await
:
public async Start() {
AsyncOperationHandle<Texture2D> handle = Addressables.LoadAssetAsync<Texture2D>("mytexture");
await handle.Task;
// The task is complete. Be sure to check the Status is successful before storing the Result.
}
AsyncOperationHandle.Task
属性在WebGL
不可用,因为该平台不支持多线程。
需要注意的是,加载场景时,使用SceneManager.LoadSceneAsync
并且allowSceneActivation
设置为false时,或者使用Addressables.LoadSceneAsync
并且参数activateOnLoad
为false时,异步加载会被打断,并且将无法完成加载。具体请看allowSceneActivation documentation.
自定义操作
IResourceProvider
API允许你通过以数据驱动的方式定义位置和依赖关系来扩展加载过程。
在某些情况下,你可能需要创建自定义操作。IResourceProvider
是构建在这些自定义操作的基础。
创建自定义操作
通过继承AsyncOperationBase和重写需要的虚函数来创建一个自定义操作。你可以把它传递给ResourceManager.StartOperation
开启自定义操作,调用后返回一个AsyncOperationHandle
结构。通过这种方式启动的操作会被注册到ResourceManager
中,所以也可以在Addressables Event Viewer中观察到。
执行自定义操作
当自定义操作所依赖的操作完成后,ResourceManager
会调用AsyncOperationBase.Execute
方法。
自定义操作完成处理
当自定义操作完成后,需要在自定义操作对象里调用AsyncOperationBase.Complete
。你可以在方法Execute
内调用,或者之后在外部某个地方调用。调用AsyncOperationBase.Complete
会通知ResourceManager
该自定义操作已经完成,然后回触发所相关的AsyncOperationBase.Complete
事件。
终止自定义操作
当你释放与你的自定义操作相关联的AsyncOperationHandle
,ResourceManager
就会调用AsyncOperationBase.Destroy
,你应该在你的自定义操作的Destroy中释放所有的已分配的内存和资源。
Addressables的Analyze工具
Analyze工具可以收集项目中Addressables里的相关信息。某些情况下Analyze可能会采取适当的措施来清除项目状态。另一方面,Analyze存粹就是一个信息展示工具,你可以根据它提供的数据来适当调整你的Addressables结构。
使用Analyze工具
在编辑器中,打开Addressables Analyze窗口有两种方式:点击菜单Window > Asset Management > Addressables > Analyze,或者在Addressables Groups窗口中,点击菜单Tools > Analyze。
Addressables Analyze窗口会有一个Analyze规则列表,并由三个操作选项:
- 分析选择的规则
- 清除选择的规则
- 修复选择的规则
分析操作
分析操作这一步主要是根据分析规则收集信息。通过一条规则或多个规则来收集构建、依赖映射等其他数据。每一条规则都负责收集它所需要的数据,然后会返回一个AnalyzeResult
列表。
在分析进行时,最好不要修改项目里任何数据和状态。根据所收集到的数据,可以做出适当的修复操作。但是,有一些规则仅仅是包含了分析数据,并不能采用统一的操作来修复问题。检查Scene和Addressables的重复依赖项和检查Resources目录和Addressables的重复依赖项就是这样的例子。
如果只有存粹的分析信息,而没有相关修复操作的规则被归类为Unfixable Rules。而包含修复操作的规则被归类为Fixable Rules。
清除步骤
这个操作将会移除所有之前采集到的分析数据,并刷新列表中的树结构。
修复操作
对于Fixable Rules可以执行修复操作。它根据之前分析来的数据执行必要的修改来解决出现的问题。
检查重复bundle依赖就是一个可修复规则的例子,因为可以采取合理适当的措施来解决分析步骤中检测到的问题。
提供分析规则
Fixable rules
检查重复bundle依赖
这条规则会扫描具有BundledAssetGroupSchemas
的资源组和投影资源组结构来找出潜在的重复资源。执行的时候会触发完整的资源构建,所以这个检查是非常耗时和耗性能的。
问题:不同的资源组中有资源依赖了重复的相同资源。例如在两个不同的资源组中分别有一个Prefab都引用了一个material,这个material(和它所有的依赖资源)会被重复包含到这两个组中。为了避免如此,这个material必须要被标记为Addressable,然后把它和他的依赖项放到另一个资源组中。
解决方案:如果发现了问题,执行修复操作后,会创建一个新的资源组,里边会包含所有被重复依赖的资源。
例外:如果有一个资源包含了多个物件,实际上不同的资源组可以包含这个资源的一部分。比如说一个拥有多个网格的FBX资源,它的一个网格在资源组GroupA
中,而另一个网格在资源组GroupB
中,那这条规则就会认为这个FBX被多个资源组共享。所以执行修复之后,会把他们都提取到FBX所在的资源组中。在这种例外的情况下,执行修复其实是有害的,因为这些资源组实际上都没有包含整个FBX资源。
还需要注意的是,不是所有的重复资源都会是问题。如果资源永远都不会被同一组用户(比如指定区域资源)加载,那么重复依赖是可以的,或者至少是无关紧要的。每个项目都是不一样的,所以是否修复重复依赖需要根据项目具体情况来分析。
Unfixable rules
检查Resources目录和Addressables的重复依赖项
这条规则是检测Addressables资源和Resources
目录下的资源是否有重复资源或依赖。
问题:这表明有资源会被Addressables打包,同时也会被打包到APP中。
解决方案:这是条无修复操作的规则,因为无法判定怎么做才合适。它只是单纯提供信息和警告来告诉你有资源冗余。你需要根据实际情况来决定如何处理。可行的方案是,把有问题的资源从Resources
目录下移出,并添加到Addressables中。
检查Scene和Addressables的重复依赖项
这条规则是检测Addressables资源和编辑器的Scene列表里的场景是否有重复。
问题:这表明有场景会被Addressables打包,同时也会被打包到APP中。
解决方案:它只是单纯提供信息和警告来告诉你有场景冗余。你需要根据实际情况来决定如何处理。可行的方案是,把有这些场景从Build Settings中移除相关引用,并把它们添加到Addressables中。
检查Sprite Atlas和Addressables的重复依赖项
这条规则会检测Addressables中的图集,是否它里边的图片也有被标记为Addressable。
问题:这表明这些图片资源会被Addressables重复打包。
解决方案:它只是单纯提供信息和警告来告诉你有图片冗余。你需要根据实际情况来决定如何处理。可行的方案是,把图片从Addressables中移除,然后把引用图片的资源修改为引用包含这张图片的图集。
Bundle构建结构
这条规则展示了Addressables中构建时的结构,可以看到有哪些显式资源,以及这些资源又隐式依赖了那些资源,从而得知哪些资源在构建时被包含进来。
采集的数据只是存粹的展示了结构信息,不表示任何错误和问题。
扩展Analyze
每一个项目或许都需要一些原始包里没有的分析规则。AAS支持创建自定义规则。
AnalyzeRule对象
创建一个继承AnalyzeRule
的子类,然后重写下面的属性:
CanFix
告诉Analyze这条规则是否具有可修复功能。ruleName
在Addressables Analyze窗口以这个名字显示这条规则。
你还需要重写以下方法:
List<AnalyzeResult> RefreshAnalysis(AddressableAssetSettings settings)
void FixIssues(AddressableAssetSettings settings)
void ClearAnalysis()
注意:如果你的自定义规则不具有修复功能,就不需要重写FixIssues
方法。
RefreshAnalysis
在这个函数里定制你的分析操作。你可以进行计算和缓存后续修复所需要的数据。返回值是一个列表List<AnalyzeResult>
。收集到数据之后,为分析中的每条数据创建一个AnalyzeResult
。把数据串作为第一个参数,把MessageType
作为第二个参数(指定这条结果是警告还是错误)。最后返回整个结果列表。
如果你想把AnalyzeResult
在TreeView
中以子列表的形式呈现,你可以用kDelimiter
来分割父节点和子节点。
FixIssues
在这个函数里可以定制你的修复操作。如果有合适的操作来处理分析步骤里出现的问题,那就在这里执行。
ClearAnalysis
这个函数是用来做清理操作。你可以在这里清除在分析步骤里缓存的数据。TreeView
也会对应的更新列表。
增加自定义规则到GUI
自定义的规则需要调用AnalyzeWindow.RegisterNewRule<RuleType>()
来注册,注册后它才会在Addressables Analyze窗口显示。例如:
class MyRule : AnalyzeRule {}
[InitializeOnLoad]
class RegisterMyRule
{
static RegisterMyRule()
{
AnalyzeWindow.RegisterNewRule<MyRule>();
}
}