Unity大文件分段下载实战:基于Range头实现断点续传与性能优化
2026/8/5 18:38:58 网站建设 项目流程

1. 项目概述与核心需求解析

最近在项目中遇到了一个典型的资源分发难题:需要从服务器下载一个2.8GB的高清视频资源包到移动设备上。直接使用UnityWebRequest的DownloadHandlerFile看似简单,但在实际网络环境(尤其是移动网络)下,大文件下载简直就是一场灾难。连接超时、下载中断、进度丢失、内存飙升……这些问题接踵而至。经过几轮踩坑和优化,最终我们通过分段下载(也叫分块下载或断点续传)的方案,不仅稳定地啃下了这块“硬骨头”,还大幅提升了下载体验。今天就来详细拆解这个实战过程,把完整的思路、代码和避坑经验分享给你。

简单来说,分段下载的核心思想就是“化整为零”。与其让一个网络连接苦苦支撑2.8GB的数据传输直到天荒地老,不如将这个文件在逻辑上分成多个小块(例如每个1MB或2MB),然后为每个小块发起独立的下载请求。这样做的好处是多方面的:首先,每个请求的生命周期短,降低了单次连接超时或失败的风险;其次,可以非常方便地实现断点续传,记录下每个分块的下载状态,即使应用重启也能从上次中断的地方继续;再者,通过并行下载多个分块,理论上可以充分利用带宽,提升下载速度;最后,也是非常重要的一点,它能有效避免将整个文件数据一次性加载到内存中,对于移动设备的内存管理非常友好。

这个方案特别适合需要处理大型资源更新、视频缓存、AssetBundle下载等场景的Unity开发者。无论你是独立开发者还是团队中的TA,掌握这套方法都能让你在面对“大文件”时更加从容。

2. 技术方案选型与设计思路

面对大文件下载,Unity社区里常见的方案有好几种,我们需要根据实际需求做出选择。

方案一:原生UnityWebRequest这是最基础的方式。UnityWebRequest配合DownloadHandlerFile可以直接将数据流写入文件,避免了将整个文件数据保存在内存中。但对于超大文件,它的主要问题在于其“原子性”:下载要么成功,要么完全失败。一旦网络波动导致连接中断,整个下载进度就丢失了,只能从头再来。这对于2.8GB的文件来说是不可接受的。

方案二:第三方插件(如Best HTTP/2, UniTask等)许多优秀的第三方插件提供了更强大的HTTP客户端功能,包括内置的断点续传、多线程下载等。它们的优点是开箱即用,功能完善。但缺点也很明显:引入额外的插件依赖,可能增加包体大小,需要学习其特定的API,并且在某些对第三方库有严格限制的项目中可能无法使用。

方案三:基于UnityWebRequest封装分段下载(本文方案)这是我们最终选择的路径。它的核心优势在于:

  1. 零依赖:完全基于Unity官方API,无需引入任何第三方库,兼容性和可控性最高。
  2. 高度定制化:我们可以完全控制分块策略、错误重试机制、进度计算和状态持久化逻辑,使其完美契合项目需求。
  3. 学习价值高:通过自己实现,你能深入理解HTTP协议中Range头、文件流操作、异步编程等核心概念,这是使用插件无法获得的。

我们的设计思路如下:

  1. 分块策略:将2.8GB的总文件大小,按固定大小(如2MB)进行分块。计算总块数,并为每一块分配一个唯一的索引。
  2. Range请求:利用HTTP/1.1标准中的Range请求头。例如,Range: bytes=0-2097151表示请求文件开头的2MB数据。这是实现分段下载的协议基础。
  3. 任务调度:设计一个下载管理器,负责创建和管理所有分块下载任务。可以顺序下载以保证稳定性,也可以实现有限的并行下载以提升速度(需注意服务器并发连接限制和本地IO压力)。
  4. 状态持久化:将每个分块的下载状态(未开始、下载中、已完成)以及已下载的文件临时数据,持久化到本地(如PlayerPrefs或一个专门的配置文件)。这是实现断点续传的关键。
  5. 文件合并:所有分块下载完成后,按照索引顺序,将它们从临时文件读取并写入到最终的目标文件中。

注意:使用Range请求的前提是服务器必须支持。绝大多数标准的HTTP文件服务器(如Apache, Nginx)以及云存储服务(如AWS S3, 阿里云OSS)都支持此功能。在实现前,最好先确认你的资源服务器支持Range请求。

3. 核心模块实现与代码拆解

接下来,我们深入到代码层面,看看各个核心模块是如何实现的。我会附上关键代码并解释其作用。

3.1 分块信息与下载状态定义

首先,我们需要一个数据结构来记录每个分块的信息和状态。

[System.Serializable] public class ChunkInfo { public int index; // 分块索引,从0开始 public long startByte; // 分块起始字节位置 public long endByte; // 分块结束字节位置 public bool isCompleted; // 该分块是否已完成下载 public string tempFilePath; // 该分块对应的临时文件路径 } [System.Serializable] public class DownloadConfig { public string fileUrl; // 文件完整URL public string savePath; // 最终保存的完整路径 public long totalFileSize; // 文件总大小(字节),可从服务器HEAD请求获取 public int chunkSizeInMB = 2; // 每个分块大小,默认为2MB public List<ChunkInfo> allChunks; // 全部分块信息列表 public string configSavePath; // 本配置的持久化路径 }

ChunkInfo记录了每个分块的元数据。DownloadConfig则代表了整个下载任务的配置,它会被序列化到本地,用于断点续传时恢复任务上下文。

3.2 下载管理器:任务调度与状态控制

下载管理器 (DownloadManager) 是整个功能的大脑,负责统筹协调。

using UnityEngine; using UnityEngine.Networking; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; public class DownloadManager : MonoBehaviour { private DownloadConfig _currentConfig; private bool _isDownloading = false; private int _maxConcurrentDownloads = 2; // 最大并发下载数,可根据情况调整 // 启动或恢复下载 public async Task StartOrResumeDownload(string url, string savePath) { if (_isDownloading) { Debug.LogWarning("下载任务正在进行中,请勿重复启动。"); return; } _isDownloading = true; // 1. 尝试加载已有的下载配置(用于断点续传) string configPath = Path.Combine(Application.persistentDataPath, Path.GetFileName(savePath) + ".config.json"); if (File.Exists(configPath)) { Debug.Log("检测到未完成的下载任务,尝试恢复..."); string json = File.ReadAllText(configPath); _currentConfig = JsonUtility.FromJson<DownloadConfig>(json); } else { // 2. 全新下载:获取文件大小并初始化分块 _currentConfig = await CreateNewDownloadConfig(url, savePath, configPath); } // 3. 执行分块下载 await DownloadAllChunks(); // 4. 所有分块完成后,合并文件 if (await MergeAllChunks()) { Debug.Log($"文件下载并合并完成:{savePath}"); // 清理临时文件和配置 CleanupTempFiles(); File.Delete(configPath); } else { Debug.LogError("文件合并失败!"); } _isDownloading = false; } private async Task<DownloadConfig> CreateNewDownloadConfig(string url, string savePath, string configPath) { long fileSize = await GetRemoteFileSize(url); if (fileSize <= 0) throw new System.Exception("无法获取远程文件大小或文件不存在。"); int chunkSizeInBytes = _currentConfig?.chunkSizeInMB ?? 2 * 1024 * 1024; int totalChunks = (int)Mathf.Ceil((float)fileSize / chunkSizeInBytes); List<ChunkInfo> chunks = new List<ChunkInfo>(); for (int i = 0; i < totalChunks; i++) { long start = i * chunkSizeInBytes; long end = Math.Min(start + chunkSizeInBytes - 1, fileSize - 1); string tempPath = Path.Combine(Application.temporaryCachePath, $"{Path.GetFileName(savePath)}_chunk_{i}.tmp"); chunks.Add(new ChunkInfo { index = i, startByte = start, endByte = end, isCompleted = false, tempFilePath = tempPath }); } var config = new DownloadConfig { fileUrl = url, savePath = savePath, totalFileSize = fileSize, chunkSizeInMB = 2, allChunks = chunks, configSavePath = configPath }; // 保存初始配置 SaveConfig(config); return config; } // 获取远程文件大小(使用HEAD方法,避免下载整个文件) private async Task<long> GetRemoteFileSize(string url) { using (UnityWebRequest headRequest = UnityWebRequest.Head(url)) { await headRequest.SendWebRequest(); if (headRequest.result == UnityWebRequest.Result.Success) { string contentLength = headRequest.GetResponseHeader("Content-Length"); if (long.TryParse(contentLength, out long size)) { return size; } } Debug.LogError($"获取文件大小失败:{headRequest.error}"); return -1; } } }

管理器的主要流程清晰可见:检查恢复点 -> 创建/加载配置 -> 分块下载 -> 合并清理。其中,GetRemoteFileSize方法使用UnityWebRequest.Head非常关键,它只获取文件的头部信息(包括大小),而不会下载文件体,是一种轻量级的查询方式。

3.3 单分块下载与Range请求实现

这是最核心的下载单元,负责下载一个指定的字节范围。

private async Task<bool> DownloadSingleChunk(ChunkInfo chunk) { string rangeHeader = $"bytes={chunk.startByte}-{chunk.endByte}"; Debug.Log($"下载分块 {chunk.index}: {rangeHeader}"); using (UnityWebRequest request = new UnityWebRequest(_currentConfig.fileUrl, "GET")) { // 1. 设置Range请求头 request.SetRequestHeader("Range", rangeHeader); // 2. 配置DownloadHandlerFile,直接写入临时文件 // 注意:这里使用FileMode.Create,因为每个分块都是独立的文件 var downloadHandler = new DownloadHandlerFile(chunk.tempFilePath, false); request.downloadHandler = downloadHandler; // 3. 可选的超时设置(单位:秒) request.timeout = 30; var operation = request.SendWebRequest(); // 4. 异步等待完成,并支持取消 while (!operation.isDone) { // 这里可以更新该分块的独立进度 // float chunkProgress = request.downloadProgress; await Task.Yield(); // 让出控制权,避免阻塞主线程 // 可以在这里添加取消检测 // if (_isCancelled) { request.Abort(); return false; } } // 5. 处理结果 if (request.result == UnityWebRequest.Result.Success) { chunk.isCompleted = true; SaveConfig(_currentConfig); // 更新持久化配置,标记该分块完成 Debug.Log($"分块 {chunk.index} 下载成功。"); return true; } else { Debug.LogError($"分块 {chunk.index} 下载失败: {request.error}"); // 这里可以加入重试逻辑 return false; } } }

这段代码的精髓在于request.SetRequestHeader("Range", rangeHeader)。通过设置这个HTTP头,我们告诉服务器:“我只需要文件从startByteendByte的这一部分数据”。服务器会返回状态码206 Partial Content以及对应的数据块。DownloadHandlerFile会将这些数据直接流式写入到指定的临时文件路径,内存占用极小。

3.4 分块调度与并发控制

如何有序或并行地下载所有分块?这里提供一个带简单并发控制的调度示例。

private async Task DownloadAllChunks() { List<ChunkInfo> pendingChunks = new List<ChunkInfo>(); foreach (var chunk in _currentConfig.allChunks) { if (!chunk.isCompleted) { pendingChunks.Add(chunk); } } Debug.Log($"待下载分块数:{pendingChunks.Count}"); // 使用一个列表来跟踪正在运行的任务 List<Task<bool>> runningTasks = new List<Task<bool>>(); for (int i = 0; i < pendingChunks.Count; i++) { // 如果正在运行的任务数达到最大并发数,等待其中一个完成 while (runningTasks.Count >= _maxConcurrentDownloads) { Task<bool> finishedTask = await Task.WhenAny(runningTasks); runningTasks.Remove(finishedTask); // 可以在这里检查任务结果,如果失败可以考虑加入重试队列 } // 启动一个新的下载任务 ChunkInfo chunkToDownload = pendingChunks[i]; Task<bool> downloadTask = DownloadSingleChunkWithRetry(chunkToDownload, maxRetries: 3); runningTasks.Add(downloadTask); // 更新总进度(示例) // float overallProgress = CalculateOverallProgress(); // OnProgressUpdated?.Invoke(overallProgress); } // 等待所有剩余任务完成 await Task.WhenAll(runningTasks); } private async Task<bool> DownloadSingleChunkWithRetry(ChunkInfo chunk, int maxRetries) { int retryCount = 0; while (retryCount < maxRetries) { bool success = await DownloadSingleChunk(chunk); if (success) { return true; } retryCount++; Debug.LogWarning($"分块 {chunk.index} 第{retryCount}次重试..."); await Task.Delay(1000 * retryCount); // 重试延迟,避免频繁请求 } Debug.LogError($"分块 {chunk.index} 下载失败,已达最大重试次数。"); return false; }

这个调度器实现了简单的并发控制。_maxConcurrentDownloads控制了同时进行的分块下载任务数量。设置并发数需要权衡:数值太小,无法充分利用带宽;数值太大,可能会对服务器造成压力,也可能受限于客户端的网络连接数或IO性能,移动设备上建议设为2-4。同时,我们为每个分块下载包裹了一个重试逻辑,增强了鲁棒性。

3.5 文件合并与清理

当所有分块都下载完毕后,需要将它们按顺序拼接成完整的文件。

private async Task<bool> MergeAllChunks() { Debug.Log("开始合并分块文件..."); string finalPath = _currentConfig.savePath; // 确保目标目录存在 string directory = Path.GetDirectoryName(finalPath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } try { // 使用FileStream以追加模式写入最终文件 using (FileStream finalFileStream = new FileStream(finalPath, FileMode.Create, FileAccess.Write)) { // 按照分块索引顺序处理 foreach (var chunk in _currentConfig.allChunks.OrderBy(c => c.index)) { if (!File.Exists(chunk.tempFilePath)) { Debug.LogError($"找不到临时分块文件:{chunk.tempFilePath}"); return false; } // 读取临时分块文件 byte[] chunkData = File.ReadAllBytes(chunk.tempFilePath); // 写入最终文件 await finalFileStream.WriteAsync(chunkData, 0, chunkData.Length); Debug.Log($"已合并分块 {chunk.index}"); // 可选:合并后立即删除临时文件,以节省空间 // File.Delete(chunk.tempFilePath); } } Debug.Log("文件合并成功!"); return true; } catch (System.Exception e) { Debug.LogError($"文件合并过程中发生异常:{e.Message}"); return false; } } private void CleanupTempFiles() { foreach (var chunk in _currentConfig.allChunks) { if (File.Exists(chunk.tempFilePath)) { try { File.Delete(chunk.tempFilePath); } catch (System.Exception e) { Debug.LogWarning($"删除临时文件 {chunk.tempFilePath} 失败:{e.Message}"); } } } }

合并操作的关键是按正确的索引顺序 (OrderBy(c => c.index)) 读取和写入。这里使用了File.ReadAllBytes,对于2MB的分块是合适的。如果分块设置得非常大(比如100MB),则需要考虑使用流式读取 (FileStream) 来避免一次性加载大块数据到内存。合并完成后,清理临时文件是一个好习惯。

4. 关键参数调优与实战心得

实现功能只是第一步,要让它在生产环境中稳定高效地运行,还需要进行细致的调优。以下是我在实战中总结的几个关键点。

4.1 分块大小的选择:平衡的艺术

分块大小 (chunkSizeInMB) 是影响性能的核心参数之一,它需要在多个因素间取得平衡。

  • 过小(如256KB):优点是对网络波动容忍度高,单次失败重试成本低。缺点是会产生大量的小文件IO操作(创建、写入、合并),增加系统开销;同时HTTP请求头等额外开销占比变高,影响整体效率。
  • 过大(如10MB):优点是减少了请求次数和文件IO次数。缺点是单次请求失败的成本变高,重试需要重新下载的数据量更大;并且在内存管理上,虽然DownloadHandlerFile是流式写入,但UnityWebRequest内部可能仍有缓冲区,过大的分块可能带来短暂的内存压力。

经过多次测试,对于移动端2.8GB左右的大文件,我将分块大小设置为2MB(即2 * 1024 * 1024字节)。这是一个比较折中的选择:它确保了单个请求在一般网络环境下能在合理时间内完成(降低了超时风险),同时分块数量控制在可管理范围内(对于2.8GB文件,约1400个分块),合并时的IO压力也适中。你可以根据你的平均网络速度和目标设备性能进行微调。

4.2 并发数设置:并非越多越快

很多人认为并发数 (_maxConcurrentDownloads) 开得越大,下载速度就越快。这在理论上没错,但实际会遇到瓶颈。

  1. 服务器限制:许多HTTP服务器对同一客户端的并发连接数有限制,过多的并发请求可能导致部分请求被拒绝或延迟。
  2. 本地IO瓶颈:多个分块同时写入磁盘(即使是不同的临时文件),在移动设备上可能会造成IO争用,反而降低整体吞吐量。
  3. 网络公平性:在共享网络环境下,过多的TCP连接可能引发网络拥塞控制,导致性能下降。

我的建议是:从2-3个并发开始测试。在Wi-Fi环境下,可以尝试提升到4个。你可以设计一个简单的测试场景,用不同的并发数下载同一个大文件,记录总耗时。你会发现,超过某个阈值后,提速效果就不明显了,甚至可能因为IO竞争而变慢。在移动网络(4G/5G)下,建议保守一点,使用2个并发。

4.3 进度计算的准确性

给用户展示一个准确、平滑的下载进度条至关重要。分段下载的进度计算需要格外注意。 不能简单地用已完成的分块数除以总分块数,因为每个分块的大小可能不同(最后一个分块通常较小)。正确的整体进度计算公式应该是:总进度 = (所有已完成分块的字节数之和) / 文件总字节数

我们需要在ChunkInfo中记录startByteendByte,那么每个分块的大小就是endByte - startByte + 1。在DownloadSingleChunk中,每当一个分块完成,就累加其大小到“已下载字节数”这个变量中,然后用它除以_currentConfig.totalFileSize得到精确进度。

此外,为了避免进度条“卡顿”(因为分块完成是离散事件),可以在每个分块下载过程中,也利用request.downloadProgress来估算该分块内部的进度,并将其贡献到总进度中,从而实现更平滑的更新。

4.4 错误处理与重试策略的强化

网络请求失败是常态。一个健壮的系统必须有完善的错误处理和重试机制。

  1. 错误分类处理
    • 网络错误(超时、连接断开):直接加入重试队列。
    • HTTP错误(如404、403、416 Range Not Satisfiable):需要特殊处理。例如416错误可能意味着请求的字节范围无效,这可能是因为服务器上的文件发生了变化。这时可能需要重新获取文件大小并重建分块信息。
    • 本地IO错误(磁盘已满、权限不足):需要通知用户,并可能中止整个任务。
  2. 指数退避重试:我的DownloadSingleChunkWithRetry方法中使用了简单的固定延迟。更优的策略是“指数退避”,即每次重试的等待时间逐渐加倍(例如1秒、2秒、4秒…),这样既能给网络恢复留出时间,又不会过于激进。
  3. 孤立失败分块:如果某个分块在多次重试后依然失败,可以考虑将其记录到“失败列表”,并允许用户跳过它继续下载其他部分(对于可容忍部分损坏的文件),或者在最后统一报告。

4.5 内存与存储空间管理

这是移动端开发永远不能忽视的问题。

  • 内存:使用DownloadHandlerFile是避免内存暴涨的关键。确保没有在回调中无意间将下载的字节数组 (byte[]) 保存到长期存在的变量中。在合并文件时,对于大分块,使用FileStream.Read分批读取,而不是File.ReadAllBytes
  • 存储空间:在开始下载前,务必检查可用磁盘空间是否大于文件总大小的1.5倍左右。因为你需要空间存放临时分块文件和最终文件。在下载过程中,如果检测到空间不足,应优雅地暂停任务并提示用户。合并完成后,立即清理临时文件。

5. 完整代码集成与使用示例

将上述模块整合到一个可用的MonoBehaviour中,并提供简单的调用接口。

// 文件:ChunkedFileDownloader.cs using UnityEngine; using UnityEngine.Events; using System.IO; using System.Threading.Tasks; using System.Linq; public class ChunkedFileDownloader : MonoBehaviour { [Header("下载设置")] public string remoteFileURL = "http://your-server.com/path/to/bigfile.zip"; public string localSavePath = "downloaded_bigfile.zip"; // 可以是相对路径或绝对路径 [Header("高级设置")] [Tooltip("每个分块的大小(MB)")] public int chunkSizeMB = 2; [Tooltip("同时下载的最大分块数")] public int maxConcurrent = 2; [Tooltip("每个分块失败后的最大重试次数")] public int maxRetriesPerChunk = 3; [Header("事件")] public UnityEvent<float> onDownloadProgress; // 进度更新,参数为0-1的进度 public UnityEvent<string> onDownloadStatus; // 状态更新 public UnityEvent<bool, string> onDownloadCompleted; // 完成事件,参数为(是否成功, 消息) private DownloadManager _downloadManager; void Start() { _downloadManager = new DownloadManager(); // 可以在这里初始化一些路径 if (!Path.IsPathRooted(localSavePath)) { localSavePath = Path.Combine(Application.persistentDataPath, localSavePath); } } // 供UI按钮调用的方法 public async void StartDownload() { onDownloadStatus?.Invoke("正在准备下载..."); try { await _downloadManager.StartOrResumeDownload(remoteFileURL, localSavePath); onDownloadCompleted?.Invoke(true, "下载成功!"); } catch (System.Exception e) { Debug.LogError($"下载失败: {e}"); onDownloadCompleted?.Invoke(false, $"下载失败: {e.Message}"); } } // 暂停下载(需要DownloadManager暴露暂停接口) public void PauseDownload() { // _downloadManager.Pause(); onDownloadStatus?.Invoke("下载已暂停"); } // 获取当前下载任务的总进度(需要DownloadManager暴露进度属性) public float GetCurrentProgress() { // return _downloadManager.OverallProgress; return 0f; } }

在场景中创建一个GameObject,挂载此脚本,并在Inspector面板中配置好远程URL和本地保存路径。你可以将onDownloadProgress事件关联到UI进度条,将onDownloadStatus关联到状态文本,即可实现一个带进度显示的大文件下载器。

6. 常见问题排查与性能优化实录

在实际开发和测试中,我遇到了不少“坑”。这里记录下最典型的几个问题及其解决方法,希望能帮你节省时间。

问题一:服务器返回“416 Range Not Satisfiable”错误。

  • 现象:某个分块请求失败,错误信息中包含416。
  • 原因:请求的字节范围(Range头)超出了服务器上文件的实际大小。这通常发生在两种情况下:1) 我们在本地计算的分块信息有误(比如文件总大小获取错了);2) 服务器上的文件在下载过程中被更新或替换了,大小发生了变化。
  • 解决
    1. 在重试逻辑中捕获416错误。
    2. 当遇到416时,中止当前下载任务。
    3. 重新发起一个HEAD请求,获取最新的文件大小和ETag(如果有)。
    4. 比较新的文件大小和本地记录的总大小。如果不同,说明文件已变更,需要提示用户“文件已更新,请重新下载”,并清理本地状态,从头开始。如果大小相同,则可能是分块计算错误,需要重新计算并验证分块区间。

问题二:下载到一半,应用退出再进入,进度丢失。

  • 现象:实现了配置持久化,但重新启动后,管理器加载配置时出错,或者进度显示为0%。
  • 原因:配置文件的序列化/反序列化出错,或者临时文件被系统清理了。
  • 解决
    1. 健壮的序列化:使用JsonUtility.ToJsonFromJson时,确保DownloadConfigChunkInfo类是[System.Serializable]的,并且所有需要持久化的字段都是public的或标记了[SerializeField]
    2. 完整性校验:加载配置后,增加校验步骤。检查allChunks列表是否为空,每个ChunkInfotempFilePath对应的临时文件是否存在(对于标记为未完成的分块,文件可能不存在是正常的;对于标记为已完成的分块,文件必须存在)。如果校验失败,则视为无效配置,启动新的下载任务。
    3. 使用更可靠的存储:对于关键配置,PlayerPrefs可能不够可靠(尤其是数据量大时)。可以考虑使用System.IO.File直接读写到Application.persistentDataPath下的一个专用文件,并定期备份。

问题三:下载速度慢,甚至不如直接下载。

  • 现象:使用了分段下载,但整体耗时比单个UnityWebRequest下载还要长。
  • 原因排查与优化
    1. 并发数过高:如前所述,过高的并发可能导致IO或网络竞争。将并发数降至2或3进行测试
    2. 分块大小不匹配:分块大小可能不适合当前网络环境。在Wi-Fi下可以尝试增大到4MB或5MB,在移动网络下保持1-2MB。
    3. 服务器限速或限制:有些服务器会对频繁的Range请求进行限速或限制。尝试在请求头中添加一些礼貌性的标识,如User-Agent,并适当降低请求频率(在分块下载之间增加微小延迟)。
    4. 进度更新过于频繁:如果在主线程中频繁更新UI进度条(例如每收到一点数据就更新),可能会造成性能开销。可以改为每完成一个分块,或者每下载完一定数据量(如1%)再更新一次UI。

问题四:在低端安卓设备上,合并文件时卡顿甚至崩溃。

  • 现象:下载很快,但在合并阶段应用无响应或闪退。
  • 原因:合并时一次性将整个分块文件读入内存(File.ReadAllBytes),如果分块较大或同时处理多个分块,可能导致内存峰值过高。
  • 解决使用流式合并。修改MergeAllChunks方法:
    using (FileStream finalFileStream = new FileStream(finalPath, FileMode.Create, FileAccess.Write)) { foreach (var chunk in _currentConfig.allChunks.OrderBy(c => c.index)) { using (FileStream chunkStream = new FileStream(chunk.tempFilePath, FileMode.Open, FileAccess.Read)) { byte[] buffer = new byte[81920]; // 80KB缓冲区 int bytesRead; while ((bytesRead = await chunkStream.ReadAsync(buffer, 0, buffer.Length)) > 0) { await finalFileStream.WriteAsync(buffer, 0, bytesRead); } } File.Delete(chunk.tempFilePath); // 合并完立即删除 await Task.Yield(); // 每合并一个文件,让出一帧控制权,避免主线程阻塞 } }
    这种方式内存占用恒定(仅为缓冲区大小),非常适合处理大文件。

问题五:后台下载与唤醒。

  • 需求:应用切换到后台或锁屏后,希望下载能继续,并在完成后给用户通知。
  • 挑战:Unity应用在移动平台切换到后台时,默认会被暂停,所有线程和协程都会挂起。
  • 方案
    • Unity原生不支持长期后台任务。对于必须后台下载的需求,需要平台原生代码支持。
    • 折中方案:利用Application.runInBackground = true可以让应用在失去焦点时继续运行一段时间(但可能被系统挂起)。更可行的方案是,在应用每次被唤醒(OnApplicationPause(false))时,检查并尝试恢复下载。这至少保证了用户主动打开应用时,下载能继续。

最后,分享一个调试小技巧:在开发阶段,可以先将chunkSizeInMB调得很大(比如50MB),这样文件会被分成很少的几块。然后重点测试你的状态持久化/恢复逻辑、合并逻辑是否正确。等功能稳定后,再调整到适合生产环境的小分块大小进行性能和稳定性测试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询