以下所有代码都是基于Unity2019.4.0f1版本进行开发


文章目录

前言

一、网络加载图片的两种方式

二、Texture和Sprite的转换

三、制作网络加载图片单例类

四、网络图片的缓存及本地存储

五、图片完整性的验证

结语


前言

在日常的项目制作当中,我们难免会涉及到使用到服务器上面的图片资源,就会需要我们去进行加载、显示等操作。但是在加载过程当中也会有意外的产生,导致图片加载失败,在未经处理的情况下,就会显示一个大大红问号,这会对用户体验产生一定的问题。没关系,咱们一个一个的解决加载网络图片的问题。

一、网络加载图片的两种方式

首先我们想要把服务器的图片加载到本地,不可获取的就是这个图片的URL,这一点我相信对于看到这篇文章的各位并不是问题,不知道怎么获取的可自行去网上随便找一个图片~,而后我们就需要对这个连接进行读取,这里我用到的是Unity的WWW类。但是现在Unity官方比较推荐的是使用UnityWebRequest来进行加载,我会将代码都贴到下面,示例代码如下:

// 使用 WWW 加载
    public void downImageAction(string url)
    {
        StartCoroutine(downloadImage(url));
    }

    private IEnumerator downloadImage(string url)
    {
        WWW www = new WWW(url);

        yield return www;

        Texture2D texture = www.texture;
    }
 
    

    // 使用 UnityWebRequest 加载
    public void downImageAction(string url)
    {
        Uri uri = new Uri(url);
        StartCoroutine(downloadImage(uri));
    }

    private IEnumerator downloadImage(Uri uri)
    {
        UnityWebRequest unityWebRequest = UnityWebRequestTexture.GetTexture(uri);

        DownloadHandlerTexture downloadHandlerTexture = new DownloadHandlerTexture(true);

        unityWebRequest.downloadHandler = downloadHandlerTexture;

        yield return unityWebRequest.SendWebRequest();

        Texture2D texture = downloadHandlerTexture.texture;
    }

通过上述代码不论是哪一种方式,我们都可以通过URL获取到了这个图片的Texture,而这两种方式我再实际使用的过程当中并没有感受到有什么区别,目前来说大家可以随意选择。

二、Texture和Sprite的转换

言归正传,当我们获取到了图片的Texture之后可以满足一部分操作了,比如Material的贴图、RawImage等可接收Texture的组件就实现了使用网络图片资源了,但是如果要用到Image当中就需要将Texture转换成Sprite后再对Image进行赋值,转换脚本如下:

public Sprite textureConvert(Texture2D texture)
    {
        texture.wrapMode = TextureWrapMode.Clamp;
        texture.filterMode = FilterMode.Point;

        Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);

        return sprite;
    }

到这里基本上加载网络图片的功能就已经实现了,可是这些脚本还是过于简单,对于一些测试、或者是学习了解加载网络资源来说是可以满足了的,但我们不能满足于此,在针对实际使用环境来进行进一步的制作:将加载脚本制作成单例实例、队列加载及存储、缓存至内存及本地、增加图片完整性的判断。好了,废话不多说我们直接上干货!

三、制作网络加载图片单例类

对于单例模式我想我在这里就不用多说些什么,直接上代码!

public class AsyncImageDownload : MonoBehaviour
    {
        private static AsyncImageDownload _instance;

        public static AsyncImageDownload GetInstance()
        {
            return Instance;
        }

        public static AsyncImageDownload Instance
        {
            get
            {
                if (_instance == null)
                {
                    GameObject obj = new GameObject("AsyncImageDownload");
                    _instance = obj.AddComponent<AsyncImageDownload>();
                    _instance.init();
                }

                return _instance;
            }
        }
    }

用到单例之后,就不用担心引用不到,或者是在物体上挂载太多组件的问题了,直接在脚本中调用就好了,接下来就可以制作缓存相关的脚本了。因为缓存跟队列加载都属于优化用户体验的问题,就放到一起说了。使用队列是因为想把加载内容拆成X份,在设定的间隔时间内只执行这一份,用来减少CPU的负担,优化用户体验。而缓存则是避免了资源重复加载的问题。

四、网络图片的缓存及本地存储

缓存分为两种存储在内存中和保存到本地磁盘当中,这两种可以根据实际情况使用,也可以都使用,在内存当中进行缓存操作是为了避免相同的一份资源被多次从服务器中拉取,在所需资源量比较少的情况下还不是很明显,一旦需要加载的数量大了,我们的工程就会出现因为过多的加载网络资源产生卡顿,所以相同的资源我们只需要下载一次就可以了。而保存在本地磁盘的操作是进一步去优化了这一点,相同的资源我们只进行一次下载后,保存到本地磁盘当中,等下一次启动项目时则直接可以从磁盘当中读取,避免了二次下载的问题。而这样做,需要考虑到磁盘空间是否足够,在什么时机进行保存等等条件,我这里把代码贴出来你们自行取用~

using System.Collections.Generic;
using UnityEngine;
using System.IO;
using UnityEngine.UI;
 
public class AsyncImageDownload : MonoBehaviour
    {
        private static AsyncImageDownload _instance;

        public static AsyncImageDownload GetInstance()
        {
            return Instance;
        }

        public static AsyncImageDownload Instance
        {
            get
            {
                if (_instance == null)
                {
                    GameObject obj = new GameObject("AsyncImageDownload");
                    _instance = obj.AddComponent<AsyncImageDownload>();
                }

                return _instance;
            }
        }
        
        private Dictionary<string, Sprite> spriteDic;

    private void Start()
    {
        InvokeRepeating("readLoadImageQueue", 0, 0.05f);
        InvokeRepeating("readSaveImageQueue", 0, 1);
    }

    private void readLoadImageQueue()
    {
        AsyncImageQueue.readLoadImageQueue();
    }

    private void readSaveImageQueue()
    {
        AsyncImageQueue.readSaveImageQueue();
    }

    private void init()
    {
        if (!Directory.Exists(imageCacheFolderPath))
        {
            Directory.CreateDirectory(imageCacheFolderPath);
        }

        spriteDic = new Dictionary<string, Sprite>();
    }

    public void setAsyncImage(string url, Image image, bool isReload = false)
    {
        AsyncImageInfo asyncImageInfo = new AsyncImageInfo
        {
            asyncImageDownload = this,
            URL = url,
            image = image
        };

        if (!File.Exists(imageCacheFolderPath + url.GetHashCode() + ".png"))
        {
            if (Application.internetReachability == NetworkReachability.ReachableViaLocalAreaNetwork)
            {
                if (!spriteDic.ContainsKey(imageCacheFolderPath + url.GetHashCode()))
                {
                    asyncImageInfo.type = AsyncImageType.net;
                }
                else
                {
                    asyncImageInfo.type = AsyncImageType.local;
                }
            }
        }
        else
        {
            asyncImageInfo.type = AsyncImageType.local;
        }
    }

    public void downloadImageAction(string url, Image image)
    {
        StartCoroutine(downloadImage(url, image));
    }

    public void localImageAction(string url, Image image)
    {
        StartCoroutine(loadLocalImage(url, image));
    }

    private IEnumerator downloadImage(string url, Image image)
    {
        WWW www = new WWW(url);

        yield return www;

        Texture2D texture = www.texture;

        try
        {
            SaveImageInfo saveImageInfo = new SaveImageInfo
            {
                pngData = texture.EncodeToPNG(),
                fileName = imageCacheFolderPath + url.GetHashCode() + ".png"
            };

            AsyncImageQueue.addSaveImageQueue(saveImageInfo);
        }
        catch
        {
        }

        texture.wrapMode = TextureWrapMode.Clamp;
        texture.filterMode = FilterMode.Point;

        Sprite m_sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);

        image.sprite = m_sprite;

        if (!spriteDic.ContainsKey(imageCacheFolderPath + url.GetHashCode()))
        {
            spriteDic.Add(imageCacheFolderPath + url.GetHashCode(), m_sprite);
        }
    }

    private IEnumerator loadLocalImage(string url, Image image)
    {
        if (!spriteDic.ContainsKey(imageCacheFolderPath + url.GetHashCode()))
        {
            string filePath = "file:///" + imageCacheFolderPath + url.GetHashCode() + ".png";

            WWW www = new WWW(filePath);

            yield return www;

            Texture2D texture = www.texture;

            texture.wrapMode = TextureWrapMode.Clamp;
            texture.filterMode = FilterMode.Point;

            Sprite m_sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);

            image.sprite = m_sprite;

            if (!spriteDic.ContainsKey(imageCacheFolderPath + url.GetHashCode()))
            {
                spriteDic.Add(imageCacheFolderPath + url.GetHashCode(), m_sprite);
            }
        }
        else
        {
            image.sprite = spriteDic[imageCacheFolderPath + url.GetHashCode()];
        }
    }

    private string imageCacheFolderPath
    {
        get { return Application.persistentDataPath + "/xxx/ImageCaChe/"; }
    }
}

在上述脚本中,用到了一个队列结构:

public class AsyncImageQueue
    {
        private static Queue loadImageQueue = new Queue();
        private static Queue saveImageQueue = new Queue();

        public static void addLoadImageQueue(AsyncImageInfo asyncImageInfo)
        {
            loadImageQueue.Enqueue(asyncImageInfo);
        }

        public static void readLoadImageQueue()
        {
            if (loadImageQueue.Count > 0)
            {
                AsyncImageInfo asyncImageInfo = (AsyncImageInfo) loadImageQueue.Dequeue();

                if (asyncImageInfo.image)
                {
                    switch (asyncImageInfo.type)
                    {
                        case AsyncImageType.local:
                            asyncImageInfo.asyncImageDownload.localImageAction(asyncImageInfo.URL, asyncImageInfo.image);
                            break;
                        case AsyncImageType.net:
                            asyncImageInfo.asyncImageDownload.downloadImageAction(asyncImageInfo.URL, asyncImageInfo.image);
                            break;
                        default:
                            break;
                    }
                }
            }
        }

        public static void addSaveImageQueue(SaveImageInfo saveImageInfo)
        {
            saveImageQueue.Enqueue(saveImageInfo);
        }

        public static void readSaveImageQueue()
        {
            if (saveImageQueue.Count > 0)
            {
                SaveImageInfo saveImageInfo = (SaveImageInfo) saveImageQueue.Dequeue();

                File.WriteAllBytes(saveImageInfo.fileName, saveImageInfo.pngData);
            }
        }
    }

以及队列信息和存储信息

using UnityEngine;
    using UnityEngine.UI;

    public class AsyncImageInfo
    {
        public AsyncImageDownload asyncImageDownload;
        public string URL;
        public Image image;
        public AsyncImageType type;
    }

    public enum AsyncImageType
    {
        local,
        net
    }
public class SaveImageInfo
{
    public byte[] pngData;
    public string fileName;
}

至此,我们网络图片加载已经缓存功能都已经完成了。下面我们来解决如何判断图片完整性的问题,因为这一部分我个人涉猎的比较少,如果有哪些不对的地方,欢迎大家的指正。

五、图片完整性的验证

简单的思路是这样的,根据相同类型的图片(jpg、png等),头尾的两位字节是相同的,以此我们可以来判断这个图片的类型以及是否下载完成。

根据查询和实验可以得到:

类型



JPG

255216

255217

PNG

13780

96130

private bool checkImage(byte[] pngData)
    {
        if (pngData.Length > 4)
        {
            string fileHead = pngData[0].ToString() + pngData[1].ToString();
            string flieTail = pngData[pngData.Length - 2].ToString() + pngData[pngData.Length - 1].ToString();

            return checkImageFileFormat(fileHead, flieTail);
        }
        else
        {
            return false;
        }
    }

    private bool checkImage(string filePath)
    {
        try
        {
            FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);

            BinaryReader reader = new BinaryReader(fs);

            if (fs.Length > 0)
            {
                byte[] pngData = reader.ReadBytes((int) fs.Length);

                string fileHead = pngData[0].ToString() + pngData[1].ToString();
                string flieTail = pngData[pngData.Length - 2].ToString() + pngData[pngData.Length - 1].ToString();

                fs.Close();

                reader.Close();

                return checkImageFileFormat(fileHead, flieTail);
            }
            else
            {
                fs.Close();
                reader.Close();

                return false;
            }
        }
        catch
        {
            return false;
        }
    }

    private bool checkImageFileFormat(string fileHead, string fileTail)
    {
        if ((fileHead == "255216" && fileTail == "255217") ||
            (fileHead == "13780" && fileTail == "96130"))
        {
            return true;
        }
        else
        {
            return false;
        }
    }

结语

以上就是今天要讲的内容,希望会对大家有所帮助!