Unity HttpWebRequest 下载失败,流已关闭(Stream has been closed)_unity filestream 视频文件
一、Unity下载资源报错提示: ec / ec08ce25978f12d7.fe417.abFailed beacuse exceprion Cannot access a disposed object.
Object name: ‘Stream has been closed’.
1、思考:
这个错误提示的关键信息是:
Cannot access a disposed object. Object name: ‘Stream has been closed’
翻译:无法访问已释放的对象。对象名:‘流已关闭’
错误原因分析
-
对象已被释放:尝试使用已经被关闭或释放的流(Stream)对象。
-
多线程访问问题:可能在多个线程中同时操作同一个流对象,导致一个线程关闭了流,而另一个线程还在尝试使用。
-
异常处理不当:在发生异常后,流被关闭(例如在
finally
块中关闭),但后续又尝试使用该流。 -
资源管理不当:使用了
using
语句自动释放资源后,又尝试在外部使用该资源。
解决方案
1. 确保流在使用前未被关闭
检查代码中是否在读取流之前就调用了Close()
或Dispose()
方法。
2. 避免重复关闭流
确保流只被关闭一次。多次关闭同一个流会导致后续访问抛出异常。
3. 检查异常处理逻辑
确保在异常发生时,不会导致流被关闭后还继续使用。例如:
FileStream fileStream = null;try{fileStream = new FileStream(path, FileMode.Open);// 使用流}catch (Exception ex){// 处理异常}finally{// 确保流不为null再关闭if (fileStream != null){fileStream.Close();fileStream.Dispose();}}
4. 避免在using
块外部使用流
using
语句会在代码块结束时自动释放资源,之后不能再使用该流。
错误示例:
Stream stream;using (stream = new FileStream(path, FileMode.Open)){// 读取流}// 这里stream已经被关闭,再使用会抛出异常stream.Read(...); // 错误!
正确做法:在using
块内完成所有操作。
5. 多线程同步
如果多个线程访问同一个流,需要使用锁机制:
private readonly object streamLock = new object();private Stream sharedStream;void ReadStream(){lock (streamLock){// 确保流没有被关闭if (sharedStream == null || !sharedStream.CanRead)return;// 读取流byte[] buffer = new byte[1024];int bytesRead = sharedStream.Read(buffer, 0, buffer.Length);}}
6. 检查流的状态
在访问流之前,检查流是否已被关闭:
if (myStream != null && myStream.CanRead){// 安全读取}
结合之前的下载代码
在之前的断点续传代码中,我们使用了多个流(remoteStream
和localStream
),需要确保:
-
不要在
using
块外使用流:所有操作都在using
内部完成。 -
避免嵌套释放:不要在外部代码中再次关闭已经在
using
中自动关闭的流。
修复后的下载代码示例
IEnumerator DownloadChunk(){isDownloading = true;Stream remoteStream = null;FileStream localStream = null;try{currentRequest = (HttpWebRequest)WebRequest.Create(url);currentRequest.AddRange(downloadedBytes);using (HttpWebResponse response = (HttpWebResponse)currentRequest.GetResponse()){remoteStream = response.GetResponseStream();localStream = new FileStream(savePath, FileMode.Append);byte[] buffer = new byte[4096];int bytesRead;while ((bytesRead = remoteStream.Read(buffer, 0, buffer.Length)) > 0){localStream.Write(buffer, 0, bytesRead);downloadedBytes += bytesRead;yield return null;}} // 这里response和remoteStream会被自动释放}catch (WebException ex){Debug.LogError($\"Download error: {ex.Message}\");}finally{// 确保本地文件流被正确关闭(如果using没有自动处理)if (localStream != null){localStream.Close();localStream.Dispose();}// 注意:remoteStream在using块结束后已经被释放,不需要再处理isDownloading = false;}}
关键点:
-
using
块:确保HttpWebResponse
和其中的流在结束时被正确释放。 -
单独处理本地文件流:因为
FileStream
不在using
块内,所以在finally
中手动释放。 -
避免重复释放:在
using
块中已经释放的remoteStream
不需要在finally
中再次释放。
常见陷阱
-
异步操作中的流关闭:如果在异步读取过程中提前关闭流,会导致后续读取失败。确保在完成所有操作前不要关闭流。
-
跨方法传递流:将流作为参数传递到其他方法时,很难控制其生命周期。建议在单一方法内完成流的创建和使用。
替代方案:使用UnityWebRequest
UnityWebRequest的DownloadHandlerFile会自动管理文件流,避免手动操作流:
IEnumerator DownloadWithUnityWebRequest(){var uwr = new UnityWebRequest(url);uwr.downloadHandler = new DownloadHandlerFile(savePath);uwr.disposeDownloadHandlerOnDispose = true;yield return uwr.SendWebRequest();if (uwr.result != UnityWebRequest.Result.Success)Debug.LogError(uwr.error);}
这种方法不需要手动处理流,减少了出错概率。
总结
错误Cannot access a disposed object
通常由资源生命周期管理不当引起。通过:
-
规范使用
using
语句 -
确保资源不被重复释放
-
避免跨作用域使用已释放对象
-
多线程环境下加锁
可以解决此问题。对于下载任务,推荐使用UnityWebRequest简化资源管理。
2、解决方案
Unity 下载错误解决方案:Cannot access a disposed object. Stream has been closed
这个错误提示表明您的代码尝试访问一个已经被关闭或释放的流对象。在下载过程中,流被提前关闭或多次释放会导致此问题。
错误原因分析
-
流被提前关闭:
- 在读取流之前或期间意外关闭了流
using
语句或Dispose()
调用后再次使用流
-
多线程访问问题:
- 多个协程同时访问同一个流对象
- 主线程和后台线程同时操作流
-
异常处理不当:
- 发生异常后未正确释放资源
- 在
finally
块中关闭流后再次尝试使用
-
资源管理冲突:
- Unity的生命周期管理与.NET的IDisposable冲突
- 对象被Unity销毁后仍尝试访问
完整解决方案
1. 安全的流管理实现
using UnityEngine;using System.IO;using System.Net;using System.Collections;public class SafeDownloader : MonoBehaviour{ public string url = \"http://your-resource-url/file.zip\"; private string savePath; private long downloadedBytes; private bool isDownloading; private FileStream fileStream; private Stream responseStream; private HttpWebRequest webRequest; void Start() { savePath = Path.Combine(Application.persistentDataPath, \"file.zip\"); StartCoroutine(StartDownload()); } IEnumerator StartDownload() { // 检查并加载之前的下载进度 if (File.Exists(savePath)) { downloadedBytes = new FileInfo(savePath).Length; Debug.Log($\"Resuming from {downloadedBytes} bytes\"); } while (downloadedBytes < GetRemoteFileSize()) { yield return StartCoroutine(DownloadChunk()); } } IEnumerator DownloadChunk() { isDownloading = true; bool success = false; int retryCount = 0; const int maxRetries = 3; while (!success && retryCount < maxRetries) { try { // 创建请求 webRequest = (HttpWebRequest)WebRequest.Create(url); webRequest.AddRange(downloadedBytes); // 获取响应 using (var response = (HttpWebResponse)webRequest.GetResponse()) { // 打开文件流(追加模式) fileStream = new FileStream( savePath, FileMode.Append, FileAccess.Write, FileShare.None, 4096, true // 异步IO ); // 获取响应流 responseStream = response.GetResponseStream(); byte[] buffer = new byte[4096]; int bytesRead; // 读取数据 while ((bytesRead = responseStream.Read(buffer, 0, buffer.Length)) > 0) { // 安全写入文件 fileStream.Write(buffer, 0, bytesRead); downloadedBytes += bytesRead; // 每帧暂停以允许Unity处理其他任务 yield return null; } success = true; } } catch (WebException ex) { Debug.LogWarning($\"Download failed (attempt {retryCount + 1}/{maxRetries}): {ex.Message}\"); retryCount++; // 指数退避策略 yield return new WaitForSeconds(Mathf.Pow(2, retryCount)); } catch (IOException ioEx) { // 专门处理流访问异常 Debug.LogError($\"IO Exception: {ioEx.Message}\"); // 重置流状态 ResetStreams(); retryCount++; yield return new WaitForSeconds(1); } finally { // 安全关闭流(不重复关闭) CloseStreamsSafely(); } } isDownloading = false; if (!success) { Debug.LogError(\"Download failed after maximum retries\"); } } // 安全关闭所有流 private void CloseStreamsSafely() { if (responseStream != null) { try { responseStream.Close(); } catch { } responseStream = null; } if (fileStream != null) { try { fileStream.Flush(); fileStream.Close(); } catch { } fileStream = null; } } // 重置流状态 private void ResetStreams() { CloseStreamsSafely(); // 重新初始化文件流(保留已下载内容) try { fileStream = new FileStream( savePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096, true ); fileStream.Seek(downloadedBytes, SeekOrigin.Begin); } catch (Exception ex) { Debug.LogError($\"Failed to reset file stream: {ex.Message}\"); } } // 获取远程文件大小 private long GetRemoteFileSize() { try { var request = (HttpWebRequest)WebRequest.Create(url); request.Method = \"HEAD\"; using (var response = (HttpWebResponse)request.GetResponse()) { return response.ContentLength; } } catch { return 0; } } // MonoBehaviour生命周期结束时清理资源 private void OnDestroy() { CloseStreamsSafely(); if (webRequest != null) { webRequest.Abort(); webRequest = null; } }}
2. 关键改进点
安全的流处理
// 使用单独的流引用private FileStream fileStream;private Stream responseStream;// 专门的安全关闭方法private void CloseStreamsSafely(){ // 避免重复关闭 if (responseStream != null) { try { responseStream.Close(); } catch { } responseStream = null; // 清空引用 } if (fileStream != null) { try { fileStream.Flush(); // 确保数据写入 fileStream.Close(); } catch { } fileStream = null; }}
流状态重置机制
private void ResetStreams(){ CloseStreamsSafely(); // 重新打开文件流并定位到上次位置 try { fileStream = new FileStream( savePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096, true // 异步IO ); fileStream.Seek(downloadedBytes, SeekOrigin.Begin); } catch (Exception ex) { Debug.LogError($\"重置文件流失败: {ex.Message}\"); }}
改进的异常处理
catch (IOException ioEx){ // 专门处理流访问异常 Debug.LogError($\"IO异常: {ioEx.Message}\"); // 重置流状态而不是继续使用无效流 ResetStreams(); retryCount++; yield return new WaitForSeconds(1);}
3. 替代方案:使用UnityWebRequest(推荐)
UnityWebRequest自动处理流管理,避免手动操作流:
using UnityEngine;using UnityEngine.Networking;using System.Collections;using System.IO;public class UnityWebDownloader : MonoBehaviour{ public string url = \"http://your-resource-url/file.zip\"; private string savePath; private UnityWebRequest webRequest; private DownloadHandlerFile downloadHandler; private bool isDownloading; void Start() { savePath = Path.Combine(Application.persistentDataPath, \"file.zip\"); StartCoroutine(StartDownload()); } IEnumerator StartDownload() { isDownloading = true; // 设置临时文件路径 string tempPath = savePath + \".tmp\"; // 创建下载处理器(支持断点续传) downloadHandler = new DownloadHandlerFile(tempPath); downloadHandler.removeFileOnAbort = false; // 创建请求 webRequest = new UnityWebRequest(url, UnityWebRequest.kHttpVerbGET); webRequest.downloadHandler = downloadHandler; // 检查已有下载进度 if (File.Exists(tempPath)) { long existingBytes = new FileInfo(tempPath).Length; webRequest.SetRequestHeader(\"Range\", $\"bytes={existingBytes}-\"); } // 发送请求 var operation = webRequest.SendWebRequest(); // 等待下载完成 while (!operation.isDone) { // 显示下载进度 float progress = webRequest.downloadProgress; Debug.Log($\"下载进度: {progress:P0}\"); yield return null; // 检查网络错误 if (webRequest.result == UnityWebRequest.Result.ConnectionError) { Debug.LogWarning(\"网络连接错误,暂停下载\"); break; } } // 处理结果 if (webRequest.result == UnityWebRequest.Result.Success) { // 重命名临时文件 if (File.Exists(savePath)) File.Delete(savePath); File.Move(tempPath, savePath); Debug.Log(\"下载完成!\"); } else { Debug.LogError($\"下载失败: {webRequest.error}\"); } isDownloading = false; webRequest.Dispose(); } // 网络恢复时重新开始下载 public void ResumeDownload() { if (!isDownloading && webRequest == null) { StartCoroutine(StartDownload()); } } private void OnDestroy() { if (webRequest != null) { webRequest.Abort(); webRequest.Dispose(); } }}
4. 最佳实践建议
-
优先使用UnityWebRequest:
- 自动处理底层网络和流管理
- 内置断点续传支持
- 更好的跨平台兼容性
-
流生命周期管理原则:
- 单一职责:每个方法只负责打开或关闭流
- 明确所有权:哪个对象打开流就负责关闭它
- 使用
using
语句确保及时释放资源
-
防御性编程:
// 在使用流前检查状态if (fileStream != null && fileStream.CanWrite){ // 安全操作}else{ Debug.LogError(\"文件流不可用\"); ResetStreams();}
-
添加日志记录:
private void LogStreamState(){ Debug.Log($\"FileStream: {(fileStream != null ? \"Exists\" : \"Null\")}, \" + $\"CanWrite: {(fileStream?.CanWrite ?? false)}\"); Debug.Log($\"ResponseStream: {(responseStream != null ? \"Exists\" : \"Null\")}, \" + $\"CanRead: {(responseStream?.CanRead ?? false)}\");}
-
使用Unity的生命周期管理:
void OnApplicationPause(bool paused){ if (paused) { // 暂停时安全关闭流 CloseStreamsSafely(); } else { // 恢复时重新开始下载 if (!isDownloading) StartCoroutine(StartDownload()); }}
这些解决方案能有效处理\"Stream has been closed\"错误,同时提供健壮的下载功能,特别适合移动设备上不稳定的网络环境。