Unity的资源下载与缓存系统的实现

在Unity开发的游戏中,我们经常会下载,缓存,更新资源文件,达到更新游戏内容的目的。(主要是AssetBundle文件)

Unity中已经提供了简单的下载与缓存功能,WWW.LoadFromCacheOrDownload可以满足基本的文件下载和缓存的需求。

为什么要自己重新造轮子

为什么还要自己重新设计实现这个下载缓存系统呢。

对我们的项目来说,WWW.LoadFromCacheOrDownload有几个比较重大的缺点:

  1. 无法取得下载的资源文件的路径,导致无法实现对文件的管理。

  2. 在使用WWW.LoadFromCacheOrDownload前,需要等待Caching.ready,随着缓存文件的增加,Caching.ready的速度会越来越慢。

  3. WWW不支持断点续传。断点续传需要设置HTTP Header,但是WWW会把设置了Header的Request全部作为POST来处理。 我们的游戏需要应对各种复杂的网络环境,在网络很差的环境下,一旦文件下载失败就从头开始下载是不可接受的。

  4. WWW可设置的参数非常少,例如Timeout不可设置。

HTTP Client的选择

我们对比了几种Client,总结如下表。

Libray 来源 优点 缺点
WWW Unity内置 可以使用Unity自带Cache功能;不会增加build的size 设置Header会导致Request变成POST,因此无法用于CDN下载;无法设置Timeout,Redirect次数等参数
UnityWebRequest Unity内置 不会增加build的size 无法使用Unity的cache,需要自己实现cache;Timeout无法设置,无法使用HTTPS的证书;还处于Experimental中,经常会导致程序崩溃
BestHTTP 第三方Asset 可以断点续传;文档非常丰富;Redirect,Timeout等参数可以设置;可以使用HTTPS的self-signed certificate 会增加数MB的build size;需要自己实现cache system
HttpWebRequest .NET / Mono System.Net namespace,可以减少build size 无法在Coroutines中使用

最终我们选择了BestHTTP,这是一个付费的Asset,价格55美元。好在并不需要每人一个license。给官方发邮件确认后,得知一个团队(公司)只需要购买一个license就可以了。

缓存系统的设计与实现

由于不使用WWW.LoadFromCacheOrDownload,所以需要自己实现缓存系统。
基本的需求是,游戏逻辑向缓存系统请求一个资源文件list:

  1. 当服务器的文件本地不存在时,下载并缓存。
  2. 当服务器的文件版本与本地不一致时,下载更新,替换本地文件
  3. 服务文件与本地文件一致,不下载,直接使用本地的缓存文件
  4. 检查下载文件完整性

源文件版本控制

AssetBundle的依赖关系我们可以从Single Manifest文件中取得。Single Manifest还包含了每个AssetBundle文件的Hash值,用 AssetBundleManifest.GetAssetBundleHash方法可以取得该HASH值。可以用于确认本地文件和服务器文件的版本是否一致。
本想直接利用这个Hash值进行文件完整性验证,但是这个Hash的算法既不是MD5也不是SHA1,自己无法计算,所以就放弃了这个想法。
我们在AssetBundle build的同时,生成一个ResourceList的文本文件,里面记录文件的CRC和size。格式如下:

filename|CRC|size

加入文件大小的原因是,想要在下载的界面上显示剩余下载的大小,而SingleManifest文件中并没有提供文件的大小。 在每次下载前,先需要下载SingleManifest文件和ResourceList文件,然后再判定哪些文件需要下载,哪些文件从缓存中读取。
f:id:lvmingbei:20160427163004p:plain

文件下载分为,新文件下载,文件更新下载,断点续传下载,从本地读取几种情况。判定方法如下图。
f:id:lvmingbei:20160427165724p:plain

下载的文件都存储于Application.persistentDataPath中,该目录不会被系统清理缓存等操作被清空。但是注意Android手机如果SD卡被拔出,可能会造成之前写入的文件无法访问。

下载断点续传的实现

根据HTTP协议,在HTTP Request的Header中加入Range: bytes=start-end(单位是字节),就可以从上次中断的位置继续下载。
本地写入文件时,选择Append模式即可。

保持下载不中断

当下载文件较大时,下载可能会消耗比较长的时间。如果用户把手机放在一旁,手机自动进入休眠导致下载中断,或者切换到别的程序,而导致下载中断,会导致比较差的游戏体验。所以这个方面也要进行处理。

让设备不进入休眠

Screen.sleepTimeout = SleepTimeout.NeverSleep;   

我们可以使用这个API来设置,在下载期间不让机器进入休眠状态。当下载结束后,再将这个设置恢复成默认。

当程序切换

Application.runInBackground = true;

使用这个API可以让程序切换到后台的时候仍然保持下载继续。同样不要忘记在下载结束后,把设置恢复。

网络连接情况的监测

当设备进行网络切换的时候,例如,从蜂窝移动网络切换到Wifi,从Wifi A切换到Wifi B,这种情况下,原本的连接一定会超时失败,但是我们不想等待超时,当网络连接发生变化时候,主动放弃连接,并进行Retry,以提高下载效率。
通过Unity中Application.internetReachability可以获取到当前网络状态,我们可以将其保存下来,然后在Update方法中进行检测,就可以得知网络是否发生了变化。

void Update ()
{
    if (Application.internetReachability != m_reachability) {
        m_request.Abort ();
    }
    m_reachability = Application.internetReachability;
}