基础准备

我们需要使用协程来开启异步加载

ResourceManager中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
public class ResourceManager : SingletonPattern<ResourceManager>
{
//异步加载中间类的类对象池
protected ClassObjectPool<AsyncLoadResParam> m_AsyncLoadResParamPool = ObjectPoolManager.Instance.GetOrCreatClassPool<AsyncLoadResParam>(50);
//异步加载回调类的类对象池
protected ClassObjectPool<AsyncCallBack> m_AsyncCallBackPool = ObjectPoolManager.Instance.GetOrCreatClassPool<AsyncCallBack>(100);
//Mono脚本
protected MonoBehaviour m_StartMono;
//正在异步加载的资源列表数组,这一步是为了控制异步加载资源的顺序按照“High——Middle——Low、High——Middle——Low……”的方式加载
protected List<AsyncLoadResParam>[] m_LoadingAssetList = new List<AsyncLoadResParam>[(int)LoadResPriority.RES_NUM];
//正在异步加载的Dic,这一步是为了多次异步加载同一个资源时,如果这个资源正在加载中,那么只添加回调
protected Dictionary<uint,AsyncLoadResParam> m_LoadingAssetDic = new Dictionary<uint,AsyncLoadResParam>();
public void Init(MonoBehaviour mono)
{
for (int i = 0; i < (int)LoadResPriority.RES_NUM; i++)
{
m_LoadingAssetList[i] = new List<AsyncLoadResParam>();
}
m_StartMono = mono;
m_StartMono.StartCoroutine(AsyncLoadCor());
}
/// <summary>
/// 异步加载资源,仅加载不需要实例化的资源
/// </summary>
public void AsyncLoadResource(string path,OnAsyncObjFinish dealFinish,LoadResPriority priority,bool isSprite = false, object param1 = null, object param2 = null, object param3 = null,uint crc = 0)
{
if (crc == 0)
{
crc = CRC32.GetCRC32(path);
}
ResourceItem item = GetCacheResourceItem(crc);
if (item != null)
{
if (dealFinish != null)
{
dealFinish(path,item.m_Obj,param1,param2,param3);
}
return;
}
//判断是否在加载中
AsyncLoadResParam para = null;
if (!m_LoadingAssetDic.TryGetValue(crc, out para) || para == null)
{
para = m_AsyncLoadResParamPool.Spawn(true);
para.m_Crc = crc;
para.m_Path = path;
para.m_IsSprite = isSprite;
para.m_Priority = priority;
m_LoadingAssetDic.Add(crc, para);
m_LoadingAssetList[(int)priority].Add(para);
}
//往AsyncLoadResParam中间类的回调列表内添加回调
//如果相同的资源有多次被加载,只需要添加回调
AsyncCallBack callBack = m_AsyncCallBackPool.Spawn(true);
callBack.m_DealFinish = dealFinish;
callBack.m_Param1 = param1;
callBack.m_Param2 = param2;
callBack.m_Param3 = param3;
para.m_CallBackList.Add(callBack);
}

/// <summary>
/// 异步加载
/// </summary>
IEnumerator AsyncLoadCor()
{
while(true)
{
yield return null;
}
}

}
public enum LoadResPriority
{
RES_HIGH = 0,
RES_MIDDLE = 1,
RES_LOW = 2,
RES_NUM = 3,
}
public class AsyncLoadResParam
{
public List<AsyncCallBack> m_CallBackList = new List<AsyncCallBack>();
public uint m_Crc;
public string m_Path;
public bool m_IsSprite = false;
public LoadResPriority m_Priority = LoadResPriority.RES_LOW;
public void Reset()
{
m_CallBackList.Clear();
m_Crc = 0;
m_Path = string.Empty ;
m_IsSprite = false;
m_Priority = LoadResPriority.RES_LOW;
}
}
public class AsyncCallBack
{
public OnAsyncObjFinish m_DealFinish = null;
public object m_Param1 = null;
public object m_Param2 = null;
public object m_Param3 = null;
public void Reset()
{
m_DealFinish = null;
m_Param1 = null ;
m_Param2 = null ;
m_Param3 = null ;
}
}
public delegate void OnAsyncObjFinish(string path, Object obj,object param1 = null,object param2 = null,object param3 = null);

解释

我们添加了AsyncLoadResParam中间类,这个中间类通过m_AsyncLoadResParamPool类对象池来缓存,它是异步加载列表m_LoadingAssetList的最基本单位,其中包括了资源的crc、path以及加载优先级等

我们添加了异步资源加载的回调类AsyncCallBack,这个类通过m_AsyncCallBackPool类对象池来缓存,我们还添加了OnAsyncObjFinish委托。AsyncCallBack回调类的目的就是储存回调参数和回调方法。

当开启一个异步加载时,会先判断此资源是否已经加载完毕,如果没有,会再判断此资源是否在加载中(m_LoadingAssetDic来记录),如果在加载中,我们只需要在AsyncLoadResParam中间类的回调列表添加回调类AsyncCallBack即可。之所以这么做,是因为在异步加载时同一个资源可能会被多个地方请求,在资源加载完成之前,我们需要将这些请求的回调来缓存。同步加载不这么做,是因为同步加载需要一帧内完成,所以不需要缓存。

异步加载协程

我们来补充上异步加载协程内部的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
public class ResourceManager : SingletonPattern<ResourceManager>
{
/// <summary>
/// 最长连续卡着加载资源的时间,单位微秒
/// </summary>
private const long MAXLOADRESTIME = 200000;
//...
/// <summary>
/// 异步加载
/// </summary>
IEnumerator AsyncLoadCor()
{
List<AsyncCallBack> callBackList = null;
//上一次Yield的时间
long lastYieldTime = System.DateTime.Now.Ticks;
while(true)
{
bool haveYield =false;
for (int i = 0; i < (int)LoadResPriority.RES_NUM; i++)
{
if (m_LoadingAssetList[(int)LoadResPriority.RES_HIGH].Count > 0)
{
i = (int)LoadResPriority.RES_HIGH;
}
else if(m_LoadingAssetList[(int)LoadResPriority.RES_MIDDLE].Count > 0)
{
i = (int)LoadResPriority.RES_MIDDLE;
}
List<AsyncLoadResParam> loadingList = m_LoadingAssetList[i];
if(loadingList.Count <= 0)
{
continue;
}
AsyncLoadResParam loadingItem = loadingList[0];//?
loadingList.RemoveAt(0);
callBackList = loadingItem.m_CallBackList;

Object obj = null;
ResourceItem resItem = null;
#if UNITY_EDITOR
if (!m_LoadFormAssetBundle)
{
if(loadingItem.m_IsSprite)
{
obj = LoadAssetByEditor<Sprite>(loadingItem.m_Path);
}
else
{
obj = LoadAssetByEditor<Object>(loadingItem.m_Path);//这不是个异步方法
}
//模拟异步加载
yield return new WaitForSeconds(0.5f);
resItem = AssetBundleManager.Instance.FindResourceItem(loadingItem.m_Crc);
if (resItem == null)
{
resItem = new ResourceItem
{
m_Crc = loadingItem.m_Crc,
};
}
}
#endif
if(obj == null)
{
resItem = AssetBundleManager.Instance.LoadResourceAssetBundle(loadingItem.m_Crc);
if(resItem != null && resItem.m_AssetBundle != null)
{
AssetBundleRequest abRequest = null;
if (loadingItem.m_IsSprite)
{
abRequest = resItem.m_AssetBundle.LoadAssetAsync<Sprite>(resItem.m_AssetName);
}
else
{

abRequest = resItem.m_AssetBundle.LoadAssetAsync(resItem.m_AssetName);
}
yield return abRequest;
if(abRequest.isDone)
{
obj = abRequest.asset;
}
lastYieldTime = System.DateTime.Now.Ticks;
}
}
//callBackList.Count的数量就是此资源被引用的数量
CacheResource(loadingItem.m_Path,ref resItem,loadingItem.m_Crc,obj,callBackList.Count);
for (int j = 0; j < callBackList.Count; j++)
{
AsyncCallBack callBack = callBackList[j];
if(callBack != null && callBack.m_DealFinish != null)
{
callBack.m_DealFinish(loadingItem.m_Path, obj, callBack.m_Param1, callBack.m_Param2, callBack.m_Param3);
callBack.m_DealFinish = null;
}
callBack.Reset();
m_AsyncCallBackPool.Recycle(callBack);
}

obj = null;
callBackList.Clear();
m_LoadingAssetDic.Remove(loadingItem.m_Crc);

loadingItem.Reset();
m_AsyncLoadResParamPool.Recycle(loadingItem);

if (System.DateTime.Now.Ticks - lastYieldTime > MAXLOADRESTIME)
{
yield return null;
lastYieldTime = System.DateTime.Now.Ticks;
haveYield = true;
}
}
//这是一个无限循环的协程,我们不能一直return,所以需要设置一个时间差,
//既能保证每次加载的资源足够,又能防止加载时间过长
if(!haveYield || System.DateTime.Now.Ticks - lastYieldTime > MAXLOADRESTIME)
{
lastYieldTime = System.DateTime.Now.Ticks;
yield return null;
}
}
}
}

解释

异步加载的逻辑是:

在一个大循环(while(true))内,将m_LoadingAssetList列表内部的资源按照“RES_HIGH -RES_MIDDLE -RES_LOW” 的顺序加载出来。

注意我们的AsyncLoadResParam有一个m_IsSprite变量,我们读取Sprite的时候需要单独设定,因为Sprite不能通过as关键字转换出来,那样做的话as出来的是Texture2D而不是Sprite。

异步加载演示

如果想要异步加载,我们首先要初始化ResourceManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using UnityEngine;

public class GameStart : MonoBehaviour
{
public AudioSource m_AudioSource;
private AudioClip m_AudioClip;
private void Awake()
{
AssetBundleManager.Instance.LoadAssetBundleConfig();
ResourceManager.Instance.Init(this);//初始化异步加载
}
private void Start()
{
ResourceManager.Instance.AsyncLoadResource("Assets/GameData/Sounds/menusound.mp3", OnLoadFinish, LoadResPriority.RES_MIDDLE);
}
void OnLoadFinish(string path,Object obj,object param1,object param2,object param3)
{
m_AudioClip = (AudioClip)obj;
m_AudioSource.clip = m_AudioClip;
m_AudioSource.Play();
}
private void Update()
{
if(Input.GetKeyDown(KeyCode.A))
{
m_AudioSource.Stop();
ResourceManager.Instance.ReleaseResource(m_AudioClip,true);
m_AudioSource.clip = null;
m_AudioClip = null;
}
}

}

我们可以在Profiler中点击Memory——切换为Detailed——再点击Make Sample,查看我们的AudioClip在内存中的引用,这是我们按下A键,由于我们调用了ResourceManager.Instance.ReleaseResource(m_AudioClip,true);,第二个参数true代表着我们将此资源完全清除,这时我们再Make Sample,AudioClip已经不在内存中了。

注意,我们可以修改ResourceManagerm_LoadFromAssetBundle来测试编辑器下加载和AssetBundle加载的情况。