19.引用计数规则

19.其他_资源加载相关_引用计数规则


19.1 知识点

什么是引用计数规则?

当使用可寻址资源时,Addressables 系统会自动进行内部引用计数管理:

  • 加载资源时,引用计数递增(+1);
  • 释放资源时,引用计数则递减(-1);
  • 当某个可寻址资源的引用计数降为0时,表明该资源已无任何依赖,此时系统将自动卸载该资源以释放内存。

为了防止内存泄漏(即不再需要的内容仍保留在内存中),务必确保对资源的加载与卸载操作成对执行。

注意事项

  1. 即使释放了资源,它也不一定会立即从内存中卸载。若资源所在的AB包仍有其他资源在使用,该AB包就不会被卸载,因此相关资源占用的内存也不会释放。推荐在场景切换时调用 Resources.UnloadUnusedAssets 方法来卸载未使用的资源。

  2. AB包同样具有自身的引用计数机制,并被视为可寻址资源:

    • 当从AB包中加载资源时,其引用计数增加(+1);
    • 当从AB包中卸载资源时,引用计数相应减少(-1);
    • 若AB包引用计数降至0,则表示该包内所有资源均不再使用,系统会将其从内存中卸载。

举例说明:假设一个AB包包含资源a和资源b,其中a被加载并使用了两次,b被加载并使用了一次。随后,即使a资源被卸载两次,由于AB包中尚存在对b资源的引用,故AB包并不会自动卸载a资源或整个包体。这种情况下,可以适时调用 Resources.UnloadUnusedAssets 来手动卸载不再使用的资源(如在切换场景时)。一旦b资源也被卸载,由于AB包无任何引用,整个AB包将会被卸载。

总结
Addressables 通过引用计数机制在内部实现对内存的有效管理。开发者只需确保正确匹配资源的加载与卸载操作,即可充分利用这一机制,高效管理游戏中的资源内存。

举例说明引用计数

我们创建两个一样的资源,然后一个一个的释放他们的资源句柄,观察他们创建出来的对象变化。

注意:使用从AB包中加载模式来加载资源。

// 存储异步操作句柄的列表,用于管理加载的游戏对象
private List<AsyncOperationHandle<GameObject>> list = new List<AsyncOperationHandle<GameObject>>();

// 每帧更新方法
private void Update()
{
    // 当按下空格键时
    if (Input.GetKeyDown(KeyCode.Space))
    {
        print("按下空格异步创建对象并添加句柄");
        // 创建异步操作句柄,加载名为"Cube"的游戏对象
        AsyncOperationHandle<GameObject> handle = Addressables.LoadAssetAsync<GameObject>("Cube");

        // 异步操作完成后的处理
        handle.Completed += (obj) => { Instantiate(obj.Result); };

        // 将异步操作句柄添加到列表中
        list.Add(handle);
    }

    // 当按下Q键时
    if (Input.GetKeyDown(KeyCode.Q))
    {
        print("按下Q释放资源");
        // 如果列表中有异步操作句柄
        if (list.Count > 0)
        {
            // 释放列表中的第一个异步操作句柄的资源
            Addressables.Release(list[0]);
            // 从列表中移除释放的异步操作句柄
            list.RemoveAt(0);
        }
    }
}

按两下空格,生成两个Cube,再按以下Q,这时资源没有丢失,再按一下Q
,引用次数归0,资源和Ab包被卸载,资源丢失。
要全部释放才会释放。

回顾之前写的资源管理器

我们之前写的资源管理器通过自己管理异步加载的返回句柄会让系统自带的引用计数功能不起作用。因为我们不停的在复用存储在字典中一个句柄,通过一个Handle来再次创建对象的,这样我们多次加载一次卸载就会进行卸载,资源将丢失。需要优化。


19.2 知识点代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class Lesson19_其他_资源加载相关_引用计数规则 : MonoBehaviour
{
    void Start()
    {
        #region 知识点一 什么是引用计数规则?

        //当我们通过加载使用可寻址资源时
        //Addressables会在内部帮助我们进行引用计数
        //使用资源时,引用计数+1
        //释放资源时,引用计数-1
        //当可寻址资源的引用为0时,就可以卸载它了

        //为了避免内存泄露(不需要使用的内容残留在内存中)
        //我们要保证加载资源和卸载资源是配对使用的

        //注意:释放的资源不一定立即从内存中卸载
        //在卸载资源所属的AB包之前,不会释放资源使用的内存
        //(比如自己所在的AB包 被别人使用时,这时AB包不会被卸载,所以自己还在内存中)
        //我们可以使用Resources.UnloadUnusedAssets卸载资源(建议在切换场景时调用)

        //AB包也有自己的引用计数(Addressables把它也视为可寻址资源)
        //从AB包中加载资源时,引用计数+1
        //从AB包中卸载资源时,引用计数-1
        //当AB包引用计数为0时,意味着不再使用了,这时会从内存中卸载

        //就比如一个AB包里有a资源b资源
        //使用2次a资源 使用1次b资源
        //这时候卸载了2次a资源 但是AB包还有引用b资源 所以AB包不会自动帮我们卸载a资源 也不会卸载AB包
        //我们可以手动Resources.UnloadUnusedAssets卸载资源(建议在切换场景时调用) 这样a资源就会被卸载
        //假如b资源也被卸载 那么AB包没有引用 整个AB都被卸载

        //总结:Addressables内部会通过引用计数帮助我们管理内存
        //我们只需要保证 加载和卸载资源配对使用即可

        #endregion

        #region 知识点二 举例说明引用计数

        //我们创建两个一样的资源
        //然后一个一个的释放他们的资源句柄
        //观察他们创建出来的对象变化

        //注意:使用从AB包中加载模式来加载资源

        #endregion

        #region 知识点三 回顾之前写的资源管理器

        //我们之前写的资源管理器
        //通过自己管理异步加载的返回句柄会让系统自带的引用计数功能不起作用
        //因为我们不停的在复用存储在字典中一个句柄 通过一个Handle来再次创建对象的
        //这样我们多次加载 一次卸载就会进行卸载 资源将丢失
        //需要优化

        #endregion
    }

    #region 知识点二 举例说明引用计数

    // 存储异步操作句柄的列表,用于管理加载的游戏对象
    private List<AsyncOperationHandle<GameObject>> list = new List<AsyncOperationHandle<GameObject>>();

    // 每帧更新方法
    private void Update()
    {
        // 当按下空格键时
        if (Input.GetKeyDown(KeyCode.Space))
        {
            print("按下空格异步创建对象并添加句柄");
            // 创建异步操作句柄,加载名为"Cube"的游戏对象
            AsyncOperationHandle<GameObject> handle = Addressables.LoadAssetAsync<GameObject>("Cube");

            // 异步操作完成后的处理
            handle.Completed += (obj) => { Instantiate(obj.Result); };

            // 将异步操作句柄添加到列表中
            list.Add(handle);
        }

        // 当按下Q键时
        if (Input.GetKeyDown(KeyCode.Q))
        {
            print("按下Q释放资源");
            // 如果列表中有异步操作句柄
            if (list.Count > 0)
            {
                // 释放列表中的第一个异步操作句柄的资源
                Addressables.Release(list[0]);
                // 从列表中移除释放的异步操作句柄
                list.RemoveAt(0);
            }
        }
    }

    #endregion
}

19.3 练习题

为之前的资源管理器添加引用计数功能

思路:自己实现计数,自定义类

实现AddressablesInfo类,包括句柄,引用次数

// 可寻址资源信息
public class AddressablesInfo
{
    // 记录异步操作句柄
    public AsyncOperationHandle handle;
    // 记录引用计数
    public uint count;

    public AddressablesInfo(AsyncOperationHandle handle)
    {
        this.handle = handle;
        count += 1;
    }
}

管理器字典类型改成Dictionary<string, AddressablesInfo>

// 有一个容器帮助我们存储异步加载的返回值
public Dictionary<string, AddressablesInfo> resDic = new Dictionary<string, AddressablesInfo>();

异步加载资源时new出AddressablesInfo类并添加到字典

AddressablesInfo info = new AddressablesInfo(handle);
resDic.Add(keyName, info);

异步加载资源时判断如果存在键就添加引用计数

// 获取异步加载返回的操作内容
handle = resDic[keyName].handle.Convert<T>();
// 要使用资源了,引用计数+1
resDic[keyName].count += 1;

释放资源时如果引用计数为0才真正释放

// 释放资源的方法 
public void Release<T>(string name)
{
    // 由于存在同名不同类型资源的区分加载,所以我们通过名字和类型拼接作为 key
    string keyName = name + "_" + typeof(T).Name;
    if (resDic.ContainsKey(keyName))
    {
        // 释放时,引用计数-1
        resDic[keyName].count -= 1;
        // 如果引用计数为0,才真正释放
        if (resDic[keyName].count == 0)
        {
            // 取出对象,移除资源,并且从字典里面移除
            AsyncOperationHandle<T> handle = resDic[keyName].handle.Convert<T>();
            Addressables.Release(handle);
            resDic.Remove(keyName);
        }
    }
}

清空资源时改成.handle

// 清空资源
public void Clear()
{
    foreach (var item in resDic.Values)
    {
        Addressables.Release(item.handle);
    }

    resDic.Clear();
    AssetBundle.UnloadAllAssetBundles(true);
    Resources.UnloadUnusedAssets();
    GC.Collect();
}

进行测试

void Update()
{
    // 创建对象,记录异步操作句柄
    if (Input.GetKeyDown(KeyCode.Space))
    {
        AddressablesManager.Instance.LoadAssetAsync<GameObject>("Cube", (obj) => { Instantiate(obj.Result); });
    }

    // 从创建对象中释放异步操作句柄资源
    if (Input.GetKeyDown(KeyCode.Q))
    {
        AddressablesManager.Instance.Release<GameObject>("Cube");
    }
}

19.4 练习题代码

AddressablesInfo

using UnityEngine.ResourceManagement.AsyncOperations;

namespace BaseFramework
{
    //可寻址资源 信息
    public class AddressablesInfo
    {
        //记录 异步操作句柄
        public AsyncOperationHandle handle;
        //记录 引用计数
        public uint count;

        public AddressablesInfo(AsyncOperationHandle handle)
        {
            this.handle = handle;
            count += 1;
        }
    }
}

AddressablesManager

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

namespace BaseFramework
{
    public class AddressablesManager : BaseSingletonInMonoBehaviour<AddressablesManager>
    {
        //有一个容器 帮助我们存储 异步加载的返回值
        public Dictionary<string, AddressablesInfo> resDic = new Dictionary<string, AddressablesInfo>();

        //异步加载资源的方法
        public void LoadAssetAsync<T>(string name, Action<AsyncOperationHandle<T>> callBack)
        {
            //由于存在同名 不同类型资源的区分加载
            //所以我们通过名字和类型拼接作为 key
            string keyName = name + "_" + typeof(T).Name;
            AsyncOperationHandle<T> handle;
            //如果已经加载过该资源
            if (resDic.ContainsKey(keyName))
            {
                //获取异步加载返回的操作内容
                handle = resDic[keyName].handle.Convert<T>();
                //要使用资源了 那么引用计数+1
                resDic[keyName].count += 1;
                //判断 这个异步加载是否结束
                if (handle.IsDone)
                {
                    //如果成功 就不需要异步了 直接相当于同步调用了 这个委托函数 传入对应的返回值
                    callBack(handle);
                }
                //还没有加载完成
                else
                {
                    //如果这个时候 还没有异步加载完成 那么我们只需要 告诉它 完成时做什么就行了
                    handle.Completed += (obj) =>
                    {
                        if (obj.Status == AsyncOperationStatus.Succeeded)
                            callBack(obj);
                    };
                }

                return;
            }

            //如果没有加载过该资源
            //直接进行异步加载 并且记录
            handle = Addressables.LoadAssetAsync<T>(name);
            handle.Completed += (obj) =>
            {
                if (obj.Status == AsyncOperationStatus.Succeeded)
                    callBack(obj);
                else
                {
                    Debug.LogWarning(keyName + "资源加载失败");
                    if (resDic.ContainsKey(keyName))
                        resDic.Remove(keyName);
                }
            };
            AddressablesInfo info = new AddressablesInfo(handle);
            resDic.Add(keyName, info);
        }

        //释放资源的方法 
        public void Release<T>(string name)
        {
            //由于存在同名 不同类型资源的区分加载
            //所以我们通过名字和类型拼接作为 key
            string keyName = name + "_" + typeof(T).Name;
            if (resDic.ContainsKey(keyName))
            {
                //释放时 引用计数-1
                resDic[keyName].count -= 1;
                //如果引用计数为0  才真正的释放
                if (resDic[keyName].count == 0)
                {
                    //取出对象 移除资源 并且从字典里面移除
                    AsyncOperationHandle<T> handle = resDic[keyName].handle.Convert<T>();
                    Addressables.Release(handle);
                    resDic.Remove(keyName);
                }
            }
        }

        //异步加载多个资源 或者 加载指定资源
        public void LoadAssetAsync<T>(Addressables.MergeMode mode, Action<T> callBack, params string[] keys)
        {
            //1.构建一个keyName  之后用于存入到字典中
            List<string> list = new List<string>(keys);
            string keyName = "";
            foreach (string key in list)
                keyName += key + "_";
            keyName += typeof(T).Name;
            //2.判断是否存在已经加载过的内容 
            //存在做什么
            AsyncOperationHandle<IList<T>> handle;
            if (resDic.ContainsKey(keyName))
            {
                handle = resDic[keyName].handle.Convert<IList<T>>();
                //要使用资源了 那么引用计数+1
                resDic[keyName].count += 1;
                //异步加载是否结束
                if (handle.IsDone)
                {
                    foreach (T item in handle.Result)
                        callBack(item);
                }
                else
                {
                    handle.Completed += (obj) =>
                    {
                        //加载成功才调用外部传入的委托函数
                        if (obj.Status == AsyncOperationStatus.Succeeded)
                        {
                            foreach (T item in handle.Result)
                                callBack(item);
                        }
                    };
                }

                return;
            }

            //不存在做什么
            handle = Addressables.LoadAssetsAsync<T>(list, callBack, mode);
            handle.Completed += (obj) =>
            {
                if (obj.Status == AsyncOperationStatus.Failed)
                {
                    Debug.LogError("资源加载失败" + keyName);
                    if (resDic.ContainsKey(keyName))
                        resDic.Remove(keyName);
                }
            };
            AddressablesInfo info = new AddressablesInfo(handle);
            resDic.Add(keyName, info);
        }

        public void LoadAssetAsync<T>(Addressables.MergeMode mode, Action<AsyncOperationHandle<IList<T>>> callBack,
            params string[] keys)
        {
        }

        public void Release<T>(params string[] keys)
        {
            //1.构建一个keyName  之后用于存入到字典中
            List<string> list = new List<string>(keys);
            string keyName = "";
            foreach (string key in list)
                keyName += key + "_";
            keyName += typeof(T).Name;

            if (resDic.ContainsKey(keyName))
            {
                resDic[keyName].count -= 1;
                if (resDic[keyName].count == 0)
                {
                    //取出字典里面的对象
                    AsyncOperationHandle<IList<T>> handle = resDic[keyName].handle.Convert<IList<T>>();
                    Addressables.Release(handle);
                    resDic.Remove(keyName);
                }
            }
        }

        //清空资源
        public void Clear()
        {
            foreach (var item in resDic.Values)
            {
                Addressables.Release(item.handle);
            }

            resDic.Clear();
            AssetBundle.UnloadAllAssetBundles(true);
            Resources.UnloadUnusedAssets();
            GC.Collect();
        }
    }
}

Lesson19_练习题

using System.Collections;
using System.Collections.Generic;
using BaseFramework;
using UnityEngine;

public class Lesson19_练习题 : MonoBehaviour
{
    void Update()
    {
        //创建对象 记录异步操作句柄
        if (Input.GetKeyDown(KeyCode.Space))
        {
            AddressablesManager.Instance.LoadAssetAsync<GameObject>("Cube", (obj) => { Instantiate(obj.Result); });
        }

        //从创建对象中 释放异步操作句柄资源
        if (Input.GetKeyDown(KeyCode.Q))
        {
            AddressablesManager.Instance.Release<GameObject>("Cube");
        }
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏