UFL5-3——添加引用计数的Resource资源加载模块

通过引用计数判断资源是否使用的问题

  1. 在卸载资源时,我们并不知道是否还有地方使用着该资源
  2. UnloadUnusedAssets​是卸载没有使用的资源,我们无法判断是否使用

我们可以使用引用计数来解决上述问题

引用计数是一种内存管理技术,用于跟踪资源被引用的次数
我们通过一个整形int​变量来记录资源的使用次数
当有对象引用该资源时,计数器会增加;当对象不再引用该资源时,计数器会减少

ResManager​中加入引用计数功能

  1. ResInfo​类加入引用计数成员变量和方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public 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了,请检查使用或者卸载是否配对执行");
    }
    }
  2. 使用资源时引用计数加一

    每当调用加载资源时(无论是同步加载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
    92
    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;
    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); //直接执行传入的回调函数
    }
    }
  3. 不使用资源时引用计数减一

    每当调用卸载资源时,引用计数就减一

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public 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(); //减少引用计数
    //...
    }
    }
  4. 处理异步回调问题,某一个异步加载决定不使用资源了应该移除该异步对应的回调函数的记录,而不是直接移除资源

  5. 修改移除资源函数逻辑,引用计数为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
    50
    public 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​的条件需要从 移除标识 改为 资源引用计数归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
    50
    51
    52
    53
    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);
    }
    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
    9
    public 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
    7
    ResManager.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"));

    输出:image

  6. 考虑资源频繁移除问题,加入马上移除bool​标签

    将原来作为删除标识的ResInfo.isDel​(在上述的修改下,原来的作用已经废弃),改为判断是否在引用计数归0后就直接移除资源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public 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
    104
    public 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;
    }
    }
    }
  7. 修改移除不使用资源函数UnloadUnusedAssets​逻辑,释放时引用计数为0的记录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public 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();
    }
  8. 为移除资源方法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
    107
    public 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
2
3
4
5
6
7
8
9
10
ResManager.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", false, TestFun);
Debug.Log(ResManager.Instance.GetRefCount<GameObject>("Test"));

ResManager.Instance.UnloadAsset<GameObject>("Test", false, TestFun);
Debug.Log(ResManager.Instance.GetRefCount<GameObject>("Test"));

输出:image

注意事项

  1. 加入引用计数的ResManager​,我们在使用资源时就需要有用就有删
    当使用某个资源的对象移除时,一定要记得调用移除方法

  2. 如果觉得卸载资源的功能麻烦,也完全可以不使用卸载的相关方法
    加载相关逻辑不会有任何影响,和以前直接使用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
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 资源消息基类
/// </summary>
public abstract class ResInfoBase
{
public int refCount; //引用计数
}

/// <summary>
/// 资源消息对象 主要用于存储资源消息,异步加载委托消息,异步加载协程消息
/// </summary>
/// <typeparam name="T"></typeparam>
public class ResInfo<T> : ResInfoBase
{
public T asset; //资源
public UnityAction<T> callBack; //用于异步资源加载完后 传递资源到外部的委托
public Coroutine coroutine; //用于异步加载时 开启的协同程序
public bool isDel; //当引用计数归0时,是否需要立刻移除


public void AddRefCount()
{
++refCount;
}

public void SubRefCount()
{
--refCount;
if (refCount < 0)
Debug.LogError("引用计数小于0了,请检查使用或者卸载是否配对执行");
}
}

/// <summary>
/// Resources 资源加载模块管理器
/// </summary>
public class ResManager : BaseManager<ResManager>
{
//主要用于存储加载过的资源或者加载中的资源的容器
private Dictionary<string, ResInfoBase> resDic = new Dictionary<string, ResInfoBase>();

private ResManager() { }

/// <summary>
/// 同步加载资源的方法
/// </summary>
/// <typeparam name="T">资源类型</typeparam>
/// <param name="path"></param>
/// <returns></returns>
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;
info.AddRefCount(); //增加引用计数
resDic.Add(resName, info);
return res;
}
else
{
//取出字典的记录
info = resDic[resName] as ResInfo<T>;
info.AddRefCount(); //增加引用计数
//存在异步加载且还在加载中
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;
}
}
}

/// <summary>
/// 异步加载资源的方法
/// </summary>
/// <typeparam name="T">资源类型</typeparam>
/// <param name="path">资源路径(Resources下的)</param>
/// <param name="callBack">加载结束后的回调函数</param>
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); //直接执行传入的回调函数
}
}

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;
}
}
}

/// <summary>
/// 异步加载资源的方法
/// </summary>
/// <param name="path">资源路径(Resources下的)</param>
/// <param name="type">资源类型</param>
/// <param name="callBack">加载结束后的回调函数</param>
[Obsolete("注意:建议使用泛型方法,如果需要使用Type加载,请不要和泛型加载混用去加载同类型同名资源")]
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); //直接执行传入的回调函数
}
}

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;
}
}
}

/// <summary>
/// 指定卸载一个资源
/// </summary>
/// <typeparam name="T">要卸载的资源类型</typeparam>
/// <param name="path">要卸载的资源的路径</param>
/// <param name="isDel">引用计数为0时,是否立刻移除</param>
/// <param name="callBack">要移除的回调函数(可选,适用于尚未加载完毕时移除回调)</param>
/// <param name="isSub">引用计数是否减一</param>
public 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;
}
}
}

/// <summary>
/// 指定卸载一个资源
/// </summary>
/// <param name="path">要卸载的资源的路径</param>
/// <param name="type">要卸载的资源类型</param>
/// <param name="isDel">引用计数为0时,是否立刻移除</param>
/// <param name="callBack">要移除的回调函数(可选,适用于尚未加载完毕时移除回调)</param>
/// <param name="isSub">引用计数是否减一</param>
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;
}
}
}

/// <summary>
/// 异步卸载对应没有使用的Resources相关资源
/// </summary>
/// <param name="callBack">回调函数</param>
public 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();
}

/// <summary>
/// 获取当前某个资源的引用计数
/// </summary>
/// <typeparam name="T">资源的类型</typeparam>
/// <param name="path">资源的路径</param>
/// <returns>引用计数(若传入无效资源则返回0)</returns>
public int GetRefCount<T>(string path)
{
string resName = $"{path}_{typeof(T).Name}";
if (resDic.ContainsKey(resName))
{
return resDic[resName].refCount;
}
return 0;
}

/// <summary>
/// 清空资源记录,同时释放掉未使用的资源
/// </summary>
/// <param name="callBack">资源释放完毕后要执行的方法</param>
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();
}
}