工作中遇到一个加载卡顿的问题。
公司软件有一个色卡功能,用户可以根据点击的色卡更改背景等相关图片。并且色卡支持用户自定义上传,但是这里就遇到了两个问题。
1、从服务器下载色卡会造成卡顿。
2、创建物体并给RawImage赋值的时候会造成卡顿。
以上两个问题加起来会造成明显的卡顿,特别影响客户使用体验,因此,尝试使用协程优化下载和加载。

实现过程:
第一步:创建一个下载数据包类与一个静态帮助类。
下载数据包类用来存储需要下载数据的url,保存在本地的url。
静态类用来存储一些各个界面都会用到的数据。

public class AsyncHttpPackage
{
    //需要下载资源的地址
    private string url;
    //资源下载到本地的地址
    private string DonwFolder;
    //资源保存在本地的路径
    private string localFolder;
    public string Url
    {
        get { return url; }
        set { url = value; }
    }
    public string LocalFolder
    {
        get { return localFolder; }
        set { localFolder = value; }
    }
    //委托,后续给rawImage会用到
    public Action<string> DownLoaded;
    public AsyncHttpPackage(string url, string localFolder, Action<string>callback)
    {
        this.url = url;
        this.localFolder = localFolder;
        DownLoaded = callback;
    }
}
//静态帮助类,用来存储各个界面常用的数据
public static class StaticHelper
{
    public static string folderPath = @"D:\Test";
}

第二步:创建一个单例下载类,用来实现下载相关的功能

public class AsyncDownSingleton : MonoBehaviour
{
    private static AsyncDownSingleton _instance;
    public static AsyncDownSingleton Instance
    {
        get
        {
            if (_instance == null)
            {
                GameObject obj = new GameObject("AsyncSingleton");
                _instance = obj.AddComponent<AsyncDownSingleton>();
                DontDestroyOnLoad(obj);
            }
                return _instance;
        }
    }
    //需要下载数据包的队列
    public Queue<AsyncHttpPackage> listPackage = new Queue<AsyncHttpPackage>();

    public void EnterLoadingQueue(AsyncHttpPackage asyncHttpPackage)
    {
        if (!listPackage.Contains(asyncHttpPackage))
            listPackage.Enqueue(asyncHttpPackage);
    }
    //用来初始化文件保存的路径
    public void InitPath()
    {
        if (!Directory.Exists(StaticHelper.folderPath))
        {
            Directory.CreateDirectory(StaticHelper.folderPath);
        }
    }
    public IEnumerator AsyncDownData(AsyncHttpPackage package)
    {
        WWW www = new WWW(package.Url);
        yield return www;
        if(www.isDone)
        {
            package.LocalFolder = package.LocalFolder;
            Debug.Log("下载完成");
            byte[] bytes = www.bytes;
            CreateFile(package.LocalFolder, bytes);
            package.DownLoaded(package.LocalFolder);
        }
    }
    public void OnUpdate()
    {
        if (listPackage.Count > 0)
        {
            var package = listPackage.Dequeue();
            try
            {
                var filePath = package.LocalFolder;
                InitPath();
                if(!File.Exists(filePath))
                {
                    Debug.Log("文件不存在,开始下载");
                    StartCoroutine(AsyncDownData(package));
                }
                else
                {
                    Debug.Log("文件存在,执行委托");
                    package.DownLoaded(package.LocalFolder);
                }
            }
            catch (Exception)
            {
                throw;
            }
        }
    }
    void CreateFile(string path,byte[] bytes)
    {
        File.WriteAllBytes(path,bytes);
    }
    private void Update()
    {
        OnUpdate();
    }
}

第三步:创建一个单例加载图片类,用来实现给rawImage赋值texture的相关功能。

public class AsyncLoadRawImageSingleton : MonoBehaviour
{
    private static AsyncLoadRawImageSingleton _instance;
    public static AsyncLoadRawImageSingleton Instacne
    {
        get
        {
            if(_instance==null)
            {
                GameObject obj = new GameObject("AsyncLoadRawImageSingleton");
                DontDestroyOnLoad(obj);
                _instance = obj.AddComponent<AsyncLoadRawImageSingleton>();
            }
            return _instance;
        }
    }
    public void LoadRawImage(RawImage rawImage, string path)
    {
        StartCoroutine(LoadRawSprite(rawImage, path));
    }
    IEnumerator LoadRawSprite(RawImage rawimage, string path)
    {
        WWW www = new WWW(path);
        yield return www;
        if (www.isDone)
        {
            rawimage.texture = www.texture;
        }
    }
}

实际调用代码:

public class Test : MonoBehaviour {
    AsyncDownSingleton test;
    void Start () {
        test = AsyncDownSingleton.Instance;
        test.InitPath();
        List<string> downList = CreatUrlList();
        for (int i = 0; i < downList.Count; i++)
        {
            GameObject gameObject = new GameObject(i.ToString());
            gameObject.transform.SetParent(transform);
            gameObject.AddComponent<RawImage>();
            test.EnterLoadingQueue(new AsyncHttpPackage(downList[i], StaticHelper.folderPath+"\\"+i.ToString()+".jpg", (path) => {
                AsyncLoadRawImageSingleton.Instacne.LoadRawImage(gameObject.GetComponent<RawImage>(), path);
            }));
        }
    }
    //创建需要下载链接的集合
     List<string>CreatUrlList()
    {
        List<string> list = new List<string>();
        string url1 = "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2164724814,1401845036&fm=26&gp=0.jpg";
        string url2 = "https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1595225204&di=9e00f9e3da7ff7fe0af78d89ac83dd3b&src=http://img.juimg.com/tuku/yulantu/130506/240498-1305060IU666.jpg";
        string url3 = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1595237482741&di=2837a39b2fef6bf7a2490c77dfcb03ef&imgtype=0&src=http%3A%2F%2Fattach.bbs.miui.com%2Fforum%2F201312%2F03%2F165620x7cknad7vruvec1z.jpg";
        string url4 = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1595237482740&di=1cbb18e268851ac5d3835580cc583689&imgtype=0&src=http%3A%2F%2Fimg.pconline.com.cn%2Fimages%2Fupload%2Fupc%2Ftx%2Fwallpaper%2F1209%2F26%2Fc0%2F14139494_1348624365103.jpg";
        string url5 = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1595237482739&di=1615be1823e373873e6ffeef97e4346b&imgtype=0&src=http%3A%2F%2Fimg.ewebweb.com%2Fuploads%2F20191006%2F19%2F1570360737-HvGOTkxnum.jpg";
        string url6 = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1595237482738&di=49555cda0bdf10c9826e9ef62b82148d&imgtype=0&src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2Fb%2F57faf430da5d0.jpg";
        string url7 = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1595237482738&di=1968b09642b82d0f0e1b43397e969980&imgtype=0&src=http%3A%2F%2Fwww.jituwang.com%2Fuploads%2Fallimg%2F160405%2F257858-160405000g246.jpg";
        string url8 = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1595237917929&di=51716913142d9db1c88a8a32aa01b681&imgtype=0&src=http%3A%2F%2Fimg1.imgtn.bdimg.com%2Fit%2Fu%3D2845937221%2C3024056832%26fm%3D214%26gp%3D0.jpg";
        string url9 = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1595237482737&di=c68e4a944c13d42e22498bf9b752034a&imgtype=0&src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F2018-01-03%2F5a4c4270d8799.jpg";
        string url10 = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1595237950190&di=0732216827e8f0ec36e1ba725d1764db&imgtype=0&src=http%3A%2F%2Fimg2.imgtn.bdimg.com%2Fit%2Fu%3D1753371632%2C983488119%26fm%3D214%26gp%3D0.jpg";
        list.Add(url1);
        list.Add(url2);
        list.Add(url3);
        list.Add(url4);
        list.Add(url5);
        list.Add(url6);
        list.Add(url7);
        list.Add(url8);
        list.Add(url9);
        list.Add(url10);
        return list;
    }
}

逻辑思路整理:
1、获得需要下载数据的url,工作中一般是后台人员通过接口传递,这里我随便百度了几张图片,将url添加到一个string的List中做演示。
2、根据传递过来的List创造相应的游戏物体与数据包类,调用数据包类的构造函数对数据包类赋值,然后将数据添加到单例下载类的数据包队列中。
3、下载类调用自身下载方法,如果已存在,调用加载图片类的协程加载图片方法,如果不存在,调用自身协程下载方法后再调用图片类的协程加载图片方法。

下载类实现详解:下载方法通过Update调用,判断下载队列的数量是否进行下载操作。

调用详解:数据包类声明了一个委托,下载类下完数据之后会对这个委托赋值从而调用(传递图片地址然后调用下载类加载图片方法,此处应该是当成事件来用(个人对委托与事件了解的还不够熟悉,后续熟悉了会补充))。

最终实现效果:

unity resources异步加载资源 unity 异步加载图片_数据


可以看到,在加载图片的时候依旧可以点击Button,如果是同步的话则会卡住,全部执行完才可以进行点击,此处就不做演示了。

案例Demo PS:这里是U3d萌新一只,日常分享工作中遇到的问题以及解决方法。

2020.8.20 后续更新
工作中遇到了一件非常操蛋的事情,有一个老项目,登录的时候非常卡,于是查看了一下登录代码。
发现主要执行了以下几个方法:
1、UI层初始化方法A。
2、下载压缩包方法,压缩包里有画图等相关数据(大坑,后续会讲)。
3、获取需要订单数据的后台接口。
执行顺序是将A作为回调函数传给3,执行完2之后进行回调。
即先调用后台接口,获取需要加载订单的常规数据。然后再下载压缩包,压缩包全部下载完成之后再进行UI层的初始化方法A。
看到这里,小伙伴们估计已经看出来问题了,没错,问题就出现等待压缩包下载完成,调用后台接口拿到数据其实就已经可以可以进行UI层的初始化方法了。
原本以为是个美差,改下回调方法执行位置不就完成工单了吗,遂改个位置,开始调试。加载速度瞬间加快,点登陆之后就开始加载,结果没想到苦难从此开始。突然间跳出一堆错误,一看全是空引用。好家伙,仔细一看代码,初始化方法A需要调用压缩包里的数据。难怪当初设计的人要先等压缩包下载完成解压然后再初始化。
那么问题就来了,该如何解决呢?有两种方法:
1、代码重构,基本不可能,远古代码里面的逻辑鬼知道是啥,而且里面涉及了很多回调(当初找问题的时候回调都快把我人回调傻了),其次太浪费时间,当初写代码的人走了,期间遇到问题也没有办法问。那么就只能使用第二种方法了。(PS:偷偷吐槽一下公司的合作制度,公司前端后端分开,后端的数据可能多个项目部门都在用,经常会这个部门加了一个字段,其他部门没有通知导致调用接口报错)。
2、使用协程和while循环无限判断本地是否有文件,有则加载并跳出循环,没有则继续循环。
代码如下:首先改动下载包类,改为只下载文件。

public AsyncHttpPackage(string url, string localFolder)
    {
        this.url = url;
        this.localFolder = localFolder;
    }

然后改动加载类,新写一个加载方法。

IEnumerator LoadRawSprite(RawImage rawimage, string path)
    {
        //第一种正常加载方式
        //WWW www = new WWW("file://" + path);
        //yield return www;
        //if (www.isDone && www.error == null)
        //{
        //    rawimage.texture = www.texture;
        //}
        //第二种方式,不知道数据什么时候下载完成。
        WWW www;
        while (true)
        {
            if(File.Exists(path))
            {
                www = new WWW("file://" + path);
                yield return www;
                if (www.isDone && www.error == null)
                {
                    rawimage.texture = www.texture;
                    yield break;
                }
            }
            yield return 0;
        }
    }

最后在Test脚本中执行。

for (int i = 0; i < downList.Count; i++)
        {
            GameObject gameObject = new GameObject(i.ToString());
            gameObject.transform.SetParent(transform);
            gameObject.AddComponent<RawImage>();
            //test.EnterLoadingQueue(new AsyncHttpPackage(downList[i], StaticHelper.folderPath+"\\"+i.ToString()+".jpg", (path) => {
            //    AsyncLoadRawImageSingleton.Instacne.LoadRawImage(gameObject.GetComponent<RawImage>(), path);
            //}));
            //先执行加载的代码
            AsyncLoadRawImageSingleton.Instacne.LoadRawImage(gameObject.GetComponent<RawImage>(), StaticHelper.folderPath + "\\" + i.ToString() + ".jpg");         
        }
        //3秒后执行下载代码
        yield return new WaitForSeconds(3);
        for (int i = 0; i < downList.Count; i++)
            test.EnterLoadingQueue(new AsyncHttpPackage(downList[i], StaticHelper.folderPath + "\\" + i.ToString() + ".jpg"));

如代码所示,先创建了游戏物体,然后调用加载的方法,3秒后才执行下载文件的方法,效果如下:

unity resources异步加载资源 unity 异步加载图片_加载_02


图片有问号不用管,原因是网络上图片不存在。另外需要注意,用WWW类加载本地文件的时候,需要加上“file://”,第一次加载的时候没加上这个前缀竟然也加载出来了(灵异事件)

再次总结一下两种协程实现异步的方法:

第一种:下载数据的时候提供一个额外的回调方法,下载完成的时候调用回调方法加载。

第二种:先下载图片,想要加载的时候用协程和while循环配合,循环判断本地文件是否存在,存在的时候则加载。(适用于代码结构比较复杂的情况,即偷懒,或许一直判断性能有额外开销?本新手表示这方面不太理解,就不多说了)

2021.3.11后续改为使用unitywebquest
 UnityWebRequest webRequest = UnityWebRequest.Get(package.Url);
 //        webRequest.timeout = 30;
 //        yield return webRequest.SendWebRequest();
 //        if (webRequest.isNetworkError)
 //        {
 //            Debug.Log(“Download Error:” + webRequest.error);
 //            if (package.DownLoaded != null)
 //                package.DownLoaded(“error”, package.PackageID);
 //        }
 //        else
 //        {
 //            Debug.Log(“下载完成”);
 //            Debug.LogWarning(“下载完成这句话应该先执行,然后执行下载完成后面的那句话”);
 //            byte[] bytes = webRequest.downloadHandler.data;
 //            CreateFile(package.LocalFolder, bytes);
 //            if (package.DownLoaded != null)
 //                package.DownLoaded(package.LocalFolder, package.PackageID);