【Unity】Addressables下的图集(SpriteAtlas)内存优化
前言:
资源管理系统:Addressables
UI:模拟NGUI图集Sprite,在UGUI下继承Image增加UIImage组件,实现将SpriteAtlas组件拖拽到属性面板上,切换选择里面的小图
问题:在检查项目内存占用过高问题时,发现直接拖拽上去的资源不受Addressables系统的自动引用管理,导致部分资源虽然没有引用,但是未被释放,需要等待统一释放
ps.发现一个自己的“BUG”,在销毁UIImage物体时,忘记把Sprite属性置空,它经过代码控制切换的小图,在销毁后引用关系没有解除。。。
--------------------------------------------------------------------------------------------------------------------------
想法一(希望不会有二三四):将原本拖拽图集并把引用保存到序列化信息中的操作,更改为只保存拖拽时对应资源的Addressables地址,并在UIImage初始化时,通过Addressables的加载接口去加载,这样把图集都放入它的自动管理里面,得让它干活!
开干->新建文件夹
1 public class LImage : Image 2 { 3 /// <summary> 图集资源地址 </summary> 4 [SerializeField] 5 private string m_AtlasResPath; 6 7 /// <summary> 加载到的图集 </summary> 8 private SpriteAtlas m_Atlas; 9 10 /// <summary> 当前显示小图名称 </summary> 11 [SerializeField] 12 private string m_SpriteName; 13 }
暂时先需要这些,图集资源地址和当前设置的小图名称是需要参与序列化,保存到预设中
1、初始加载图集
1 public void Start() 2 { 3 InitAtlas(); 4 } 5 6 private void InitAtlas() 7 { 8 Addressables.LoadAssetAsync<SpriteAtlas>(m_AtlasResPath).Completed += AtlasLoadCompleted; 9 } 10 11 private void AtlasLoadCompleted(UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle<SpriteAtlas> obj) 12 { 13 if (!obj.IsDone || obj.Status == AsyncOperationStatus.Failed) 14 { 15 if (obj.OperationException != null && obj.OperationException.Message != null) 16 UnityEngine.Debug.LogError("instantiate error:" + obj.OperationException.Message); 17 else 18 UnityEngine.Debug.LogError("instantiate error:....."); 19 return; 20 } 21 22 m_Atlas = obj.Result; 23 }
2、初始化Sprite
1 #region 初始化Sprite 2 private void InitSprite() 3 { 4 if (m_Atlas != null && !string.IsNullOrEmpty(m_SpriteName)) 5 { 6 sprite = m_Atlas.GetSprite(m_SpriteName); 7 } 8 } 9 #endregion
在上面加载到图集后调用一次
3、提供SpriteName切换接口
1 public string SpriteName 2 { 3 get { return m_SpriteName; } 4 set 5 { 6 SetSpriteName(value); 7 } 8 } 9 10 #region 初始化Sprite 11 private void InitSprite() 12 { 13 SetSprite(); 14 } 15 16 private void SetSprite() 17 { 18 if (m_Atlas != null && !string.IsNullOrEmpty(m_SpriteName)) 19 { 20 sprite = m_Atlas.GetSprite(m_SpriteName); 21 } 22 } 23 24 private void SetSpriteName(string newValue) 25 { 26 if (m_SpriteName != newValue) 27 { 28 m_SpriteName = newVal
ue; 29 30 SetSprite(); 31 } 32 } 33 #endregion
先简单测试一遍上面的流程
完全没有问题
问题:在同一个图集,切换小图时,重复使用过的它也会重新复制一个出来,如果你这个用来播放序列帧,那内存就是+++++++
有两个方案:
方案一:当时是在LImage中用缓存字典单独储存当前图集中的小图,重复使用时,可以直接用之前复制出来的缓存,避免重复生成;
1 private Dictionary<string, Sprite> m_CacheSpriteDic = new Dictionary<string, Sprite>(); 2 3 private Sprite GetSprite(string spritename) 4 { 5 if (!m_CacheSpriteDic.TryGetValue(spritename, out var _spriteCache)) 6 { 7 if (m_Atlas != null) 8 { 9 _spriteCache = m_Atlas.GetSprite(spritename); 10 m_CacheSpriteDic[spritename] = _spriteCache; 11 } 12 } 13 14 return _spriteCache; 15 }
多次切换,可以看出是可以解决缓存不断复制的问题,只是引用关系都还在,所以在物体销毁时,增加一个队缓存字典的释放销毁即可
方案二:在切换Sprite的时候把旧Sprite销毁掉
1 private void SetSprite() 2 { 3 if (m_Atlas != null && !string.IsNullOrEmpty(m_SpriteName)) 4 { 5 if (sprite != null) 6 { 7 GameObject.Destroy(sprite); 8 } 9 sprite = null; 10 sprite = m_Atlas.GetSprite(m_SpriteName); 11 } 12 }
多次切换,可以看到在缓存中只有一份,它的问题在于如果是频繁切换,它会频繁销毁,缓存(不推荐)
方案三:新想的方案,之前是在单一LImage中缓存,如果创建一个图集单例管理,把正在使用的图集对应的缓存存储,不管是哪个LImage使用,都可以使用用一份Sprite缓存;
这个方案是在方案一的基础上诞生的,因为方案一尽管同一个LImage上的相同缓存只会保留一份,但如果多个LImage,还是会有多份缓存,所以我想着是不是可以在内存中,同一个图集中的同一个小图缓存只保留一份
新加脚本!!!。。。。
1 using System; 2 using System.Collections.Generic; 3 using UnityEngine; 4 using UnityEngine.AddressableAssets; 5 using UnityEngine.ResourceManagement.AsyncOperations; 6 using UnityEngine.U2D; 7 8 public class SpriteAtlasManager 9 { 10 private static SpriteAtlasManager _instance; 11 public static SpriteAtlasManager Instance => _instance ?? (_instance = new SpriteAtlasManager()); 12 13 private readonly Dictionary<string, AtlasInfo> _atlasCache = new Dictionary<string, AtlasInfo>(); 14 15 private AtlasInfo tempAtlasInfo; 16 17 public AtlasInfo GetAtlasInfo(string atlasName, Action callback = null) 18 { 19 if (_atlasCache.TryGetValue(atlasName, out tempAtlasInfo)) 20 { 21 tempAtlasInfo.AddCallBack(callback); 22 tempAtlasInfo.AddRefCount(); 23 return tempAtlasInfo; 24 } 25 26 _atlasCache[atlasName] = tempAtlasInfo = new AtlasInfo(atlasName, callback); 27 28 return tempAtlasInfo; 29 } 30 31 public Sprite GetSprite(string atlasName, string spriteName, Action callback) 32 { 33 tempAtlasInfo = GetAtlasInfo(atlasName, callback); 34 if (tempAtlasInfo != null) 35 { 36 return tempAtlasInfo.GetSprite(spriteName); 37 } 38 return null; 39 } 40 41 42 public void ReleaseAtlas(string atlasName, Action callback) 43 { 44 if (_atlasCache.TryGetValue(atlasName, out tempAtlasInfo)) 45 { 46 tempAtlasInfo.RemoveCallBack(callback); 47 tempAtlasInfo.RemoveRefCount(); 48 } 49 } 50 internal void DestroyAtlas(string atlasName) 51 { 52 if (_atlasCache.TryGetValue(atlasName, out tempAtlasInfo)) 53 { 54 tempAtlasInfo.Release(); 55 _atlasCache.Remove(atlasName); 56 } 57 } 58 } 59 60 public class AtlasInfo 61 { 62 private string _atlasName; 63 private SpriteAtlas _atlas; 64 private Dictionary<string, Sprite> _spriteCache; 65 private int refCount; 66 67 68 public AsyncOperationHandle<SpriteAtlas> OperationHandle; 69 private List<Action> m_LoadCompletedCallBack; 70 71 public AtlasInfo(string atlasName, Action callback) 72 { 73 _atlasName = atlasName; 74 m_LoadCompletedCallBack = new List<Action>(); 75 _spriteCache = new Dictionary<string, Sprite>(); 76 77 AddCallBack(callback); 78 OperationHandle = Addressables.LoadAssetAsync<SpriteAtlas>(atlasName); 79 OperationHandle.Completed += OnAtlasLoadCompleted; 80 } 81 82 public void AddCallBack(Action callback) 83 { 84 if (!m_LoadCompletedCallBack.Contains(callback)) 85 { 86 m_LoadCompletedCallBack.Add(callback); 87 } 88 } 89 90 public void RemoveCallBack(Action callback) 91 { 92 m_LoadCompletedCallBack.Remove(callback); 93 } 94 95 private void OnAtlasLoadCompleted(AsyncOperationHandle<SpriteAtlas> handle) 96 { 97 if (handle.Status == AsyncOperationStatus.Succeeded) 98 { 99 _atlas = handle.Result; 100 foreach (var callback in m_LoadCompletedCallBack) 101 { 102 callback?.Invoke(); 103 } 104 } 105 else 106 { 107 Debug.LogError($"Failed to load SpriteAtlas: {_atlasName}. Error: {handle.OperationException?.Message}"); 108 } 109 } 110 111 public Sprite GetSprite(string spriteName) 112 { 113 if (string.IsNullOrEmpty(spriteName)) return null; 114 if (!_spriteCache.TryGetValue(spriteName, out var sprite)) 115 { 116 sprite = _atlas?.GetSprite(spriteName); 117 if (sprite != null) 118 { 119 _spriteCache[spriteName] = sprite; 120 } 121 } 122 123 return sprite; 124 } 125 126 internal void AddRefCount() 127 { 128 refCount++; 129 } 130 131 internal void RemoveRefCount() 132 { 133 refCount--; 134 135 if (refCount == 0) 136 { 137 SpriteAtlasManager.Instance.DestroyAtlas(_atlasName); 138 } 139 } 140 141 internal void Release() 142 { 143 _atlas = null; 144 foreach (var item in _spriteCache) 145 { 146 GameObject.Destroy(item.Value); 147 } 148 _spriteCache.Clear(); 149 _spriteCache = null; 150 if (OperationHandle.IsValid()) 151 { 152 Addressables.Release(OperationHandle); 153 } 154 } 155 }
加载图集信息
m_Atlas = SpriteAtlasManager.Instance.GetAtlasInfo(m_AtlasResPath, InitSprite);
protected override void OnDestroy() { base.OnDestroy(); SpriteAtlasManager.Instance.ReleaseAtlas(m_AtlasResPath, InitSprite); }
在不需要当前图集信息的时候,调用一下卸载接口,减引用数量,当引用数为0的时候就会卸载掉资源
只看资源缓存和引用方面来看,这个方案是成功的
但是我还是担心在调用过程中,有什么临时变量等问题产生,再做一下性能对比!
===============================================================
图集加载和使用缓存、卸载就大概这样,下面处理一下编辑器模式下的面板处理和预览
在非运行情况下(编辑器模式下),将资源直接拖拽到Image
组件的Sprite
属性上后,该资源会被Unity序列化到场景或预制件中,即便在运行时销毁了Image
组件本身,资源本体依然保存在序列化数据中,并不会被直接卸载。运行期间,Unity对拖拽到Inspector面板上的资源进行引用计数管理,即使Image
组件被销毁,拖拽到Sprite
属性中的资源依然占用内存,因为它在场景中存在序列化引用。