在开始介绍Addressable系统之前,我们先来回顾下传统的几种资源加载方式:
可寻址资产系统是Unity推出的新的资源管理插件。Addressable系统是在 Asset Bundle之上,提供了异步加载、依赖管理以及内存管理等丰富的资源管理功能,也能够让开发者实现远程资源更新更加的便捷。
Unity 2018.2及以上版本,使用PackageManager安装Addressables。
Window > Asset Management > Addressables > Groups进入使用界面
Addressable设置:
资源默认分为Built In Data和LocalGroup(Default)两组,前者包含一些内置资源,不能改动,后者可以进行添加或删除资源。
这里有两种方式将资源标记成可寻址的,在安装好可寻址资源包后,你可以在属性面板进行标记或者将其拖拽到管理窗口指定分组上。在资源的属性窗口上,点击Address复选框并为资源设置唯一标识符。
注:如果我们标记的资产在Resources文件夹下时,Addressable系统会提示你讲资产移出Resources文件夹。
配置好资源分组,根据需要设置Play Mode Script,再通过Build > New Build > Default Build Script打包测试。
本地资源打包路径: Library/com.unity.addressables/StreamingAssetsCopy/aa/Android/
远程资源打包路径: ServerData/Android/
同时远程目录下会生成有.hash和.json文件,.hash文件内只包含一个catalog文件的Hash值,用于客户单检测catalog更新时,通过对比这个hash值,判断是否有catalog更新,json文件内包含每个Ab包的hash值和地址。
使用Build > Update a previous Build 更新资源包时,需要选择一个bin文件(android环境为例,Anroid/.bin),这个bin文件记录了所有Ab包之间的依赖关系和分组信息,Addressable系统通过这个bin文件管理依赖。
使用AssetReference加载资源:
[SerializeField] private AssetReference m_AssetReference;
private void Start() {
m_AssetReference.LoadAssetAsync<GameObject>();
}
使用Addressables加载单个资源:
private void OnResLoadAsset(string key)
{
Addressables.LoadAssetAsync<GameObject>(key).Completed += OnCompleteLoad;
}
private void OnCompleteLoad(AsyncOperationHandle<GameObject> asyncOperationHandle)
{
GameObject go = GameObject.Instantiate(asyncOperationHandle.Result);
}
private void OnResInstantiate(string key)
{
Addressables.InstantiateAsync(key);
}
加载多个资源:
private void OnResLoadAsset(string key,string lable)
{
Addressables.LoadAssetsAsync<Texture2D>(new List<object> { key, lable }, null,
Addressables.MergeMode.Intersection).Completed += OnCompleteLoadAssets;
}
private void OnCompleteLoadAssets(AsyncOperationHandle<IList<Texture2D>> asyncOperationHandle)
{
//DebugTools.Log(asyncOperationHandle.Result.Count);
}
注:第三个参数,MergeMode查找资源的合并模式,以传入的参数是new List{key,label}为例
- Node或UseFirst时,会取第一个key查询到的资源
- Union时,取并集
- Intersection时,取交集
小结:
(1)加载资源时,若加载资源指定的类型与资源类型不一致,Addressable系统找不到该资源,则抛出异常,无法加载资源,前提:系统设置勾选了Send Profiler Events。
(2)使用标签管理,同一个资源的地址和标签可以相同,当有多个资源标签相同,Addressable系统会返回第一个满足条件的资源。
(3)若资源的地址名称与下一个资源的标签相同,返回还是第一个资源,Addressable系统会对比资源的地址和标签,若都不相同,才会继续向下查找
热更新资源包修改后,需要对资源重新打包
Check for Content Update Restrictions: 针对是静态资源组,既是Update Restriciton属性为Cannot Change Post Release值。点击后弹出选择之前打包资源组生成的bin文件,点击“Apply Changes”应用更改,增加或修改的资源会被移动到新建Content Update分组。
Update a Previous Build: 动态资源组更新时,执行该操作,同样需要选择bin文件,系统会自动生成一个新的AB包。
核心代码
public IEnumerator CheckForContentUpdate(List<object> keys)
{
for (int index = 0; index < keys.Count; index++)
{
AsyncOperationHandle<long> DownloadSize = Addressables.GetDownloadSizeAsync(keys[index]);
yield return DownloadSize;
if (DownloadSize.Result <= 0)
{
Debug.Log("[Addressable]:不需要更新的资源标签:" + keys[index]);
keys.Remove(keys[index]);
}
else
{
m_TotalSize += DownloadSize.Result / Mathf.Pow(1024, 2);
}
}
m_DownloadDependencies = Addressables.DownloadDependenciesAsync(keys, Addressables.MergeMode.Union, false);
yield return m_DownloadDependencies;
}
注:动态资源更新,旧资源会被覆盖,动态资源组中有一个资源需要更新,热更时会将整个资源组都下载下来
,因此合理划分资源分组十分重要,减少重复下载以及打包粒度(多个资源需要相同的材质、贴图等资源)。
项目中需要管理的资源过多时,勾选Addressable或拖拽的方式明显不在合适,因此需要实现一个方法,将某个文件夹下的所有资源标记为可寻址资源。
编辑状态创建一个新的菜单,并创建一个asset文件,配置需要标记的资源文件夹,可同时标记多个资源,配置如下:
public static void AutoSetGroup(string groupName, string lableName, string assetPath, bool isSimplied = false)
{
var set = AddressableAssetSettingsDefaultObject.Settings;
AddressableAssetGroup Group = set.FindGroup(groupName);
if (Group == null)
{
Group = set.CreateGroup(groupName, false, false, false, new List<AddressableAssetGroupSchema>
{ set.DefaultGroup.Schemas[0], set.DefaultGroup.Schemas[1]}, typeof(SchemaType));
}
string Guid = AssetDatabase.AssetPathToGUID(assetPath); //获取指定路径下资源的 GUID(全局唯一标识符)
AddressableAssetEntry asset = set.CreateOrMoveEntry(Guid, Group);
if (isSimplied)
{
asset.address = Path.GetFileNameWithoutExtension(assetPath);
}
else
{
asset.address = assetPath;
}
asset.SetLabel(lableName, true, true);
}
具体思路:
根据文件夹路径,获取该文件夹下的所有资源的路径信息,调用添加分组接口,检查是否存在当前分组,若无,则创建AddressableAssetGroup类型分组对象,设置默认状态,使用AssetDatabase.AssetPathToGUID获取当前路径资源的GUID,通过AddressableAssetSettingsDefaultObject.Settings.CreateOrMoveEntry(),创建AddressableAssetEntry对象,既勾选了addressable,再去简化资源地址和设置标签。
资源加载
Addressables.LoadAssetAsync();单个资源
Addressables.LoadAssetsAsync();多个资源
Addressables.LoadSceneAsync();场景的加载
GamoeObject实例化加载
Addressables.InstantiateAsync();实例化加载
GameObject.Instantiate();Unity提供实例化方法
资源卸载
Addressables.UnloadSceneAsync();场景的卸载
Addressables.Release();释放资源,参数是资源或 AsyncOperationHandle句柄
Addressables.ReleaseInstance();销毁Addressable系统创建的实例
注:Addressables.InstantiateAsync()和其他加载调用的另一个区别就是有一个可选的trackHandle参数,当设置为false时,就必须通过AsyncOperationHandle句柄来释放资源,而不能再通过AsyncOperationHandle.Result加载资源释放了。
引用计数问题: 资源卸载,可手动和自动。
手动卸载,Addressable系统加载和卸载资源都是成对存在的,使用Addrsssables.Release()或Addressables.ReleaseInstance()方法卸载资源,减少引用计数。当资源的引用计数为0时,该资源就准备好卸载了,并减少了所有依赖项的引用计数。
自动卸载,包含它的场景关闭时允许自动清理。
卸载问题
若使用Addressables.ReleaseInstance()传入的实例并不是Addressables系统API创建的,或者是通过句柄创建实例,系统会检测到并返回false,以指示该方法无法释放指定的实例。在这种情况下,实例不会被销毁。
Addressables.LoadAsset()和Addressables.InstantiateAsync()讨论
Addressables.InstantiateAsync()有一些相关的开销,所以如果需要在每一帧中实例化数百次相同的对象,可以考虑通过Addressables.LoadAsset()方法加载,然后通过GameObject.Instantiate()实例化。缺点是Addressables系统不知道您创建了多少实例,如果管理不当,可能会导致内存问题。例如,一个Prefab引用了一个加载不正确或者已经卸载的纹理,会导致渲染问题(或更糟)。这类问题很难跟踪,因为您可能不会立即触发内存卸载 。
清除内存
不再被引用的资源并不一定意味着资源产已被卸载。一个常见的应用场景涉及到一个资源包中包含多个资源。例如:
您有三个资源(“树”,“坦克”,“牛”)在同一个资源包(“东西”)。
当“树”加载时,“树”的ref-count +1,“东西”的ref-count +1。
稍后,当“坦克”加载时,“树”和“坦克”的ref-count均为1,并且“东西”包的ref-count为2。
如果你释放“树”,它的ref-count就会变成0。
在这个例子中,“树”资源实际上并没有被卸载。您可以加载资源包或其部分内容,但不能部分卸载资源包。在包本身完全卸载之前,所有资产都不会卸载。这个规则的例外是Resources.UnloadUnusedAssets,在上述场景中执行此方法将导致树卸载。因为Addressables系统不能识别这些事件, 只反映Addressables的ref-counts (不完全反映内存中存在的内容)。注意,如果您选择使用Resources.UnloadUnusedAssets,这是一个非常慢的操作,应该只在一个不会显示任何游戏内容的屏幕调用(比如加载屏幕)。
我们在使用Addressable系统时,需要考虑的是:需要多少个Group?这个Group里面放什么资源?打包方式是Pack Together 或者 Pack Together By Label 或者 Pack Separately? 很显然,这个跟使用Assetbundle是一样的,需要开发人员自己来规划。这不是因为Addressable不够强大,而是这是跟具体项目有关,每个项目的情况各不相同。