AssetBundle学习初探

应该如何创建一个AssetBundle文件?应该如何正确使用AssetBundle?

要了解这两个问题,就不得不先学习一下AssetBundle的工作流程

使用AssetBundle的工作流程,大体可以分为两个阶段,即开发阶段和运行阶段

一.开发阶段

在开发阶段,我们将AssetBundle操作分为两个部分:创建AssetBundle文件,上传创建好的AssetBundle到外部存储空间以备后续项目需要下载使用

1.创建AssetBundle文件,要使用一个编辑器相关的类:BuildPipeline

现在Unity使用的是新的AssetBundle系统,所以打包用的API是BuildPipeline.BuildAssetBundles

第二个API的第一个参数是打包到哪个文件夹里,Asset目录下;第二是包体的压缩格式,要是不太清楚BuildAssetBundleOptions是啥玩意,点F12进入看.就是个枚举,里面有很多选项

第三个参数是,你要在哪个平台使用ab包,BuildTarget也是个枚举

这个可使用插件,也可使用拓展编辑器,方便来说,是拓展编辑器 

使用拓展编辑器,要注意一点,就是在Asset目录下新建一个文件夹Editor,然后将所有和编辑器相关的游戏脚本放到里面。别问为啥,人家Unity底层就是这么搞的

 2.上传创建好的AssetBundle到外部存储空间

开发的时候,一般会将AB包放到本地,因为会进行频繁的操作,发布的时候才会上传到服务器上.

二.运行阶段

第一步很明显,加载ab包,或从本地或从服务器

加载ab包常用4个不同的APi

AssetBundle.LoadFromMemory(Async optional)


 

Unity的建议是——不要使用这个API。

LoadFromMemory(Async) 是从托管代码的字节数组里加载AssetBundle。也就是说你要提前用其它的方式将资源的二进制数组加入到内存中。然后该接口会将源数据从托管代码字节数组复制到新分配的、连续的本机内存块中。

如果AssetBundle使用了LZMA压缩类型,它将在复制时解压AssetBundle。而未压缩和LZ4压缩类型的AssetBundle将逐字节的完整复制。

之所以不建议使用该API是因为,此API消耗的最大内存量将至少是AssetBundle的两倍:本机内存中的一个副本,和LoadFromMemory(Async)从托管字节数组中复制的一个副本。

因此,从通过此API创建的AssetBundle加载的资产将在内存中冗余三次:一次在托管代码字节数组中,一次在AssetBundle的栈内存副本中,第三次在GPU或系统内存中,用于Asset本身。

注意:在Unity 5.3.3之前,这个API被称为AssetBundle.CreateFromMemory。但功能没有改变。

 

AssetBundle.LoadFromFile(Async optional)

LoadFromFile是一种高效的API,用于从本地存储(如硬盘或SD卡)加载未压缩或LZ4压缩格式的AssetBundle。

在桌面独立平台、控制台和移动平台上,API将只加载AssetBundle的头部,并将剩余的数据留在磁盘上。

AssetBundle的Objects会按需加载,比如:加载方法(例如:AssetBundle.Load)被调用或其InstanceID被间接引用的时候。在这种情况下,不会消耗过多的内存。

但在Editor环境下,API还是会把整个AssetBundle加载到内存中,就像读取磁盘上的字节和使用AssetBundle.LoadFromMemoryAsync一样。

如果在Editor中对项目进行了分析,此API可能会导致在AssetBundle加载期间出现内存尖峰。但这不应影响设备上的性能,在做优化之前,这些尖峰应该在设备上重新再测试一遍。

要注意,这个API只针对未压缩或LZ4压缩格式,因为前面说过了,如果使用LZMA压缩,它是针对整个生成后的数据包进行压缩的,所以在未解压之前是无法拿到AssetBundle的头信息的。

注意:这里曾经有过一个历史遗留问题,即在Unity 5.3或更老版本的Android设备上,当试图从Streaming Assets路径加载AssetBundles时,此API将失败。这个问题已在Unity 5.4中解决。

在Unity 5.3之前,这个API被称为AssetBundle.CreateFromFile。其功能没有改变。

UnityWebRequest's DownloadHandlerAssetBundle


WWW.LoadFromCacheOrDownload (on Unity 5.6 or older)

AssetBundleDownloadHandler
DownloadHandlerAssetBundle的操作是通过UnityWebRequest的API来完成的。

UnityWebRequest API允许开发人员精确地指定Unity应如何处理下载的数据,并允许开发人员消除不必要的内存使用。使用UnityWebRequest下载AssetBundle的最简单方法是调用UnityWebRequest.GetAssetBundle。

就实战项目而言,最有意思的类是DownloadHandlerAssetBundle。它使用工作线程,将下载的数据流存储到一个固定大小的缓冲区中,然后根据下载处理程序的配置方式将缓冲数据放到临时存储或AssetBundle缓存中。

所有这些操作都发生在非托管代码中,消除了增加堆内存的风险。此外,该下载处理程序并不会保留所有下载字节的栈内存副本,从而进一步减少了下载AssetBundle的内存开销。

LZMA压缩的AssetBundles将在下载和缓存的时候更改为LZ4压缩。这个可以通过设置Caching.CompressionEnable属性来更改。

如果将缓存信息提供给UnityWebRequest对象,一旦有请求的AssetBundle已经存在于Unity的缓存中,那么AssetBundle将立即可用,并且此API的行为将会与AssetBundle.LoadFromFile相同操作。

在Unity 5.6之前,UnityWebRequest系统使用了一个固定的工作线程池和一个内部作业系统来防止过多的并发下载,并且线程池的大小是不可配置的。在Unity 5.6中,这些安全措施已经被删除,以便适应更现代化的硬件,并允许更快地访问HTTP响应代码和报头。

WWW.LoadFromCacheOrDownload


这是一个很古老的API了,从Unity 2017.1开始,就只是简单地包装了UnityWebRequest。因此,使用Unity 2017.1或更高版本的开发者应该直接使用UnityWebRequest来工作。Unity已经放弃了对改接口的维护,并可能在未来的某个版本中移除。

所以下面说的这些内容只适合于Unity 5.6或更老的版本。

WWW.LoadFromCacheOrDownload允许从远程服务器和本地存储加载对象。也可以通过文件//URL从本地存储加载文件。如果AssetBundle存在于Unity Cache中,则此API的行为将与AssetBundle.LoadFromFile完全相同。

如果AssetBundle尚未缓存,则WWW.LoadFromCacheOrDownload会将从它的源文件读取AssetBundle。如果AssetBundle被压缩过,它会使用工作线程进行解压缩并写入缓存中。否则,它将通过工作线程直接写入缓存。

在缓存AssetBundle之后,WWW.LoadFromCacheOrDownload将从缓存的、解压缩的AssetBundle加载头信息。然后,和AssetBundle.LoadFromFile加载AssetBundle行为相同。

此缓存会在WWW.LoadFromCacheOrDownload和UnityWebRequest之间共享。一个API下载的任何AssetBundle也可以通过另一个API获得。

虽然数据将通过固定大小的缓冲区解压缩并写入缓存,但WWW对象会在本机内存中保留AssetBundle字节的完整副本。这个额外副本被保留的原因是因为要支持WWW.bytes字节属性。

由于在WWW对象中缓存AssetBundle的字节的内存开销,所以,实际项目开发中AssetBundles应该要保持较少的体积以便减少内存。

与UnityWebRequest不同的是,每次调用这个API都会产生一个新的工作线程。因此,在手机等内存有限的平台上,最好限定一次只能下载一个AssetBundle,以避免内存激增。而在其它平台也要小心创建过多的线程。如果需要下载5个以上的AssetBundles,建议在脚本代码中创建和管理下载队列,以确保只有少数几个AssetBundle同时下载。

建议
(1)一般来说,只要有可能,就应该使用AssetBundle.LoadFromFile。这个API在速度、磁盘使用和运行时内存使用方面是最有效的。

(2)对于必须下载或热更新AssetBundles的项目,强烈建议对使用Unity 5.3或更高版本的项目使用UnityWebRequest,对于使用Unity 5.2或更老版本的项目使用WWW.LoadFromCacheOrDownload。

(3)当使用UnityWebRequest或WWW.LoadFromCacheOrDownload时,要确保下载程序代码在加载AssetBundle后正确地调用Dispose。另外,C#的using语句是确保WWW或UnityWebRequest被安全处理的最方便的方法。

(4)对于需要独特的、特定的缓存或下载需求的大项目,可以考虑使用自定义的下载器。编写自定义下载程序是一项重要并且复杂的任务,任何自定义的下载程序都应该与AssetBundle.LoadFromFile保持兼容。

当然了,以上都是一些基础的操作.要是人家游戏更新了功能,你手机的游戏没有,可是你也想玩,要下载呢?AssetBundle用来做热更新,热更热更,这个热字,含义就是不改变原包的基础上进行功能更新.就是说只下载那些更新的功能,而不用重新下载整个游戏.

要知道项目新开发AB包相对原来的AB包做了哪些改变,这就需要用到MD5码了.

啥叫MD5码呢?

MD5(Message-Digest Algorithm)是MD5信息摘要算法的简称.

它是一种广泛使用的密码散列函数

可以生成出一个128位(16个字节)的散列值用于确保信息的完整一致性

当我们将数据经过MD5算法计算过后.不管我们传入的数据有多大
都会生成一个固定长度(128位共16个字节)的信息摘要值

相同的数据,每次经过MD5算法计算后的结果都会是一样的
如果数据变化,MD5码将会发生变化

因此,我们可以利用MD5码作为文件的唯一标识
通过它来判断文件内容是否变化.

怎么AB包的MD5码呢?

(以下讲述只是对上文做填补,能力有限,暂无法将两者结合讲述)

首先要知道一个关键类
MD5 —— MD5类

(C#文件流相关的知识,是下载AB包时,file fileinfo directory directoryinfo等形影不离的)
MD5CryptoServiceProvider —— MD5加密服务提供商类

流程:
1.根据文件路径,获取文件的流信息
2.利用md5对象根据流信息 计算出MD5码(字节数组形式)
3.将字节数组形式的MD5码转为 16进制字符串

如何对比新旧文件呢?用到了上文的方法GetMD5

 生成了对比文件,一般会传送到服务器,怎么传呢?拿ftp举例

注意哈,一定要首先生成对比文件

 上传MD5码对比文件成功了,下载呢?

public class ABUpdateMgr : MonoBehaviour
{
    private static ABUpdateMgr instance;

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

    //用于存储远端AB包信息的字典 之后 和本地进行对比即可完成 更新 下载相关逻辑
    private Dictionary<string, ABInfo> remoteABInfo = new Dictionary<string, ABInfo>();

    public void DownLoadABCompareFile()
    {
        //1.从资源服务器下载资源对比文件
        // www UnityWebRequest ftp相关api
        print(Application.persistentDataPath);
        DownLoadFile("ABCompareInfo.txt", Application.persistentDataPath + "/ABCompareInfo.txt");

        //2.就是获取资源对比文件中的 字符串信息 进行拆分
        string info = File.ReadAllText(Application.persistentDataPath + "/ABCompareInfo.txt");
        string[] strs = info.Split('|');//通过|拆分字符串 把一个个AB包信息拆分出来
        string[] infos = null;
        for (int i = 0; i < strs.Length; i++)
        {
            infos = strs[i].Split(' ');//又把一个AB的详细信息拆分出来
            //记录每一个远端AB包的信息 之后 好用来对比
            remoteABInfo.Add(infos[0], new ABInfo(infos[0], infos[1], infos[2]));
        }

        print("远端AB包对比文件 加载结束");        
    }

    private void DownLoadFile(string fileName, string localPath)
    {
        try
        {
            //1.创建一个FTP连接 用于下载
            FtpWebRequest req = FtpWebRequest.Create(new Uri("ftp://192.168.50.49/AB/PC/" + fileName)) as FtpWebRequest;
            //2.设置一个通信凭证 这样才能下载(如果有匿名账号 可以不设置凭证 但是实际开发中 建议 还是不要设置匿名账号)
            NetworkCredential n = new NetworkCredential("MrTang", "MrTang123");
            req.Credentials = n;
            //3.其它设置
            //  设置代理为null
            req.Proxy = null;
            //  请求完毕后 是否关闭控制连接
            req.KeepAlive = false;
            //  操作命令-下载
            req.Method = WebRequestMethods.Ftp.DownloadFile;
            //  指定传输的类型 2进制
            req.UseBinary = true;
            //4.下载文件
            //  ftp的流对象
            FtpWebResponse res = req.GetResponse() as FtpWebResponse;
            Stream downLoadStream = res.GetResponseStream();
            using (FileStream file = File.Create(localPath))
            {
                //一点一点的下载内容
                byte[] bytes = new byte[2048];
                //返回值 代表读取了多少个字节
                int contentLength = downLoadStream.Read(bytes, 0, bytes.Length);

                //循环下载数据
                while (contentLength != 0)
                {
                    //写入到本地文件流中
                    file.Write(bytes, 0, contentLength);
                    //写完再读
                    contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
                }

                //循环完毕后 证明下载结束
                file.Close();
                downLoadStream.Close();

                print(fileName + "下载成功");
            }
        }
        catch (Exception ex)
        {
            print(fileName + "下载失败" + ex.Message);
        }

    }

    private void OnDestroy()
    {
        instance = null;
    }

    //AB包信息类
    private class ABInfo
    {
        public string name;//AB包名字
        public long size;//AB包大小
        public string md5;//AB包md5码

        public ABInfo(string name, string size, string md5)
        {
            this.name = name;
            this.size = long.Parse(size);
            this.md5 = md5;
        }
    }

后面的ftp是复制之前上传用的.具体的思路是搞一个字典,拿ab包的名字当键,拿ab包的信息类当值,将下载后拆分的MD5码和后续的AB包信息类对应.当然,还没完事,因为存完了,还要和后面改动后的MD5码对比呀,下面接着

 //用于存储远端AB包信息的字典 之后 和本地进行对比即可完成 更新 下载相关逻辑
    private Dictionary<string, ABInfo> remoteABInfo = new Dictionary<string, ABInfo>();

    //这个是待下载的AB包列表文件 存储AB包的名字
    private List<string> downLoadList = new List<string>();

    //下载了多少个文件
    private int downLoadOverNum = 0;
    public void DownLoadABCompareFile()
    {
        //1.从资源服务器下载资源对比文件
        // www UnityWebRequest ftp相关api
        print(Application.persistentDataPath);
        DownLoadFile("ABCompareInfo.txt", Application.persistentDataPath + "/ABCompareInfo.txt");

        //2.就是获取资源对比文件中的 字符串信息 进行拆分
        string info = File.ReadAllText(Application.persistentDataPath + "/ABCompareInfo.txt");
        string[] strs = info.Split('|');//通过|拆分字符串 把一个个AB包信息拆分出来
        string[] infos = null;
        for (int i = 0; i < strs.Length; i++)
        {
            infos = strs[i].Split(' ');//又把一个AB的详细信息拆分出来
            //记录每一个远端AB包的信息 之后 好用来对比
            remoteABInfo.Add(infos[0], new ABInfo(infos[0], infos[1], infos[2]));
        }

        print("远端AB包对比文件 加载结束");        
    }

    public async void DownLoadABFile(UnityAction<bool> overCallBack, UnityAction<int,int> updatePro)
    {
        //1.遍历字典的键 根据文件名 去下载AB包到本地
        foreach (string name in remoteABInfo.Keys)
        {
            //直接放入 待下载列表中
            downLoadList.Add(name);
        }
        //本地存储的路径 由于多线程不能访问Unity相关的一些内容比如Application 所以声明再外部
        string localPath = Application.persistentDataPath + "/";
        //是否下载成功
        bool isOver = false;
        //下载成功的列表 之后用于移除下载成功的内容
        List<string> tempList = new List<string>();
        //重新下载的最大次数
        int reDownLoadMaxNum = 5;
        //下载成功的资源数
        int downLoadOverNum = 0;
        //这一次下载需要下载多少个资源
        int downLoadMaxNum = downLoadList.Count;
        //while循环的目的 是进行n次重新下载 避免网络异常时 下载失败
        while (downLoadList.Count > 0 && reDownLoadMaxNum > 0)
        {
            for (int i = 0; i < downLoadList.Count; i++)
            {
                isOver = false;
                await Task.Run(() => {
                    isOver = DownLoadFile(downLoadList[i], localPath + downLoadList[i]);
                });
                if (isOver)
                {
                    //2.要知道现在下载了多少 结束与否
                    updatePro(++downLoadOverNum, downLoadMaxNum);
                    tempList.Add(downLoadList[i]);//下载成功记录下来
                }
            }
            //把下载成功的文件名 从待下载列表中移除
            for (int i = 0; i < tempList.Count; i++)
                downLoadList.Remove(tempList[i]);

            --reDownLoadMaxNum;
        }

        //所有内容都下载完了 告诉外部是否下载完成
        overCallBack(downLoadList.Count == 0);
    }

    private bool DownLoadFile(string fileName, string localPath)
    {
        try
        {
            //1.创建一个FTP连接 用于下载
            FtpWebRequest req = FtpWebRequest.Create(new Uri("ftp://192.168.50.49/AB/PC/" + fileName)) as FtpWebRequest;
            //2.设置一个通信凭证 这样才能下载(如果有匿名账号 可以不设置凭证 但是实际开发中 建议 还是不要设置匿名账号)
            NetworkCredential n = new NetworkCredential("MrTang", "MrTang123");
            req.Credentials = n;
            //3.其它设置
            //  设置代理为null
            req.Proxy = null;
            //  请求完毕后 是否关闭控制连接
            req.KeepAlive = false;
            //  操作命令-下载
            req.Method = WebRequestMethods.Ftp.DownloadFile;
            //  指定传输的类型 2进制
            req.UseBinary = true;
            //4.下载文件
            //  ftp的流对象
            FtpWebResponse res = req.GetResponse() as FtpWebResponse;
            Stream downLoadStream = res.GetResponseStream();
            using (FileStream file = File.Create(localPath))
            {
                //一点一点的下载内容
                byte[] bytes = new byte[2048];
                //返回值 代表读取了多少个字节
                int contentLength = downLoadStream.Read(bytes, 0, bytes.Length);

                //循环下载数据
                while (contentLength != 0)
                {
                    //写入到本地文件流中
                    file.Write(bytes, 0, contentLength);
                    //写完再读
                    contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
                }

                //循环完毕后 证明下载结束
                file.Close();
                downLoadStream.Close();

                return true;
            }
        }
        catch (Exception ex)
        {
            print(fileName + "下载失败" + ex.Message);
            return false;
        }

    }

    private void OnDestroy()
    {
        instance = null;
    }

    //AB包信息类
    private class ABInfo
    {
        public string name;//AB包名字
        public long size;//AB包大小
        public string md5;//AB包md5码

        public ABInfo(string name, string size, string md5)
        {
            this.name = name;
            this.size = long.Parse(size);
            this.md5 = md5;
        }
    }

完善后的代码.

MD5的事情完了,还有一个后续问题,那就是如果用自带软件打AB包的话,最好打到StreamingAssetPath.就是说,你将ab包打到其他文件夹也没关系,后续再写程序将其调到StreamingAssetPath.下面的程序,就是解决这个问题的.

(对于StreamingAssetPath,无法用C#的File方式在WebGL和Android平台下访问此文件夹。如果想在这两个平台下访问,应当使用UnityWebRequest类(或使用WWW类,但UnityWebRequest正是unity平台下WWW类的升级版,因此建议使用UnityWebRequest)
ArtRes里面有打的AB包,现在要移动到SteamingAssets.

[MenuItem("AB包工具/移动选中资源到StreamingAssets中")]
    private static void MoveABToStreamingAssets()
    {
        //通过编辑器Selection类中的方法 获取再Project窗口中选中的资源 
        Object[] selectedAsset = Selection.GetFiltered(typeof(Object), SelectionMode.DeepAssets);
        //如果一个资源都没有选择 就没有必要处理后面的逻辑了
        if (selectedAsset.Length == 0)
            return;
        //用于拼接本地默认AB包资源信息的字符串
        string abCompareInfo = "";
        //遍历选中的资源对象
        foreach (Object asset in selectedAsset)
        {
            //通过Assetdatabase类 获取 资源的路径
            string assetPath = AssetDatabase.GetAssetPath(asset);
            //截取路径当中的文件名 用于作为 StreamingAssets中的文件名
            string fileName = assetPath.Substring(assetPath.LastIndexOf('/'));

            //判断是否有.符号 如果有 证明有后缀 不处理
            if (fileName.IndexOf('.') != -1)
                continue;
            //你还可以在拷贝之前 去获取全路径 然后通过FIleInfo去获取后缀来判断 这样更加的准确

            //利用AssetDatabase中的API 将选中文件 复制到目标路径
            AssetDatabase.CopyAsset(assetPath, "Assets/StreamingAssets" + fileName);

            //获取拷贝到StreamingAssets文件夹中的文件的全部信息
            FileInfo fileInfo = new FileInfo(Application.streamingAssetsPath + fileName);
            //拼接AB包信息到字符串中
            abCompareInfo += fileInfo.Name + " " + fileInfo.Length + " " + CreateABCompare.GetMD5(fileInfo.FullName);
            //用一个符号隔开多个AB包信息
            abCompareInfo += "|";
        }
        //去掉最后一个|符号 为了之后拆分字符串方便
        abCompareInfo = abCompareInfo.Substring(0, abCompareInfo.Length - 1);
        //将本地默认资源的对比信息 存入文件
        File.WriteAllText(Application.streamingAssetsPath + "/ABCompareInfo.txt", abCompareInfo);
        //刷新窗口
        AssetDatabase.Refresh();
    }

导完了SteamingAssetPath,接下来了解下persistentDataPath

Application.persistentDataPath,热更的重要路径,该文件夹可读可写,在移动端唯一一个可读写操作的文件夹。

移动端可以将本地的资源(资源MD5值配置表)等一些文件放到StreamingAssets文件夹下,通过Copy到persistentDataPath下与服务器的版本文件配置表作比对,完成资源的热更。

为啥不在StreamingAsset文件夹下直接操作?因为该文件夹只读,不可写,资源无法更新进去。

为什么不在persistentDataPath文件夹操作,因为该文件夹是apk安装以后,才会形成的一个文件夹,无法提前创建。

本来以为,persistentDataPath文件夹,是每次打开游戏,形成的,里面的数据是只在打开游戏期间临时保存,关闭游戏就会消除,今天做个小测试,原来该文件夹是安装完apk以后形成,里面的数据持久存在。

怎么将StreamingDataPath和PersitentDataPath用热更联系起来呢?


 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值