UFL5-2——优化Resources资源加载模块的异步加载
UFL5-2——优化Resources资源加载模块的异步加载
异步加载问题指的是什么
每次进行异步加载时,都会开启一个协同程序,虽然Resources
资源会在内部进行缓存,加载已加载过的资源,性能消耗不会太大
但是每次开启协程的过程也会浪费性能,因此我们希望对上节课的ResManager
进行优化
不依赖Resources
内部的缓存机制,而是自己来管理已经加载过的资源,从而解决异步加载时协同程序的频繁开启造成的性能浪费
例如下面的代码就是加载的一模一样的资源
1 | ResManager.Instance.LoadAsync<GameObject>("Test", (obj) => |
我们想要达到的目的是:通过一个字典记录已经加载过的资源,每次在进行资源加载时,如果发现是已经加载过的资源,我们直接使用即可
制作思路和具体实现
-
字典容器结构设计
主要考虑点
-
key
- 资源名(路径 + 类型 拼接而成resName = $"{path}_{typeof(T).Name}"
) -
value
- 自定义数据结构类ResInfo<T>
:资源、加载完执行的委托、协程对象等其中,不同类型的资源必然会使用不同的协程参数,为了能够让字典装载他们,需要再声明一个
ResInfoBase
并让ResInfo<T>
继承1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/// <summary>
/// 资源消息基类
/// </summary>
public abstract class ResInfoBase { }
/// <summary>
/// 资源消息对象 主要用于存储资源消息,异步加载委托消息,异步加载协程消息
/// </summary>
/// <typeparam name="T"></typeparam>
public class ResInfo<T> : ResInfoBase
{
public T asset; //资源
public UnityAction<T> callBack; //待异步资源加载完后,传递资源到外部的委托,加载完毕后清空
public Coroutine coroutine; //记录异步加载时 开启的协同程序,加载完毕后清空
}
接下来就可以声明字典容器
1
2//主要用于存储加载过的资源或者加载中的资源的容器
private Dictionary<string, ResInfoBase> resDic = new Dictionary<string, ResInfoBase>(); -
-
修改异步加载相关逻辑
-
LoadAsync<T>
内-
字典中不存在资源记录时
声明一个
ResInfo<T>
,记录回调函数,开启协同程序进行加载,同时将协同程序也记录下来
并且此时就要将声明出来的ResInfo<T>
记录进字典中(这样可以避免重复异步加载) -
字典中存在资源记录时,通过
ResInfo<T>.asset
是否为null
判断资源是否加载完毕- 资源还没加载完 —— 记录委托,等待资源加载完毕后一起执行
- 资源已经加载完 —— 直接执行回调函数,传入加载好的资源
-
-
ReallyLoadAsync<T>
内协程不再需要额外传入回调函数,可直接调用字典内的
ResInfo<T>.callBack
在加载完毕后,将资源记录到ResInfo<T>.asset
内,
调用委托将资源传递出去后,将消息记录中的回调函数和协程的引用释放掉,避免内存泄漏
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//主要用于存储加载过的资源或者加载中的资源的容器
private Dictionary<string, ResInfoBase> resDic = new Dictionary<string, ResInfoBase>();
// 异步加载资源的方法
public void LoadAsync<T>(string path, UnityAction<T> callBack) where T : UnityEngine.Object
{
//资源的唯一ID,通过路径名_资源类型拼接而成
string resName = $"{path}_{typeof(T).Name}";
ResInfo<T> info;
//不存在消息记录时,说明资源未加载过
if (!resDic.ContainsKey(resName))
{
info = new ResInfo<T>(); //声明一个 资源信息对象
resDic.Add(resName, info); //将资源记录添加到资源内(资源没有加载成功)
info.callBack += callBack; //记录传入的委托函数,一会加载完成了再使用
//通过协同程序去异步加载资源,并记录该协同程序
info.coroutine = MonoManager.Instance.StartCoroutine(ReallyLoadAsync<T>(path));
}
//存在消息记录时,资源已经加载过
else
{
info = resDic[resName] as ResInfo<T>; //从字典中取出资源信息
//资源尚未加载成功
if (info.asset == null)
info.callBack += callBack; //将回调函数添加到委托内,等待加载完毕后一起执行
//资源已经加载完毕
else
callBack?.Invoke(info.asset); //直接执行传入的回调函数
}
}
private IEnumerator ReallyLoadAsync<T>(string path) where T : UnityEngine.Object
{
//异步加载资源
ResourceRequest req = Resources.LoadAsync<T>(path);
//等待资源加载结束后,才会继续执行yield return后面的代码
yield return req;
string resName = $"{path}_{typeof(T).Name}";
//资源加载结束,将资源传到外部的委托函数去进行调用
if (resDic.ContainsKey(resName))
{
ResInfo<T> resInfo = resDic[resName] as ResInfo<T>; //取出资源消息
resInfo.asset = req.asset as T; //将资源记录到资源信息内
resInfo.callBack?.Invoke(resInfo.asset); //将加载出来的资源传递出去
//加载完毕后,这些引用就可以清空,避免引用的占用带来的内存泄露问题
resInfo.callBack = null;
resInfo.coroutine = null;
}
}对于非泛型异步加载方法
LoadAsync
,该方法使用的ResInfo<>
泛型参数默认使用UnityEngine.Object
即可
但是,由于不同泛型参数存储的ResInfo<>
存储的委托是不同的,因此LoadAsync
和LoadAsync<>
混用很可能出现问题
因此,我们规定LoadAsync
和LoadAsync<>
不可混用即可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//主要用于存储加载过的资源或者加载中的资源的容器
private Dictionary<string, ResInfoBase> resDic = new Dictionary<string, ResInfoBase>();
[ ]
public void LoadAsync(string path, Type type, UnityAction<UnityEngine.Object> callBack)
{
//资源的唯一ID,通过路径名_资源类型拼接而成
string resName = $"{path}_{type.Name}";
ResInfo<UnityEngine.Object> info;
//不存在消息记录时,说明资源未加载过
if (!resDic.ContainsKey(resName))
{
info = new ResInfo<UnityEngine.Object>(); //声明一个 资源信息对象
resDic.Add(resName, info); //将资源记录添加到资源内(资源没有加载成功)
info.callBack += callBack; //记录传入的委托函数,一会加载完成了再使用
//通过协同程序去异步加载资源,并记录该协同程序
info.coroutine = MonoManager.Instance.StartCoroutine(ReallyLoadAsync(path, type));
}
//存在消息记录时,资源已经加载过
else
{
info = resDic[resName] as ResInfo<UnityEngine.Object>; //从字典中取出资源信息
//资源尚未加载成功
if (info.asset == null)
info.callBack += callBack; //将回调函数添加到委托内,等待加载完毕后一起执行
//资源已经加载完毕
else
callBack?.Invoke(info.asset); //直接执行传入的回调函数
}
}
private IEnumerator ReallyLoadAsync(string path, Type type)
{
//异步加载资源
ResourceRequest req = Resources.LoadAsync(path, type);
//等待资源加载结束后,才会继续执行yield return后面的代码
yield return req;
string resName = $"{path}_{type.Name}";
//资源加载结束,将资源传到外部的委托函数去进行调用
if (resDic.ContainsKey(resName))
{
ResInfo<UnityEngine.Object> resInfo = resDic[resName] as ResInfo<UnityEngine.Object>; //取出资源消息
resInfo.asset = req.asset; //将资源记录到资源信息内
resInfo.callBack?.Invoke(resInfo.asset); //将加载出来的资源传递出去
//加载完毕后,这些引用就可以清空,避免引用的占用带来的内存泄露问题
resInfo.callBack = null;
resInfo.coroutine = null;
}
} -
-
修改同步加载
Load
相关逻辑修改同步加载
Load
逻辑主要是为了解决:已加载过资源但重复加载的情况,和如何处理先执行异步加载,未加载完时又执行同步加载的情况-
字典中不存在资源记录时
直接同步加载资源记录即可
-
字典中存在资源记录时
- 资源还没加载完 —— 停止协程
- 资源已经加载完 —— 直接使用
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//主要用于存储加载过的资源或者加载中的资源的容器
private Dictionary<string, ResInfoBase> resDic = new Dictionary<string, ResInfoBase>();
private ResManager() { }
// 同步加载资源的方法
public T Load<T>(string path) where T : UnityEngine.Object
{
//资源的唯一ID,通过路径名_资源类型拼接而成
string resName = $"{path}_{typeof(T).Name}";
ResInfo<T> info;
//字典中不存在资源时
if (!resDic.ContainsKey(resName))
{
//直接同步加载,并且记录资源消息到字典中,方便下次直接取出来用
T res = Resources.Load<T>(path);
info = new ResInfo<T>();
info.asset = res;
resDic.Add(resName, info);
return res;
}
else
{
//取出字典的记录
info = resDic[resName] as ResInfo<T>;
//存在异步加载且还在加载中
if (info.asset == null)
{
//停止异步加载,直接采用同步加载的方式加载,并记录
MonoManager.Instance.StopCoroutine(info.coroutine);
T res = Resources.Load<T>(path);
info.asset = res;
//将同步加载出来的内容传递到已存在的回调函数内执行
info.callBack?.Invoke(res);
//执行结束回调,将记录的回调和协程清空,避免内存泄露
info.callBack = null;
info.coroutine = null;
return res;
}
//已经加载过资源
else
{
return info.asset;
}
}
} -
-
修改卸载资源
UnloadAsset
相关逻辑-
字典中存在资源记录时
- 资源还没加载完 —— 记录删除标识,待加载完后真正移除 或者 停止协程,并且移除
- 资源已经加载完 —— 直接卸载,并且移除字典中资源记录
首先为
ResInfo<>
添加一个标识,来表示是否需要移除1
2
3
4
5
6
7public class ResInfo<T> : ResInfoBase
{
public T asset; //资源
public UnityAction<T> callBack; //用于异步资源加载完后 传递资源到外部的委托
public Coroutine coroutine; //用于异步加载时 开启的协同程序
public bool isDel; //是否需要移除
}然后,根据以上思路修改
UnloadAsset
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//主要用于存储加载过的资源或者加载中的资源的容器
private Dictionary<string, ResInfoBase> resDic = new Dictionary<string, ResInfoBase>();
// 指定卸载一个资源
public void UnloadAsset<T>(string path)
{
string resName = $"{path}_{typeof(T).Name}";
//是否存在对应资源
if (resDic.ContainsKey(resName))
{
ResInfo<T> resInfo = resDic[resName] as ResInfo<T>;
//资源已经加载结束
if (resInfo.asset != null)
{
//从字典移除,通过api卸载资源
resDic.Remove(resName);
Resources.UnloadAsset(resInfo.asset as UnityEngine.Object);
}
//资源正在异步加载中
else
{
resInfo.isDel = true; //改变标识,代表待移除
}
}
}对于需要卸载但是资源还没加载完的情况,我们需要在修改标识后,在
ReallyLoadAsync<>
中在加载完后重新执行UnloadAsset<>
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//主要用于存储加载过的资源或者加载中的资源的容器
private Dictionary<string, ResInfoBase> resDic = new Dictionary<string, ResInfoBase>();
private IEnumerator ReallyLoadAsync<T>(string path) where T : UnityEngine.Object
{
//异步加载资源
ResourceRequest req = Resources.LoadAsync<T>(path);
//等待资源加载结束后,才会继续执行yield return后面的代码
yield return req;
string resName = $"{path}_{typeof(T).Name}";
//资源加载结束,将资源传到外部的委托函数去进行调用
if (resDic.ContainsKey(resName))
{
ResInfo<T> resInfo = resDic[resName] as ResInfo<T>; //取出资源消息
resInfo.asset = req.asset as T; //将资源记录到资源信息内
//如果发现需要删除,再去移除资源
if (resInfo.isDel)
{
UnloadAsset<T>(path);
}
else
{
resInfo.callBack?.Invoke(resInfo.asset); //将加载出来的资源传递出去
//加载完毕后,这些引用就可以清空,避免引用的占用带来的内存泄露问题
resInfo.callBack = null;
resInfo.coroutine = null;
}
}
}对于不需要泛型的资源消息,需要额外声明不需要泛型的
UnloadAsset
,同时修改ReallyLoadAsync
的逻辑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//主要用于存储加载过的资源或者加载中的资源的容器
private Dictionary<string, ResInfoBase> resDic = new Dictionary<string, ResInfoBase>();
public void UnloadAsset(string path, Type type)
{
string resName = $"{path}_{type.Name}";
//是否存在对应资源
if (resDic.ContainsKey(resName))
{
ResInfo<UnityEngine.Object> resInfo = resDic[resName] as ResInfo<UnityEngine.Object>;
//资源已经加载结束
if (resInfo.asset != null)
{
//从字典移除,通过api卸载资源
resDic.Remove(resName);
Resources.UnloadAsset(resInfo.asset as UnityEngine.Object);
}
//资源正在异步加载中
else
{
resInfo.isDel = true; //改变标识,代表待移除
}
}
}
private IEnumerator ReallyLoadAsync(string path, Type type)
{
//异步加载资源
ResourceRequest req = Resources.LoadAsync(path, type);
//等待资源加载结束后,才会继续执行yield return后面的代码
yield return req;
string resName = $"{path}_{type.Name}";
//资源加载结束,将资源传到外部的委托函数去进行调用
if (resDic.ContainsKey(resName))
{
ResInfo<UnityEngine.Object> resInfo = resDic[resName] as ResInfo<UnityEngine.Object>; //取出资源消息
resInfo.asset = req.asset; //将资源记录到资源信息内
//如果发现需要删除,再去移除资源
if (resInfo.isDel)
{
UnloadAsset(path, type);
}
else
{
resInfo.callBack?.Invoke(resInfo.asset); //将加载出来的资源传递出去
//加载完毕后,这些引用就可以清空,避免引用的占用带来的内存泄露问题
resInfo.callBack = null;
resInfo.coroutine = null;
}
}
} -
存在的问题
- 在卸载资源时,我们并不知道是否还有地方使用着该资源
-
UnloadUnusedAssets
是卸载没有使用的资源,我们无法判断是否使用
具体代码
1 | using System; |