行业资讯
📅 2026/8/7 6:22:16
UnityWebRequest实战:从基础GET到高级断点续传
1. 项目概述为什么UnityWebRequest是网络交互的基石在Unity开发中无论是加载一个远程的配置文件、下载一张贴图还是从服务器拉取玩家的存档数据网络请求都是绕不开的核心功能。很多开发者尤其是刚接触Unity不久的朋友可能会从最基础的WWW类开始但很快就会发现它在现代项目中的局限性功能单一、控制粒度粗、内存管理不够友好。而UnityWebRequest作为Unity官方力推的下一代网络API正是为了解决这些问题而生的。它提供了一个更模块化、更高效、也更强大的网络请求框架。这个标题“UnityWebRequest实战从基础GET到高级断点续传”精准地勾勒出了一条从入门到精通的技能成长路径。它不仅仅是教你调用几个API而是希望你理解在网络通信这个看似简单的“黑盒”背后如何实现精细化的控制。一个简单的GET请求是起点它关乎稳定性和基础错误处理而断点续传则是终点它考验的是你对HTTP协议细节、文件I/O操作和资源管理的综合运用能力。在实际项目中小到热更新资源下载大到游戏本体包体的分发都离不开这套技术栈。如果你曾为下载大文件时网络波动导致前功尽弃而烦恼或者想给玩家提供更平滑的下载体验那么深入理解并实现断点续传就是一个必须攻克的关卡。2. UnityWebRequest核心架构与设计哲学2.1 模块化设计告别“黑盒”操作与老旧的WWW类将所有东西URL、结果、错误打包在一个对象里不同UnityWebRequest采用了清晰的职责分离设计。你可以把它想象成一个流水线每个环节都有专门的工人负责。UnityWebRequest 这是流水线的总指挥。它持有请求的目标URL、方法GET、POST等、请求头并且管理着两个关键工人UploadHandler和DownloadHandler。它的核心职责是建立连接、管理请求生命周期和提供状态查询如isNetworkError,isHttpError,responseCode。UploadHandler 负责处理“上传”数据。比如当你需要POST一个JSON字符串或者上传一个文件时就需要配置它。常见的子类有UploadHandlerRaw用于原始字节数据和UploadHandlerFile用于高效上传文件。DownloadHandler 负责处理“下载”的数据。这是本文的重点。它接收来自服务器的原始字节流并提供多种方式让你获取这些数据。基础的有DownloadHandlerBuffer将数据存到内存字节数组、DownloadHandlerTexture直接解析为Texture2D、DownloadHandlerAudioClip解析为音频片段。而我们实现断点续传则需要继承更底层的DownloadHandlerScript来自定义数据接收和存储的逻辑。这种设计的好处是显而易见的资源可控性能更优。使用DownloadHandlerBuffer下载一个大文件意味着整个文件都会先被载入内存对于几十兆甚至上百兆的资源这是不可接受的。而通过自定义的DownloadHandler我们可以直接将收到的字节流写入磁盘文件内存中只保留一小块缓冲区从而实现了流式处理内存占用是恒定且微小的。2.2 协程与异步操作如何优雅地等待网络网络请求是典型的I/O密集型操作会阻塞主线程。Unity提供了SendWebRequest()方法它立即发起请求并返回一个AsyncOperation对象。最标准的做法是使用协程Coroutine来等待其完成。IEnumerator DownloadFile(string url) { using (UnityWebRequest request UnityWebRequest.Get(url)) { request.downloadHandler new DownloadHandlerBuffer(); yield return request.SendWebRequest(); if (request.result UnityWebRequest.Result.ConnectionError || request.result UnityWebRequest.Result.ProtocolError) { Debug.LogError($下载失败: {request.error}); } else { byte[] results request.downloadHandler.data; // 处理下载的数据 } } }这里有几个关键点using语句UnityWebRequest实现了IDisposable接口。使用using语句可以确保无论请求成功还是失败网络连接和本地缓冲等非托管资源都能被及时释放这是避免内存泄漏的好习惯。request.result 在较新版本的Unity中推荐使用request.result这个枚举来替代旧的isNetworkError和isHttpError属性进行错误判断它的分类更加清晰。协程的局限性 协程虽然方便但它依赖于MonoBehaviour和游戏对象。如果你的网络模块是一个纯粹的、不依赖于游戏生命周期的服务类或者你需要更强大的取消、组合、回调控制可以考虑结合C#的async/await模式需要Unity 2018.3以上版本并通过SendWebRequest().ToAsyncOperation()等方式进行适配或使用第三方库但这会引入更多的复杂性。对于大多数游戏内资源下载场景协程已经完全够用且直观。3. 基础GET请求的实战与深度优化3.1 标准GET请求流程与错误处理一个健壮的GET请求远不止调用一个方法那么简单。让我们构建一个更工业级的示例public IEnumerator RobustGetRequest(string url, System.Actionstring onSuccess, System.Actionstring onError) { using (UnityWebRequest request new UnityWebRequest(url, GET)) { // 1. 配置DownloadHandler DownloadHandlerBuffer dhb new DownloadHandlerBuffer(); request.downloadHandler dhb; // 2. 可选设置超时时间单位秒。默认为0不超时生产环境务必设置 request.timeout 30; // 3. 可选设置请求头例如用户代理 request.SetRequestHeader(User-Agent, MyUnityGame/1.0); // 4. 发起请求并等待 yield return request.SendWebRequest(); // 5. 精细化结果判断 switch (request.result) { case UnityWebRequest.Result.InProgress: // 通常不会进入这里因为yield return已经等待完成了 break; case UnityWebRequest.Result.Success: string text dhb.text; // 获取文本内容 // 或者 byte[] data dhb.data; // 获取二进制内容 onSuccess?.Invoke(text); break; case UnityWebRequest.Result.ConnectionError: onError?.Invoke($网络连接错误: {request.error}。 URL: {url}); break; case UnityWebRequest.Result.ProtocolError: // HTTP错误如404 500 onError?.Invoke($HTTP协议错误 [{request.responseCode}]: {request.error}。 URL: {url}); break; case UnityWebRequest.Result.DataProcessingError: onError?.Invoke($数据处理错误如下载Handler问题: {request.error}); break; default: onError?.Invoke($未知错误: {request.result}); break; } } }注意关于UnityWebRequest.Get 你可能会看到UnityWebRequest.Get(string url)这个便捷方法。它内部创建了一个UnityWebRequest并设置了DownloadHandlerBuffer。对于简单请求很方便但如果你需要自定义DownloadHandler比如用DownloadHandlerFile直接存文件或设置其他属性直接使用new UnityWebRequest(url, “GET”)构造函数会更灵活。3.2 超时、重试与进度监控超时控制request.timeout属性至关重要。在网络环境不佳时一个永不返回的请求会挂起你的协程。设置一个合理的超时时间如30秒并在超时后触发重试或失败回调是提升用户体验的关键。重试机制 对于非幂等的GET请求实现简单的重试机制是提高成功率的好方法。int maxRetryCount 3; int currentRetry 0; float retryDelay 2f; while (currentRetry maxRetryCount) { // ... 发起request ... yield return request.SendWebRequest(); if (request.result UnityWebRequest.Result.Success) { // 成功跳出循环 break; } else if (request.result UnityWebRequest.Result.ConnectionError) { // 仅对连接错误进行重试 currentRetry; Debug.LogWarning($请求失败第{currentRetry}次重试...); yield return new WaitForSeconds(retryDelay); // 可以在这里增加退避策略比如 retryDelay * 1.5f; } else { // HTTP错误如404通常重试无意义直接失败 break; } }进度监控UnityWebRequest提供了downloadProgress取值0.0到1.0属性。你可以在协程等待期间每帧或每隔一段时间检查这个属性来更新UI进度条。UnityWebRequest request ...; request.SendWebRequest(); while (!request.isDone) { float progress request.downloadProgress; // 更新UI进度条例如progressBar.value progress; yield return null; // 等待一帧 } // 请求完成进行最终处理重要提示downloadProgress对于直接写入文件的DownloadHandlerFile或我们自定义的Handler同样有效因为基类DownloadHandler提供了GetProgress()虚方法供重写。4. 断点续传的原理与HTTP协议支撑4.1 断点续传到底解决了什么问题想象一下你在手机上下载一个2GB的游戏更新包进度到90%时突然进入地铁隧道网络中断了。没有断点续传你只能从头再来既浪费流量又消耗用户耐心。断点续传的核心价值就在于从已下载的位置继续下载而非重新开始。要实现这个功能需要客户端和服务器端的协同客户端 需要知道本地已下载了多大文件即“断点”的位置。客户端 需要告诉服务器“请从第N个字节之后开始发送数据”。服务器 需要支持并理解客户端的这个请求并能够从文件的指定位置开始返回数据。4.2 HTTP协议中的关键角色Range与Content-Range这一切都依赖于HTTP/1.1协议中定义的Range Requests范围请求功能。客户端请求头Range当客户端需要获取文件的一部分时会在GET请求中添加Range头。其格式为Range: bytesstart-例如如果本地已有一个500KB的文件想继续下载后续部分请求头就是Range: bytes524288-因为500KB 500 * 1024 512000字节等等这里有个坑1KB是1024字节所以500KB是512000字节。但通常我们通过FileInfo.Length获取的是准确的字节数假设是524288字节即512KB。这里务必使用准确的字节数。服务器响应Content-Range与206 Partial Content如果服务器支持范围请求对于上面的例子它会返回状态码206 Partial Content而不是正常的200 OK。同时在响应头中会包含Content-Range告诉客户端返回的是哪一部分HTTP/1.1 206 Partial Content Content-Range: bytes 524288-10485759/10485760 Content-Length: 9961472这个响应头的意思是“我返回的是从第524288字节到第10485759字节的数据共9961472字节整个文件总大小是10485760字节约10MB”。Content-Length表示的是本次响应体的长度而不是整个文件的长度。服务器不支持怎么办如果服务器不支持Range请求它会直接忽略Range头返回整个文件状态码200 OK和完整的Content-Length。我们的代码必须能处理这种情况通常的策略是如果收到200说明不支持断点续传则覆盖本地文件从头开始下载。4.3 设计思路自定义DownloadHandlerFileRangeUnity内置的DownloadHandlerFile虽然能直接写文件但它不支持设置Range头也无法在已有文件末尾追加数据。因此我们需要继承DownloadHandlerScript这个更灵活的基类来自定义一个支持断点续传的Handler。我们的DownloadHandlerFileRange需要完成以下核心任务构造时 检查本地目标文件是否存在及大小并据此为关联的UnityWebRequest设置Range请求头。接收数据时 将收到的数据块byte[]以追加模式写入本地文件流。计算进度 需要知道文件总大小。由于服务器在206响应中返回的是剩余部分的大小我们需要将其与本地已有文件大小相加得到完整的总大小。资源管理 妥善打开和关闭文件流确保在任何情况下成功、失败、强制中断文件句柄都能被释放避免文件被锁定。5. 手把手实现DownloadHandlerFileRange5.1 类结构与构造函数让我们基于网络搜索到的代码骨架进行详细地实现和解析。using UnityEngine; using UnityEngine.Networking; using System.IO; using System; public class DownloadHandlerFileRange : DownloadHandlerScript { private string _saveFilePath; private FileStream _fileStream; private UnityWebRequest _linkedRequest; private long _localFileSize 0; // 本地已存在文件的大小 private long _totalFileSize 0; // 整个远程文件的总大小 private long _currentDownloadedSize 0; // 当前已下载的总大小本地本次 private float _lastSpeedUpdateTime; private long _lastSpeedUpdateDataSize; private float _downloadSpeedBytesPerSecond; /// summary /// 当前下载速度字节/秒 /// /summary public float DownloadSpeedBytes _downloadSpeedBytesPerSecond; /// summary /// 当前下载速度KB/秒保留两位小数 /// /summary public float DownloadSpeedKB (float)Math.Round(_downloadSpeedBytesPerSecond / 1024f, 2); /// summary /// 文件总大小字节 /// /summary public long TotalFileSize _totalFileSize; /// summary /// 当前下载进度[0, 1] /// /summary public float Progress GetProgress(); /// summary /// 文件开始下载事件当获取到总大小时触发 /// /summary public event Action OnDownloadStarted; /// summary /// 构造函数 /// /summary /// param namesaveFilePath文件完整保存路径/param /// param namelinkedRequest关联的UnityWebRequest对象用于设置Range头/param /// param namebufferSize接收数据缓冲区大小默认1MB。建议与Unity内部优化块大小匹配/param public DownloadHandlerFileRange(string saveFilePath, UnityWebRequest linkedRequest, int bufferSize 1024 * 1024) : base(new byte[bufferSize]) // 将缓冲区传递给基类 { _saveFilePath saveFilePath; _linkedRequest linkedRequest; // 1. 检查并获取本地已下载部分的大小 if (File.Exists(_saveFilePath)) { try { FileInfo fileInfo new FileInfo(_saveFilePath); _localFileSize fileInfo.Length; _currentDownloadedSize _localFileSize; Debug.Log($找到已存在文件大小: {_localFileSize} 字节); } catch (Exception e) { Debug.LogError($获取本地文件信息失败: {e.Message}); // 如果文件损坏或无法访问可以选择删除从头开始 // File.Delete(_saveFilePath); // _localFileSize 0; } } // 2. 设置Range请求头告诉服务器从哪里开始发送 // 格式bytes[已下载大小]- _linkedRequest.SetRequestHeader(Range, $bytes{_localFileSize}-); Debug.Log($设置Range头: bytes{_localFileSize}-); // 3. 以追加模式打开文件流准备写入 try { // FileMode.Append: 打开现有文件并定位到文件尾若文件不存在则创建新文件。 // FileAccess.Write: 只写权限。 _fileStream new FileStream(_saveFilePath, FileMode.Append, FileAccess.Write); } catch (Exception e) { Debug.LogError($无法创建或打开文件流: {e.Message}); // 这里可以抛出异常或通过其他方式通知调用者初始化失败 } } }关键点解析缓冲区大小 基类DownloadHandlerScript的构造函数需要一个字节数组作为缓冲区。这个缓冲区是Unity底层填充数据的地方ReceiveData方法中传入的data数组就是它。在Unity 2017.2.1p1之后这个缓冲区的大小决定了每次ReceiveData回调能接收的最大数据量最大1MB。设置成1MB是一个在性能和内存占用间取得平衡的常见值。FileMode.Append 这是实现“续传”的关键。如果文件存在写入位置就在文件末尾如果不存在则创建新文件。这完美契合了我们的需求。异常处理 文件I/O操作充满不确定性磁盘已满、权限不足、文件被占用等。在生产代码中必须用try-catch包裹并进行适当的错误处理例如通知用户或触发重试逻辑。5.2 核心回调方法的重写自定义的下载逻辑主要发生在重写的几个受保护的方法中。/// summary /// 当收到响应头知道本次传输的数据大小时调用。 /// 注意参数contentLength在文件大于2GB时可能溢出int范围因此优先从响应头读取。 /// /summary protected override void ReceiveContentLength(int contentLength) { // 优先从响应头获取Content-Length这是本次传输部分的大小 string contentLengthHeader _linkedRequest.GetResponseHeader(Content-Length); long thisPartSize 0; if (!string.IsNullOrEmpty(contentLengthHeader)) { if (!long.TryParse(contentLengthHeader, out thisPartSize)) { Debug.LogWarning($无法解析Content-Length响应头: {contentLengthHeader}使用参数值。); thisPartSize contentLength; // 回退到可能溢出的参数 } } else { // 如果服务器没返回Content-Length对于chunked传输则使用参数值 thisPartSize contentLength; } // 关键总文件大小 本地已有大小 本次将要下载的部分大小 _totalFileSize _localFileSize thisPartSize; Debug.Log($文件总大小计算: {_localFileSize} (本地) {thisPartSize} (本次) {_totalFileSize} 字节); // 初始化下载速度计算变量 _lastSpeedUpdateTime Time.time; _lastSpeedUpdateDataSize _currentDownloadedSize; // 触发开始下载事件通知外部可以获取到总大小了 OnDownloadStarted?.Invoke(); } /// summary /// 每次收到数据块时调用。这是写入文件的核心。 /// /summary protected override bool ReceiveData(byte[] data, int dataLength) { // data可能是nulldataLength为0表示数据接收完毕 if (data null || dataLength 0 || _linkedRequest.responseCode 400) { return false; // 返回false可能中止下载但通常让流程自然结束 } try { // 将收到的数据块写入文件流 _fileStream.Write(data, 0, dataLength); _fileStream.Flush(); // 可选立即将缓冲区数据写入磁盘避免断电丢失但影响性能。 // 更常见的做法是依赖FileStream的自动缓冲定期或在完成时Flush。 // 更新当前已下载总量 _currentDownloadedSize dataLength; // 计算实时下载速度每秒更新一次 float currentTime Time.time; if (currentTime - _lastSpeedUpdateTime 1.0f) { long dataDiff _currentDownloadedSize - _lastSpeedUpdateDataSize; float timeDiff currentTime - _lastSpeedUpdateTime; _downloadSpeedBytesPerSecond dataDiff / timeDiff; _lastSpeedUpdateTime currentTime; _lastSpeedUpdateDataSize _currentDownloadedSize; // 可以在这里触发速度更新事件 // OnSpeedUpdated?.Invoke(DownloadSpeedKB); } } catch (IOException e) { Debug.LogError($写入文件时发生IO异常: {e.Message}); // 写入失败应中止请求 _linkedRequest.Abort(); return false; } catch (Exception e) { Debug.LogError($写入文件时发生未知异常: {e.Message}); _linkedRequest.Abort(); return false; } return true; // 返回true表示继续接收数据 } /// summary /// 提供下载进度0到1。 /// /summary protected override float GetProgress() { if (_totalFileSize 0) return 0f; // 避免除零 // 注意使用_currentDownloadedSize它包含了本地已有部分 return Mathf.Clamp01((float)_currentDownloadedSize / _totalFileSize); } /// summary /// 下载完成时调用用于清理资源。 /// /summary protected override void CompleteContent() { Debug.Log(下载完成清理文件流。); Cleanup(); } /// summary /// 析构函数作为资源清理的最后保障。 /// /summary ~DownloadHandlerFileRange() { Cleanup(); } private void Cleanup() { if (_fileStream ! null) { _fileStream.Flush(); _fileStream.Close(); // 调用Close也会自动调用Dispose _fileStream.Dispose(); _fileStream null; } _downloadSpeedBytesPerSecond 0; }关键点解析ReceiveContentLength中的大小计算 这是最容易出错的地方。当服务器返回206时contentLength参数和Content-Length响应头都只代表剩余部分的大小。必须加上本地文件大小_localFileSize才是正确的总大小。同时直接使用int contentLength参数有2GB溢出的风险所以代码优先从响应头读取string并解析为long。ReceiveData的性能与稳健性 写入文件是I/O操作可能会阻塞。虽然Unity会在子线程中调用此回调但写入操作本身是同步的。对于极高速的下载频繁的Flush()会严重影响性能。通常游戏下载场景下可以每接收一定量数据如10MB或每秒Flush()一次在性能和安全性间折衷。异常处理至关重要一旦写入失败应立即中止请求Abort()防止后续数据继续到来导致更多问题。进度计算GetProgress()被UnityWebRequest.downloadProgress属性调用。我们的实现使用了_currentDownloadedSize / _totalFileSize这能准确反映从零开始的整体进度。资源清理CompleteContent()在下载成功完成时被调用。但请注意如果外部代码主动调用UnityWebRequest.Abort()或Dispose()这个回调可能不会被调用因此我们在析构函数中也加入了清理逻辑作为最后防线但更好的做法是提供一个公共的Dispose方法供外部显式调用。5.3 如何使用这个自定义Handler封装好了DownloadHandlerFileRange之后使用起来就非常清晰了IEnumerator DownloadWithResume(string url, string savePath, System.Actionfloat onProgress null) { UnityWebRequest request new UnityWebRequest(url, GET); // 1. 创建我们自定义的DownloadHandler DownloadHandlerFileRange downloadHandler new DownloadHandlerFileRange(savePath, request); request.downloadHandler downloadHandler; // 2. 订阅事件可选 downloadHandler.OnDownloadStarted () { Debug.Log($开始下载文件总大小: {downloadHandler.TotalFileSize} 字节); }; // 3. 发起请求 request.SendWebRequest(); // 4. 在等待过程中更新进度和速度 while (!request.isDone) { float progress request.downloadProgress; // 这会调用我们重写的GetProgress() onProgress?.Invoke(progress); // 也可以从handler直接获取速度 // Debug.Log($当前速度: {downloadHandler.DownloadSpeedKB} KB/s); yield return null; // 每帧检查一次 } // 5. 处理最终结果 if (request.result UnityWebRequest.Result.Success) { Debug.Log(下载成功); } else if (request.result UnityWebRequest.Result.ProtocolError) { // 注意如果服务器不支持Range会返回200和整个文件这里也是Success。 // 只有真正的HTTP错误如404503才会走到这里。 Debug.LogError($HTTP错误: {request.responseCode}); // 检查是否是“不支持断点续传”导致的问题通常不是收到200即表示成功下载了完整文件。 } else { Debug.LogError($下载失败: {request.result} - {request.error}); // 可能是网络中断文件已下载的部分savePath仍然存在下次可以续传。 } // 6. 重要清理请求using语句会自动处理这里是显式示例 request.Dispose(); }6. 高级话题生产环境中的挑战与优化6.1 服务器兼容性与回退策略并非所有服务器都支持HTTP Range请求。我们的代码需要具备优雅降级的能力。检测支持度 可以在首次请求前发送一个HEAD请求来检查服务器是否返回Accept-Ranges: bytes头。但这增加了复杂度。运行时处理 更简单实用的策略是在我们的ReceiveContentLength方法中通过检查响应状态码来判断。如果状态码是206完美走断点续传流程。如果状态码是200说明服务器不支持Range它无视了我们的Range头返回了整个文件。此时Content-Length就是完整文件大小。我们需要 a. 关闭之前以追加模式打开的文件流。 b. 删除可能已存在的部分文件因为要重新下载。 c. 以创建模式FileMode.Create重新打开文件流从头开始写入。 这需要在ReceiveContentLength或ReceiveData最开始处添加判断逻辑。6.2 下载管理、队列与暂停/继续一个完整的资源更新系统往往需要同时管理多个下载任务并支持暂停、继续、取消和优先级队列。任务封装 将UnityWebRequest、DownloadHandlerFileRange、保存路径、回调函数等信息封装成一个DownloadTask类。管理器 创建一个DownloadManager单例内部维护一个ListDownloadTask或优先级队列。管理器负责按序或并发地执行这些任务通过启动多个协程。暂停与继续UnityWebRequest本身没有Pause方法。实现“暂停”实际上是调用request.Abort()中止请求但保留已下载的文件。实现“继续”就是重新创建一个新的UnityWebRequest并使用已有的文件路径和大小来设置Range头也就是重新开始一次新的“续传”请求。管理器需要记录每个任务的状态等待、下载中、暂停、错误、完成。取消 取消与暂停类似但Abort()之后可以选择删除已下载的部分文件。6.3 性能监控、日志与调试速度计算 我们的示例代码实现了简单的速度计算。更高级的实现可以计算平均速度、剩余时间并平滑速度显示如使用移动平均算法避免数字跳动过快。完整性校验 文件下载完成后尤其是用于热更新的资源必须进行校验。常见的做法是服务器提供文件的MD5或SHA1哈希值客户端下载完成后计算本地文件的哈希值进行比对。可以在DownloadHandlerFileRange的CompleteContent后发起一个校验协程。日志记录 记录每个任务的开始时间、结束时间、总大小、平均速度、是否续传等信息有助于分析用户网络情况和排查问题。6.4 常见问题排查清单问题现象可能原因排查步骤与解决方案下载进度始终为01.ReceiveData未被调用。2. 文件总大小(_totalFileSize)为0。1. 检查网络连接和URL是否正确。2. 在ReceiveContentLength中打印日志检查是否成功获取到Content-Length。3. 检查服务器是否支持Range请求看响应码是206还是200。下载的文件大小不对或损坏1.Range头设置错误。2. 文件流写入模式错误。3. 多线程并发写入同一文件。1. 打印Range头的值确认_localFileSize计算正确。2. 确保文件流以FileMode.Append打开。3. 确保同一个文件同一时间只有一个下载任务。下载到一半报错退出无法续传1. 文件流未正确关闭/释放文件被锁定。2. 异常处理不完善资源未清理。1. 确保在CompleteContent、析构函数以及Abort时都调用Cleanup。2. 在ReceiveData的catch块中也进行资源清理。3. 使用try-catch-finally确保文件流关闭。速度非常慢1. 缓冲区大小设置过小。2. 频繁调用FileStream.Flush()。3. 服务器限速或网络差。1. 确保缓冲区为1MB1024*1024。2. 移除或减少Flush调用让系统缓冲。3. 实现分块下载测试或更换网络环境对比。收到状态码200从头下载服务器不支持Range请求。实现前文所述的“回退策略”检测到200时删除旧文件以创建模式重新下载。并在UI上提示用户“不支持断点续传”。内存占用过高1. 错误地使用了DownloadHandlerBuffer。2. 自定义Handler的缓冲区设置过大。1. 确认使用的是DownloadHandlerScript或继承类而非Buffer。2. 缓冲区1MB足够无需更大。检查是否有其他内存泄漏。实现一个稳定可靠的断点续传下载器是Unity客户端开发中一项非常实用的高级技能。它不仅仅是将代码跑通更需要你深入理解HTTP协议、文件I/O、异步编程和资源管理。从最基础的GET请求开始逐步深入到自定义Handler再到处理各种边界情况和生产环境优化这个过程本身就是一个极好的学习路径。当你看到自己编写的下载器在弱网络环境下稳健地暂停、继续为用户节省时间和流量时那种成就感就是对技术深耕最好的回报。