UFL5-3——添加引用计数的Resource资源加载模块
UFL5-3——添加引用计数的Resource资源加载模块
通过引用计数判断资源是否使用的问题
- 在卸载资源时,我们并不知道是否还有地方使用着该资源
-
UnloadUnusedAssets
是卸载没有使用的资源,我们无法判断是否使用
我们可以使用引用计数来解决上述问题
引用计数是一种内存管理技术,用于跟踪资源被引用的次数
我们通过一个整形int
变量来记录资源的使用次数
当有对象引用该资源时,计数器会增加;当对象不再引用该资源时,计数器会减少
向ResManager
中加入引用计数功能
-
为
ResInfo
类加入引用计数成员变量和方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class ResInfo<T> : ResInfoBase
{
public T asset; //资源
public UnityAction<T> callBack; //用于异步资源加载完后 传递资源到外部的委托
public Coroutine coroutine; //用于异步加载时 开启的协同程序
public bool isDel; //是否需要移除
public int refCount; //引用计数
public void AddRefCount()
{
++refCount;
}
public void SubRedCount()
{
--refCount;
if (refCount < 0)
Debug.LogError("引用计数小于0了,请检查使用或者卸载是否配对执行");
}
} -
使用资源时引用计数加一
每当调用加载资源时(无论是同步加载
Load
还是异步加载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
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
92public 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;
info.AddRefCount(); //增加引用计数
resDic.Add(resName, info);
return res;
}
else
{
//取出字典的记录
info = resDic[resName] as ResInfo<T>;
info.AddRefCount(); //增加引用计数
//存在异步加载且还在加载中
if (info.asset == null)
{
//...
return res;
}
//已经加载过资源
else
{
return info.asset;
}
}
}
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>(); //声明一个 资源信息对象
info.AddRefCount(); //增加引用计数
resDic.Add(resName, info); //将资源记录添加到资源内(资源没有加载成功)
info.callBack += callBack; //记录传入的委托函数,一会加载完成了再使用
//通过协同程序去异步加载资源,并记录该协同程序
info.coroutine = MonoManager.Instance.StartCoroutine(ReallyLoadAsync<T>(path));
}
//存在消息记录时,资源已经加载过
else
{
info = resDic[resName] as ResInfo<T>; //从字典中取出资源信息
info.AddRefCount(); //增加引用计数
//资源尚未加载成功
if (info.asset == null)
info.callBack += callBack; //将回调函数添加到委托内,等待加载完毕后一起执行
//资源已经加载完毕
else
callBack?.Invoke(info.asset); //直接执行传入的回调函数
}
}
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>(); //声明一个 资源信息对象
info.AddRefCount(); //增加引用计数
resDic.Add(resName, info); //将资源记录添加到资源内(资源没有加载成功)
info.callBack += callBack; //记录传入的委托函数,一会加载完成了再使用
//通过协同程序去异步加载资源,并记录该协同程序
info.coroutine = MonoManager.Instance.StartCoroutine(ReallyLoadAsync(path, type));
}
//存在消息记录时,资源已经加载过
else
{
info = resDic[resName] as ResInfo<UnityEngine.Object>; //从字典中取出资源信息
info.AddRefCount(); //增加引用计数
//资源尚未加载成功
if (info.asset == null)
info.callBack += callBack; //将回调函数添加到委托内,等待加载完毕后一起执行
//资源已经加载完毕
else
callBack?.Invoke(info.asset); //直接执行传入的回调函数
}
} -
不使用资源时引用计数减一
每当调用卸载资源时,引用计数就减一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public void UnloadAsset<T>(string path)
{
string resName = $"{path}_{typeof(T).Name}";
//是否存在对应资源
if (resDic.ContainsKey(resName))
{
ResInfo<T> resInfo = resDic[resName] as ResInfo<T>;
resInfo.SubRefCount(); //减少引用计数
//...
}
}
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>;
resInfo.SubRefCount(); //减少引用计数
//...
}
} -
处理异步回调问题,某一个异步加载决定不使用资源了应该移除该异步对应的回调函数的记录,而不是直接移除资源
-
修改移除资源函数逻辑,引用计数为0时才真正移除资源
修改移除资源方法
UnloadAsset
的逻辑,添加一个可以移除回调的参数,便于在资源未加载完毕时移除回调函数,
同时,只有检查到资源已经加载出来且资源引用计数归0时,才需要移除资源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
50public void UnloadAsset<T>(string path, UnityAction<T> callBack = null)
{
string resName = $"{path}_{typeof(T).Name}";
//是否存在对应资源
if (resDic.ContainsKey(resName))
{
ResInfo<T> resInfo = resDic[resName] as ResInfo<T>;
resInfo.SubRefCount(); //减少引用计数
//资源已经加载结束,且引用计数归0
if (resInfo.asset != null && resInfo.refCount == 0)
{
//从字典移除,通过api卸载资源
resDic.Remove(resName);
Resources.UnloadAsset(resInfo.asset as UnityEngine.Object);
}
//资源正在异步加载中
else if (resInfo.asset == null)
{
if (callBack != null)
resInfo.callBack -= callBack;
}
}
}
// 指定卸载一个资源
public void UnloadAsset(string path, Type type, UnityAction<UnityEngine.Object> callBack = null)
{
string resName = $"{path}_{type.Name}";
//是否存在对应资源
if (resDic.ContainsKey(resName))
{
ResInfo<UnityEngine.Object> resInfo = resDic[resName] as ResInfo<UnityEngine.Object>;
resInfo.SubRefCount(); //减少引用计数
//资源已经加载结束
if (resInfo.asset != null && resInfo.refCount == 0)
{
//从字典移除,通过api卸载资源
resDic.Remove(resName);
Resources.UnloadAsset(resInfo.asset as UnityEngine.Object);
}
//资源正在异步加载中
else if (resInfo.asset == null)
{
//当异步加载不想使用时,我们应该移除它的回调记录,而不是直接去卸载资源
if (callBack != null)
resInfo.callBack -= callBack;
}
}
}当资源异步加载完毕但是发现资源引用计数已经归0的时候,需要直接移除资源,
为此,异步加载资源的协程ReallyLoadAsync
调用移除资源方法UnloadAsset
的条件需要从 移除标识 改为 资源引用计数归01
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
53private 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; //将资源记录到资源信息内
//如果发现引用计数归0,说明需要移除该资源
if (resInfo.refCount == 0)
{
UnloadAsset<T>(path);
}
else
{
resInfo.callBack?.Invoke(resInfo.asset); //将加载出来的资源传递出去
//加载完毕后,这些引用就可以清空,避免引用的占用带来的内存泄露问题
resInfo.callBack = null;
resInfo.coroutine = null;
}
}
}
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; //将资源记录到资源信息内
//如果发现引用计数归0,说明需要移除该资源
if (resInfo.refCount == 0)
{
UnloadAsset(path, type);
}
else
{
resInfo.callBack?.Invoke(resInfo.asset); //将加载出来的资源传递出去
//加载完毕后,这些引用就可以清空,避免引用的占用带来的内存泄露问题
resInfo.callBack = null;
resInfo.coroutine = null;
}
}
}针对以上的内容进行测试,首先需要声明一个查看引用计数的方法
1
2
3
4
5
6
7
8
9public int GetRefCount<T>(string path)
{
string resName = $"{path}_{typeof(T).Name}";
if (resDic.ContainsKey(resName))
{
return (resDic[resName] as ResInfo<T>).refCount;
}
return 0;
}然后调用加载和卸载方法,观察引用计数变化
1
2
3
4
5
6
7ResManager.Instance.LoadAsync<GameObject>("Test", TestFun);
Debug.Log(ResManager.Instance.GetRefCount<GameObject>("Test"));
ResManager.Instance.LoadAsync<GameObject>("Test", TestFun);
Debug.Log(ResManager.Instance.GetRefCount<GameObject>("Test"));
ResManager.Instance.UnloadAsset<GameObject>("Test", TestFun);
Debug.Log(ResManager.Instance.GetRefCount<GameObject>("Test"));输出:
-
考虑资源频繁移除问题,加入马上移除
bool
标签将原来作为删除标识的
ResInfo.isDel
(在上述的修改下,原来的作用已经废弃),改为判断是否在引用计数归0后就直接移除资源1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class ResInfo<T> : ResInfoBase
{
public T asset; //资源
public UnityAction<T> callBack; //用于异步资源加载完后 传递资源到外部的委托
public Coroutine coroutine; //用于异步加载时 开启的协同程序
public bool isDel; //当引用计数归0时,是否需要立刻移除
public int refCount; //引用计数
public void AddRefCount()
{
++refCount;
}
public void SubRefCount()
{
--refCount;
if (refCount < 0)
Debug.LogError("引用计数小于0了,请检查使用或者卸载是否配对执行");
}
}因此,在移除资源方法
UnloadAsset
中,添加一个是否在引用计数归0后就移除资源的参数,每当调用该方法,就记录传入的值
同时根据ResInfo.isDel
是否为true
,决定是否在引用计数归0后就移除资源在
ReallyLoadAsync
中调用的UnloadAsset
,需要传入ResInfo
原本的isDel
的值,保持原有状态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
104public void UnloadAsset<T>(string path, bool isDel = false, UnityAction<T> callBack = null)
{
string resName = $"{path}_{typeof(T).Name}";
//是否存在对应资源
if (resDic.ContainsKey(resName))
{
ResInfo<T> resInfo = resDic[resName] as ResInfo<T>;
resInfo.SubRefCount(); //减少引用计数
resInfo.isDel = isDel; //记录引用计数为0时,是否立刻移除
//资源已经加载结束,且引用计数归0
if (resInfo.asset != null && resInfo.refCount == 0 && resInfo.isDel == true)
{
//从字典移除,通过api卸载资源
resDic.Remove(resName);
Resources.UnloadAsset(resInfo.asset as UnityEngine.Object);
}
//资源正在异步加载中
else if (resInfo.asset == null)
{
if (callBack != null)
resInfo.callBack -= callBack;
}
}
}
public void UnloadAsset(string path, Type type, bool isDel = false, UnityAction<UnityEngine.Object> callBack = null)
{
string resName = $"{path}_{type.Name}";
//是否存在对应资源
if (resDic.ContainsKey(resName))
{
ResInfo<UnityEngine.Object> resInfo = resDic[resName] as ResInfo<UnityEngine.Object>;
resInfo.SubRefCount(); //减少引用计数
resInfo.isDel = isDel; //记录引用计数为0时,是否立刻移除
//资源已经加载结束
if (resInfo.asset != null && resInfo.refCount == 0 && resInfo.isDel == true)
{
//从字典移除,通过api卸载资源
resDic.Remove(resName);
Resources.UnloadAsset(resInfo.asset as UnityEngine.Object);
}
//资源正在异步加载中
else if (resInfo.asset == null)
{
//当异步加载不想使用时,我们应该移除它的回调记录,而不是直接去卸载资源
if (callBack != null)
resInfo.callBack -= callBack;
}
}
}
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; //将资源记录到资源信息内
//如果发现引用计数归0,说明需要移除该资源
if (resInfo.refCount == 0)
{
UnloadAsset<T>(path, resInfo.isDel);
}
else
{
resInfo.callBack?.Invoke(resInfo.asset); //将加载出来的资源传递出去
//加载完毕后,这些引用就可以清空,避免引用的占用带来的内存泄露问题
resInfo.callBack = null;
resInfo.coroutine = null;
}
}
}
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; //将资源记录到资源信息内
//如果发现引用计数归0,说明需要移除该资源
if (resInfo.refCount == 0)
{
UnloadAsset(path, type, resInfo.isDel);
}
else
{
resInfo.callBack?.Invoke(resInfo.asset); //将加载出来的资源传递出去
//加载完毕后,这些引用就可以清空,避免引用的占用带来的内存泄露问题
resInfo.callBack = null;
resInfo.coroutine = null;
}
}
} -
修改移除不使用资源函数
UnloadUnusedAssets
逻辑,释放时引用计数为0的记录1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public void UnloadUnusedAssets(UnityAction callBack)
{
MonoManager.Instance.StartCoroutine(ReallyUnloadUnusedAssets(callBack));
}
private IEnumerator ReallyUnloadUnusedAssets(UnityAction callBack)
{
//在真正移除不使用的资源之前,应该将字典内引用计数归0的资源消息且尚未移除记录的移除掉
List<string> list = new List<string>();
foreach (string path in resDic.Keys)
{
if (resDic[path].refCount == 0)
list.Add(path);
}
foreach (string path in list)
{
resDic.Remove(path);
}
AsyncOperation ao = Resources.UnloadUnusedAssets();
yield return ao;
callBack?.Invoke();
} -
为移除资源方法
UnloadAsset
添加一个是否减少引用计数的参数,用于内部的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
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
107public void UnloadAsset<T>(string path, bool isDel = false, UnityAction<T> callBack = null, bool isSub = true)
{
string resName = $"{path}_{typeof(T).Name}";
//是否存在对应资源
if (resDic.ContainsKey(resName))
{
ResInfo<T> resInfo = resDic[resName] as ResInfo<T>;
if (isSub)
resInfo.SubRefCount(); //减少引用计数
resInfo.isDel = isDel; //记录引用计数为0时,是否立刻移除
//资源已经加载结束,且引用计数归0
if (resInfo.asset != null && resInfo.refCount == 0 && resInfo.isDel == true)
{
//从字典移除,通过api卸载资源
resDic.Remove(resName);
Resources.UnloadAsset(resInfo.asset as UnityEngine.Object);
}
//资源正在异步加载中
else if (resInfo.asset == null)
{
if (callBack != null)
resInfo.callBack -= callBack;
}
}
}
// 指定卸载一个资源
public void UnloadAsset(string path, Type type, bool isDel = false, UnityAction<UnityEngine.Object> callBack = null, bool isSub = true)
{
string resName = $"{path}_{type.Name}";
//是否存在对应资源
if (resDic.ContainsKey(resName))
{
ResInfo<UnityEngine.Object> resInfo = resDic[resName] as ResInfo<UnityEngine.Object>;
if (isSub)
resInfo.SubRefCount(); //减少引用计数
resInfo.isDel = isDel; //记录引用计数为0时,是否立刻移除
//资源已经加载结束
if (resInfo.asset != null && resInfo.refCount == 0 && resInfo.isDel == true)
{
//从字典移除,通过api卸载资源
resDic.Remove(resName);
Resources.UnloadAsset(resInfo.asset as UnityEngine.Object);
}
//资源正在异步加载中
else if (resInfo.asset == null)
{
//当异步加载不想使用时,我们应该移除它的回调记录,而不是直接去卸载资源
if (callBack != null)
resInfo.callBack -= callBack;
}
}
}
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; //将资源记录到资源信息内
//如果发现引用计数归0,说明需要移除该资源
if (resInfo.refCount == 0)
{
UnloadAsset<T>(path, resInfo.isDel, null, false);
}
else
{
resInfo.callBack?.Invoke(resInfo.asset); //将加载出来的资源传递出去
//加载完毕后,这些引用就可以清空,避免引用的占用带来的内存泄露问题
resInfo.callBack = null;
resInfo.coroutine = null;
}
}
}
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; //将资源记录到资源信息内
//如果发现引用计数归0,说明需要移除该资源
if (resInfo.refCount == 0)
{
UnloadAsset(path, type, resInfo.isDel, null, false);
}
else
{
resInfo.callBack?.Invoke(resInfo.asset); //将加载出来的资源传递出去
//加载完毕后,这些引用就可以清空,避免引用的占用带来的内存泄露问题
resInfo.callBack = null;
resInfo.coroutine = null;
}
}
}
使用示例
同时加载和卸载两次同一资源,观察引用计数的变化
1 | ResManager.Instance.LoadAsync<GameObject>("Test", TestFun); |
输出:
注意事项
-
加入引用计数的
ResManager
,我们在使用资源时就需要有用就有删
当使用某个资源的对象移除时,一定要记得调用移除方法 -
如果觉得卸载资源的功能麻烦,也完全可以不使用卸载的相关方法
加载相关逻辑不会有任何影响,和以前直接使用Resources
的用法几乎一样
只需要再添加一个主动清空字典的方法即可1
2
3
4
5
6
7
8
9
10
11
12
13
14// 清空资源记录,同时释放掉未使用的资源
public void ClearDic(UnityAction callBack)
{
resDic.Clear();
MonoManager.Instance.StartCoroutine(ReallyClearDic(callBack));
}
private IEnumerator ReallyClearDic (UnityAction callBack)
{
resDic.Clear();
AsyncOperation ao = Resources.UnloadUnusedAssets();
yield return ao;
callBack?.Invoke();
}
具体代码
1 | using System; |