19.Resources异步加载

19.Resources资源动态加载-Resources异步加载


19.1 知识点

Resources异步加载是什么?

  • 在上节课学习的同步加载中,如果我们加载过大的资源可能会造成程序卡顿。
  • 卡顿的原因是从硬盘上把数据读取到内存中是需要进行计算的,越大的资源耗时越长,就会造成掉帧卡顿。
  • Resources异步加载就是内部新开一个线程进行资源加载,不会造成主线程卡顿。
  • 其基本工作流程是主线程通知新线程要加载哪些资源,新线程加载完了放到公共内存区域,主线程检测到加载完毕取出来使用。
  • 异步加载的缺点是:同步加载加载完直接拿来用,异步加载要等待加载完毕才能用
  • 异步加载的优点是:能解决卡顿问题。

Resources异步加载方法

Resources.LoadAsync<T>静态方法 异步加载

  • 注意:异步加载 不能马上得到加载的资源 至少要等一帧
//Resources中的LoadAsync方法 通过异步加载中的完成事件监听 使用加载的资源
//异步加载存储在 Resources 文件夹中的 path 处的资源。
//这句代码 你可以理解 Unity 在内部 就会去开一个线程进行资源下载
//异步加载会有一个ResourceRequest类的返回值 用个变量接一下
//ResourceRequest类是来自 Resources 包的异步加载请求。里面包含异步加载是否完成,进度这些变量,辅助做逻辑
ResourceRequest resourceRequest = Resources.LoadAsync<Texture>("Tex/TestJPG");

异步加载结束后做逻辑

方法一:添加异步加载完成回调函数

ResourceRequest.completed 异步加载完成回调
void Start()
{
    ResourceRequest resourceRequest = Resources.LoadAsync<Texture>("Tex/TestJPG");

    //怎么在异步加载结束后做逻辑了?主要有两种方法

    //使用ResourceRequest这个类异步加载完成的回调
    //ResourceRequest这个类存在完成异步加载的事件回调
    //当异步加载结束时 马上进行一个资源下载结束的一个事件函数监听回调
    //注意:completed这个事件有一个AsyncOperation类型的参数
    //AsyncOperation类型是ResourceRequest类型的父类 一般是要把AsyncOperation类型as成ResourceRequest类型再获取ResourceRequest相关
    resourceRequest.completed += AsynLoadOver;
    //打印帧数
    print("直接异步加载"+Time.frameCount);
    //这个 刚刚执行了异步加载的 执行代码 资源还没有加载完毕 不能直接使用资源 
    //一定要等加载结束过后 才能使用
    //resourceRequest.asset //错误用法
    #endregion
}
ResourceRequest.asset 异步加载得到的资源
//异步加载结束事件监听回调
private void AsynLoadOver(AsyncOperation asyncOperationq)
{
    print("异步加载结束");
    //asset 是AsyncOperation类中的资源对象 加载完毕过后 就能够得到它
    tex = (asyncOperationq as ResourceRequest).asset as Texture;
    //得到后可以在OnGUI中画出这个图片

    //打印帧数
    print("直接异步加载" + Time.frameCount);
}

方法二:使用协程函数异步加载

开启异步加载协程函数
//通过协程 异步加载的资源并写加载完之后的逻辑
StartCoroutine(CoroutineAsynLoad());
异步加载协程内使用ResourceRequest对象做逻辑
yield return resourceRequest 资源加载完毕再执行后面的代码
ResourceRequest.isDone 判断资源是否加载结束
ResourceRequest.progress 当前加载进度
异步加载协程函数代码
//协程函数异步加载
IEnumerator CoroutineAsynLoad()
{
    //开启协程就异步加载
    ResourceRequest resourceRequest = Resources.LoadAsync<Texture>("Tex/TestJPG");
    print("协程函数异步加载" + Time.frameCount);

    //方法一:直接 yield returnResourceRequest类对象 加载完再写逻辑
    //当yield return的是ResourceRequest这个类的时候 
    //Unity的协程管理器就自己知道这个类的返回值 意味着你在异步加载资源 
    yield return resourceRequest;
    //Unity 会自己判断 该资源是否加载完毕了 加载完毕过后 才会继续执行后面的代码
    print("协程函数异步加载结束" + Time.frameCount);

    //方法二:通过ResourceRequest中的变量判断当前加载情况 加载完再写逻辑
    //ResourceRequest中isDone变量 判断资源是否加载结束
    while (!resourceRequest.isDone)
    {
        //ResourceRequest中priority变量 当前的加载进度 
        //该进度 不会特别准确 过渡也不是特别明显
        print(resourceRequest.progress);//打印当前加载进度
        //每结束就下一帧再判断一次
        yield return null;
    }
    //直到结束了赋值给图片
    tex = resourceRequest.asset as Texture;
}

总结

  1. 完成事件监听异步加载:

    • 好处:写法简单。
    • 坏处:只能在资源加载结束后进行处理,是一种”线性加载”。
    • 假如一个模型要加载三个资源,那么在这三个资源的完成异步加载回调里都要判断其他两个资源是否加载完。
  2. 协程异步加载:

    • 好处:可以在协程中处理复杂逻辑,比如同时加载多个资源,比如进度条更新。
    • 坏处:写法稍麻烦,是一种”并行加载”。
    • 假如一个模型要加载三个资源,可以直接在协程开启三个异步加载,用isDone并起来判断是否都加载完就行。

注意:

  • 理解为什么异步加载不能马上加载结束,为什么至少要等1帧。
  • 理解协程异步加载的原理。

19.2 知识点代码

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

public class Lesson19_Resources资源动态加载_Resources异步加载 : MonoBehaviour
{
    private Texture tex;

    void Start()
    {
        #region 知识点一 Resources异步加载是什么?

        //上节课学习的同步加载中
        //如果我们加载过大的资源可能会造成程序卡顿
        //卡顿的原因就是 从硬盘上把数据读取到内存中 是需要进行计算的
        //越大的资源耗时越长,就会造成掉帧卡顿

        //Resources异步加载 就是内部新开一个线程进行资源加载 不会造成主线程卡顿
        //基本工作流程是主线程通知新线程要加载哪些资源,新线程加载完了放到公共内存区域,主线程检测到加载完毕取出来使用
        //缺点是同步加载加载完直接拿来用,一步加载要等待加载完毕才能用,优点是能解决卡顿问题

        #endregion

        #region 知识点二 Resources异步加载方法

        //注意:异步加载 不能马上得到加载的资源 至少要等一帧

        //Resources中的LoadAsync方法 通过异步加载中的完成事件监听 使用加载的资源
        //异步加载存储在 Resources 文件夹中的 path 处的资源。
        //这句代码 你可以理解 Unity 在内部 就会去开一个线程进行资源下载
        //异步加载会有一个ResourceRequest类的返回值 用个变量接一下
        //ResourceRequest类是来自 Resources 包的异步加载请求。里面包含异步加载是否完成,进度这些变量,辅助做逻辑
        ResourceRequest resourceRequest = Resources.LoadAsync<Texture>("Tex/TestJPG");

        //怎么在异步加载结束后做逻辑了?主要有两种方法

        //使用ResourceRequest这个类异步加载完成的回调
        //ResourceRequest这个类存在完成异步加载的事件回调
        //当异步加载结束时 马上进行一个资源下载结束的一个事件函数监听回调
        //注意:completed这个事件有一个AsyncOperation类型的参数
        //AsyncOperation类型是ResourceRequest类型的父类 一般是要把AsyncOperation类型as成ResourceRequest类型再获取ResourceRequest相关
        resourceRequest.completed += AsynLoadOver;
        //打印帧数
        print("直接异步加载"+Time.frameCount);
        //这个 刚刚执行了异步加载的 执行代码 资源还没有加载完毕 不能直接使用资源 
        //一定要等加载结束过后 才能使用
        //resourceRequest.asset //错误用法

        //通过协程 异步加载的资源并写加载完之后的逻辑
        StartCoroutine(CoroutineAsynLoad());

        #endregion

        #region 总结

        //1.完成事件监听异步加载
        //好处:写法简单
        //坏处:只能在资源加载结束后 进行处理
        //“线性加载”
        //假如一个模型要加载三个资源,那么在这三个资源的完成异步加载回调里都要判断其他两个资源是否加载完

        //2.协程异步加载
        //好处:可以在协程中处理复杂逻辑,比如同时加载多个资源,比如进度条更新
        //坏处:写法稍麻烦
        //“并行加载”
        //假如一个模型要加载三个资源 可以直接在协程开启三个异步加载 用isDone并起来判断是否都加载完就行

        //注意:
        //理解为什么异步加载不能马上加载结束,为什么至少要等1帧
        //理解协程异步加载的原理

        #endregion
    }

    //协程函数异步加载
    IEnumerator CoroutineAsynLoad()
    {
        //开启协程就异步加载
        ResourceRequest resourceRequest = Resources.LoadAsync<Texture>("Tex/TestJPG");
        print("协程函数异步加载" + Time.frameCount);

        //方法一:直接 yield returnResourceRequest类对象 加载完再写逻辑
        //当yield return的是ResourceRequest这个类的时候 
        //Unity的协程管理器就自己知道这个类的返回值 意味着你在异步加载资源 
        yield return resourceRequest;
        //Unity 会自己判断 该资源是否加载完毕了 加载完毕过后 才会继续执行后面的代码
        print("协程函数异步加载结束" + Time.frameCount);

        //方法二:通过ResourceRequest中的变量判断当前加载情况 加载完再写逻辑
        //ResourceRequest中isDone变量 判断资源是否加载结束
        while (!resourceRequest.isDone)
        {
            //ResourceRequest中priority变量 当前的加载进度 
            //该进度 不会特别准确 过渡也不是特别明显
            print(resourceRequest.progress);//打印当前加载进度
            //每结束就下一帧再判断一次
            yield return null;
        }
        //直到结束了赋值给图片
        tex = resourceRequest.asset as Texture;
    }

    //异步加载结束事件监听回调
    private void AsynLoadOver(AsyncOperation asyncOperationq)
    {
        print("异步加载结束");
        //asset 是AsyncOperation类中的资源对象 加载完毕过后 就能够得到它
        tex = (asyncOperationq as ResourceRequest).asset as Texture;
        //得到后可以在OnGUI中画出这个图片

        //打印帧数
        print("直接异步加载" + Time.frameCount);
    }

    private void OnGUI()
    {
        //不为空就画图片
        if (tex != null)
            GUI.DrawTexture(new Rect(0, 0, 100, 100), tex);
    }
}

19.3 练习题

请写一个简单的资源管理器,提供统一的方法给外部用于资源异步加载,外部可以传入委托用于当资源加载结束时使用资源

创建资源管理器,实现单例

public class ResourcesMgr
{
    private static ResourcesMgr instance = new ResourcesMgr();
    public static ResourcesMgr Instance => instance;

    private ResourcesMgr()
    {
    }
}

创建异步加载资源的泛型封装方法并实现,添加泛型约束,并假如一个字典来进行优化,假如重复加载的对象就重新加载了,直接回调

// 记录已经加载的资源
private Dictionary<string, Object> loadedAssets = new Dictionary<string, Object>();

/// <summary>
/// 异步加载指定名称的资源,并在加载完成后使用回调方法处理资源。
/// </summary>
/// <typeparam name="T">要加载的资源类型(必须是Object的子类)。</typeparam>
/// <param name="assetPath">资源的名称或路径。</param>
/// <param name="callBack">资源加载完成后的回调函数,其中参数为加载得到的资源对象。</param>
public void LoadAssetAsync<T>(string assetPath, UnityAction<T> callBack) where T : Object
{
    // 如果存在这个key,直接拿来用
    if (loadedAssets.ContainsKey(assetPath))
    {
        Debug.Log("资源已经被加载过了,直接使用已经加载的资源进行回调");
        // 资源已经被加载过了,直接使用已经加载的资源进行回调
        // 如果资源已经被加载过了,则直接从已加载资源字典中取得
        Object obj = loadedAssets[assetPath];
        if (obj != null)
        {
            callBack(obj as T);
            return;
        }
        return;
    }

    // 异步加载资源
    ResourceRequest resourceRequest = Resources.LoadAsync<T>(assetPath);

    // 异步加载完成回调
    resourceRequest.completed += (asyncOperation) =>
    {
        // 直接得到传入的对象,通过它得到资源内容,然后转换成对应类型传出去,外面不需要转换,直接使用
        // 获取加载完成得到的资源,asyncOperation类as成ResourceRequest在使用相关asset
        Object asset = (asyncOperation as ResourceRequest).asset;

        // 将资源添加到已加载资源字典中,便于后续复用
        if (!loadedAssets.ContainsKey(assetPath))
        {
            // 将加载得到的资源添加到已加载资源字典中
            loadedAssets.Add(assetPath, asset);
        }

        // 执行回调函数,并将加载得到的资源对象作为参数传入
        callBack(asset as T);
    };
}

调用封装的方法加载图片,并写一个延迟函数延迟重复加载,它就不会重新Load了

private Texture tex;

void Start()
{
    // 调用自己封装的异步加载方法
    ResourcesMgr.Instance.LoadAssetAsync<Texture>("Tex/TestJPG", (obj) =>
    {
        tex = obj;
    });

    // 延迟加载已经Load过的图片
    Invoke("DelayLoadTestJPG", 3);

    // 不封装的情况
    ResourceRequest resourceRequest = Resources.LoadAsync<Texture>("Tex/TestJPG");
    resourceRequest.completed += (asyncOperation) =>
    {
        // AsyncOperation as成 ResourceRequest在使用
        tex = (asyncOperation as ResourceRequest).asset as Texture;
    };
}

private void OnGUI()
{
    if (tex != null)
        GUI.DrawTexture(new Rect(0, 0, 100, 100), tex);
}

void DelayLoadTestJPG()
{
    // 重复调用自己封装的异步加载方法,会判断是否加载过,加载过就不异步加载了
    ResourcesMgr.Instance.LoadAssetAsync<Texture>("Tex/TestJPG", (obj) =>
    {
        tex = obj;
    });
}

19.4 练习题代码

Lesson19_练习题

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

public class Lesson19_练习题 : MonoBehaviour
{
    #region 练习题一
    //请写一个简单的资源管理器,提供统一的方法给外部用于资源异步加载,外部可以传入委托用于当资源加载结束时使用资源

    private Texture tex;

    void Start()
    {
        //调用自己封装的异步加载方法
        ResourcesMgr.Instance.LoadAssetAsync<Texture>("Tex/TestJPG", (obj) =>
        {
            tex = obj;
        });

        //延迟加载已经Load过的图片
        Invoke("DelayLoadTestJPG", 3);

        //不封装的情况
        ResourceRequest resourceRequest = Resources.LoadAsync<Texture>("Tex/TestJPG");
        resourceRequest.completed += (asyncOperation) =>
        {
            //AsyncOperation as成 ResourceRequest在使用
            tex = (asyncOperation as ResourceRequest).asset as Texture;
        };
    }

    private void OnGUI()
    {
        if (tex != null)
            GUI.DrawTexture(new Rect(0, 0, 100, 100), tex);
    }


    void DelayLoadTestJPG()
    {
        //重复调用自己封装的异步加载方法 会判断是否加载过,加载过就不异步加载了
        ResourcesMgr.Instance.LoadAssetAsync<Texture>("Tex/TestJPG", (obj) =>
        {
            tex = obj;
        });
    }
    #endregion
}

ResourcesMgr

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class ResourcesMgr
{
    // 单例类实现
    private static ResourcesMgr instance = new ResourcesMgr();
    public static ResourcesMgr Instance => instance;

    // 将构造函数设为private,防止外部直接实例化对象
    private ResourcesMgr()
    {
    }

    // 记录已经加载的资源
    private Dictionary<string, Object> loadedAssets = new Dictionary<string, Object>();

    /// <summary>
    /// 异步加载指定名称的资源,并在加载完成后使用回调方法处理资源。
    /// </summary>
    /// <typeparam assetPath="T">要加载的资源类型(必须是Object的子类)。</typeparam>
    /// <param assetPath="assetPath">资源的名称或路径。</param>
    /// <param assetPath="callBack">资源加载完成后的回调函数,其中参数为加载得到的资源对象。</param>
    public void LoadAssetAsync<T>(string assetPath, UnityAction<T> callBack) where T : Object
    {
        //如果存在这个key 直接拿来用
        if (loadedAssets.ContainsKey(assetPath))
        {
            Debug.Log("资源已经被加载过了,直接使用已经加载的资源进行回调");
            // 资源已经被加载过了,直接使用已经加载的资源进行回调
            // 如果资源已经被加载过了,则直接从已加载资源字典中取得
            Object obj = loadedAssets[assetPath];
            if (obj != null)
            {
                callBack(obj as T);
                return;
            }
            return;
        }

        // 异步加载资源
        ResourceRequest resourceRequest = Resources.LoadAsync<T>(assetPath);

        // 异步加载完成回调
        resourceRequest.completed += (asyncOperation) =>
        {
            // 直接得到 传入的 对象 通过它得到资源内容 然后转换成对应类型 传出去 外面不需要转换 直接使用

            // 获取加载完成得到的资源 asyncOperation类as成ResourceRequest在使用相关asset
            Object asset = (asyncOperation as ResourceRequest).asset;

            // 将资源添加到已加载资源字典中,便于后续复用
            if (!loadedAssets.ContainsKey(assetPath))
            {
                // 将加载得到的资源添加到已加载资源字典中
                loadedAssets.Add(assetPath, asset);
            }

            // 执行回调函数,并将加载得到的资源对象作为参数传入
            callBack(asset as T);
        };
    }
}


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

×

喜欢就点赞,疼爱就打赏