使用UnityWebRequest来实现断点续传,并且不会产生额外GC的一个Demo

1 年前

本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请求头的完整参考见: developer.mozilla.org/z

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 参数来查看哪些字节是新字节。