使用UnityWebRequest来实现断点续传,并且不会产生额外GC的一个Demo
本Demo,基于Unity2018.4.36f1版本
原理
断点续传指的是,如果下载过程中断,支持下次从中断的部分继续下载未完成的部分,而不用再重新从头开始下载。
实现流程
- 假设我们要下载remoteFile到本地的localFile
- 首先读取localFile文件的大小:localFileSize,单位字节,localFileSize就表示之前已经下载的大小
- 如果localFile不存在,那么localFileSize就是0
- 通过HTTP Range请求头,从未下载的部分继续下载
- 即请求从localFileSize到文件末尾的部分来下载
HTTP Range Request Header
Range 是一个请求首部,告知服务器返回文件的哪一部分。 从range-start请求到文件结尾的语法: Range: <unit>=<range-start>- 参数解释: <unit>: 范围所采用的单位,通常是字节(bytes)。 <range-start>: 一个整数,表示在特定单位下,范围的起始值。
Range请求头的完整参考见: https:// developer.mozilla.org/z h-CN/docs/Web/HTTP/Headers/Range
UnityWebRequest中,设置Range请求头的代码
...
var LocalFileSize = new System.IO.FileInfo(localFile).Length;
// “bytes=<range-start>-”格式的意思是,请求从range-start到文件结尾的所有bytes
// 这里就是从本地已下载文件大小,请求到文件末尾的所有bytes
unityWebRequest.SetRequestHeader("Range", "bytes=" + LocalFileSize + "-");
// 请求服务器,执行下载
unityWebRequest.SendWebRequest();
...
代码实现细节
通过搜索,网上能找到的UnityWebRequest断点续传代码模板,大概两种的实现方式:
- 不继承DownloadHandlerScript, 直接使用 UnityWebRequest.downloadHandler.data 的方式。
- 继承 DownloadHandlerScript 类的方式
不推荐的方式(有严重的GC问题)
不继承DownloadHandlerScript, 直接使用 UnityWebRequest.downloadHandler.data的方式是不推荐的,核心原因是下载多大的文件,就会分配多大的内存! 而Unity的Mono内存,被撑高之后是无法回落的。下载的文件过大,甚至会引起OOM(out of memory)崩溃。
代码如下
// 前面省略细节
var req = UnityWebRequest.Get(fileUrl);
req.SetRequestHeader("Range", "bytes=" + fileLength + "-");
var op = req.SendWebRequest();
// 这个是已写入的bytes的偏移量
var index = 0;
while (!op.isDone)
yield return null;
// 问题在这里,这个data和被下载文件的大小是一样的
byte[] buff = req.downloadHandler.data;
if (buff != null)
var length = buff.Length - index;
// 每次根据上次写入记录的偏移量,写入新下载的data
fs.Write(buff, index, length);
index += length;
fileLength += length;
onProgress(fileLength);
// 后面省略细节
推荐的方式
继承DownloadHandlerScript,并且在基类构造函数中,传递一个固定大小的Buffer作为下载的缓冲区。
public class DownloadHandlerFileRange : DownloadHandlerScript
// base(new byte[1024 * 1024])就是传递下载用的固定大小的Buffer
public DownloadHandlerFileRange(string path, UnityWebRequest request) : base(new byte[1024 * 1024])
// 收到数据,写文件
protected override bool ReceiveData(byte[] data, int dataLength)
FileStream.Write(data, 0, dataLength);
return true;
}
查看DownloadHandlerScript的构造函数文档,可以看到,上述的代码,在下载过程中,只分配了缓冲区大小的内存,下载的文件再大,也不会造成超过缓冲区大小的内存分配。这样整个下载过程中,不会产生新的GC,内存是平稳的。
public DownloadHandlerScript (byte[] preallocatedBuffer);
创建可通过重复使用预分配的缓冲区将数据传递给回调的 DownloadHandlerScript。
此构造函数会将此 DownloadHandlerScript 置于预分配模式。这会影响 DownloadHandler.ReceiveData 回调的操作。
在预分配模式下,系统将重复使用 preallocatedBuffer 字节数组以将数据传递给 DownloadHandler.ReceiveData 回调,
而非每次都会分配新缓冲区。系统不会在每次使用时都将数组归零,
因此必须使用 DownloadHandler.ReceiveData 的 dataLength 参数来查看哪些字节是新字节。