工作中遇到一个加载卡顿的问题。
公司软件有一个色卡功能,用户可以根据点击的色卡更改背景等相关图片。并且色卡支持用户自定义上传,但是这里就遇到了两个问题。
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调用,判断下载队列的数量是否进行下载操作。
调用详解:数据包类声明了一个委托,下载类下完数据之后会对这个委托赋值从而调用(传递图片地址然后调用下载类加载图片方法,此处应该是当成事件来用(个人对委托与事件了解的还不够熟悉,后续熟悉了会补充))。
最终实现效果:
可以看到,在加载图片的时候依旧可以点击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(), 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秒后才执行下载文件的方法,效果如下:
图片有问号不用管,原因是网络上图片不存在。另外需要注意,用WWW类加载本地文件的时候,需要加上“file://”,第一次加载的时候没加上这个前缀竟然也加载出来了(灵异事件)
再次总结一下两种协程实现异步的方法:
第一种:下载数据的时候提供一个额外的回调方法,下载完成的时候调用回调方法加载。
第二种:先下载图片,想要加载的时候用协程和while循环配合,循环判断本地文件是否存在,存在的时候则加载。(适用于代码结构比较复杂的情况,即偷懒,或许一直判断性能有额外开销?本新手表示这方面不太理解,就不多说了)