【Unity】Addressables下的圖集(SpriteAtlas)記憶體最佳化

lovewaits發表於2024-10-30

前言:
資源管理系統: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,還是會有多份快取,所以我想著是不是可以在記憶體中,同一個圖集中的同一個小圖快取只保留一份

新加指令碼!!!。。。。

【Unity】Addressables下的圖集(SpriteAtlas)記憶體最佳化
  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 }
SpriteAtlasManager

載入圖集資訊

m_Atlas = SpriteAtlasManager.Instance.GetAtlasInfo(m_AtlasResPath, InitSprite);
protected override void OnDestroy()
{
    base.OnDestroy();
    SpriteAtlasManager.Instance.ReleaseAtlas(m_AtlasResPath, InitSprite);
}

在不需要當前圖集資訊的時候,呼叫一下解除安裝介面,減引用數量,當引用數為0的時候就會解除安裝掉資源

只看資源快取和引用方面來看,這個方案是成功的

但是我還是擔心在呼叫過程中,有什麼臨時變數等問題產生,再做一下效能對比!

相關文章