# UnityDownloadHandlerScriptDemo **Repository Path**: zhangkaipeng/UnityDownloadHandlerScriptDemo ## Basic Information - **Project Name**: UnityDownloadHandlerScriptDemo - **Description**: UnityDownloadHandlerScriptDemo - **Primary Language**: C# - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-12-07 - **Last Updated**: 2022-12-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 使用UnityWebRequest来实现断点续传,并且不会产生额外GC的一个Demo 关键词:Unity UnityWebRequest 断点续传 无额外GC 本Demo,基于Unity2018.4.36f1版本 ## 原理 断点续传指的是,如果下载过程中断,支持下次从中断的部分继续下载未完成的部分,而不用再重新从头开始下载。 ### 实现流程 * 假设我们要下载remoteFile到本地的localFile * 首先读取localFile文件的大小:localFileSize,单位字节,localFileSize就表示之前已经下载的大小 * 如果localFile不存在,那么localFileSize就是0 * 通过HTTP Range请求头,从未下载的部分继续下载 * 即请求从localFileSize到文件末尾的部分来下载 ### HTTP Range Request Header ```Text Range 是一个请求首部,告知服务器返回文件的哪一部分。 从range-start请求到文件结尾的语法: Range: =- 参数解释: : 范围所采用的单位,通常是字节(bytes)。 : 一个整数,表示在特定单位下,范围的起始值。 ``` Range请求头的完整参考见: ### UnityWebRequest中,设置Range请求头的代码 ```C# ... var LocalFileSize = new System.IO.FileInfo(localFile).Length; // “bytes=-”格式的意思是,请求从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)崩溃。 代码如下 ```C# // 前面省略细节 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作为下载的缓冲区。 ```C# 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,内存是平稳的。 ```Text public DownloadHandlerScript (byte[] preallocatedBuffer); 创建可通过重复使用预分配的缓冲区将数据传递给回调的 DownloadHandlerScript。 此构造函数会将此 DownloadHandlerScript 置于预分配模式。这会影响 DownloadHandler.ReceiveData 回调的操作。 在预分配模式下,系统将重复使用 preallocatedBuffer 字节数组以将数据传递给 DownloadHandler.ReceiveData 回调,而非每次都会分配新缓冲区。系统不会在每次使用时都将数组归零,因此必须使用 DownloadHandler.ReceiveData 的 dataLength 参数来查看哪些字节是新字节。 在这种模式下,DownloadHandlerScript 不会在下载或处理 HTTP 响应数据时分配任何内存。如果您的用例需要避免垃圾收集操作,建议您采用预分配模式。 ``` 参考: 完整的代码见: 整个实现参考了: ## 注意事项 ### Strip的问题 问题:Android真机上,继承自DownloadHandlerScript的子类无法收到任何回调的问题 原因:开启了Managed code stripping,且“Managed Stripping Level”在Medium及以上。导致回调代码被错误的strip掉了,回调无法触发。 解决方案:在Assets/目录中,link.xml中(如果不存在,就自行添加),添加下面的部分,不裁剪UnityWebRequestModule的相关类。 ```xml ``` 关于Managed code stripping的相关参考: ### HTTP: 416 问题:下载文件,HTTP的返回码返回了416 ```Text HTTP 416 Range Not Satisfiable 错误状态码意味着服务器无法处理所请求的数据区间。最常见的情况是所请求的数据区间不在文件范围之内,也就是说,Range 首部的值,虽然从语法上来说是没问题的,但是从语义上来说却没有意义。 ``` 解释:我们下载一个a.zip,有两种方式: 一种是localFile命名为a.zip,直接开始断点续传下载 另一种是命名为a.zip.tmp,开始断点续传下载,下载完成之后,重命名为a.zip。 第一种方式,有可能本地文件已经下载完成,断点续传的时候,range-start的值已经就是文件的完整大小了,这个时候,就会返回HTTP416。这个时候就要自己判断一下,文件是否已经下载完毕了。 416返回码参考: