.NET MAUI安卓端取消令牌未抛出OperationCanceledException问题
.NET MAUI安卓端取消下载触发NetworkOnMainThreadException问题
我在.NET MAUI项目中实现了带取消功能的文件下载,下载在子线程执行并更新UI。iOS端运行正常,但安卓端取消下载时,未触发OperationCanceledException,反而抛出Android.OS.NetworkOnMainThreadException。
相关代码
MVVM下载命令
[RelayCommand] void DownloadFile(ReciterChapterDownload reciterChapterDownload) { if (ReciterChapterDownloads.Any(x => x.DownloadReciterChapterId == reciterChapterDownload.DownloadReciterChapterId) && !reciterChapterDownload.Downloaded) { MainThread.BeginInvokeOnMainThread(async () => { reciterChapterDownload.Downloading = true; reciterChapterDownload.CancellationTokenSource = new CancellationTokenSource(); var progressIndicator = new Progress<double>(reciterChapterDownload.ReportProgress); var response = await _fileDownloadService .DownloadFileAsync(reciterChapterDownload.DownloadLink, reciterChapterDownload.GetDestinationDownloadPath(), progressIndicator, reciterChapterDownload.CancellationTokenSource); switch (response?.FileDownloadStatus) { case FileDownloadStatus.Completed: reciterChapterDownload.Downloading = false; reciterChapterDownload.Downloaded = true; break; case FileDownloadStatus.Cancelled: reciterChapterDownload.Downloading = false; break; case FileDownloadStatus.Failed: reciterChapterDownload.Downloading = false; await App.CurrentShell.DisplayAlert("Operation Failed", response.StatusMessage, "OK"); break; } }); } }
MVVM取消下载命令
[RelayCommand] void CancelFileDownload(ReciterChapterDownload reciterChapterDownload) { try { if (!reciterChapterDownload.CancellationTokenSource.IsCancellationRequested) { reciterChapterDownload.CancellationTokenSource.Cancel(); } } catch (Exception ex) { Console.WriteLine($"CancelFileDownloadCommand {ex.Message}"); } }
下载异步方法取消逻辑
using (cancellationTokenSource) { try { var token = cancellationTokenSource.Token; using (var fileStream = FileDownloadService.OpenStream(tempFilePath)) { using var stream = await response.Content.ReadAsStreamAsync(token); var totalRead = 0L; var buffer = new byte[bufferSize]; var isMoreDataToRead = true; do { // cancellationTokenSource.Token.ThrowIfCancellationRequested(); // this line also didn't help if (cancellationTokenSource.IsCancellationRequested) { await fileStream.FlushAsync(); fileStream.Close(); await fileStream.DisposeAsync(); return new() { StatusMessage = $"Download Operation Cancelled", FileDownloadStatus = FileDownloadStatus.Cancelled }; } int read = 0; if ((read = await stream.ReadAsync(buffer, token)) > 0) { await fileStream.WriteAsync(buffer.AsMemory(0, read), token); totalRead += read; if (canSendProgress) { progress.Report((totalRead * 1d) / (totalData * 1d) * 100); } } else { isMoreDataToRead = false; } } while (isMoreDataToRead); await fileStream.FlushAsync(token); fileStream.Close(); await fileStream.DisposeAsync(); } File.Copy(tempFilePath, destinationPath, true); FileAccessHelper.DeleteFile(tempFilePath); return new() { StatusMessage = "Operation Completed Successfully", FileDownloadStatus = FileDownloadStatus.Completed }; } catch (OperationCanceledException) { return new() { StatusMessage = $"Download Operation Cancelled", FileDownloadStatus = FileDownloadStatus.Cancelled }; } catch (Exception e) { // Logger.LogException(e, properties); const long ERROR_HANDLE_DISK_FULL = 0x27; const long ERROR_DISK_FULL = 0x70; long errorCode = System.Runtime.InteropServices.Marshal.GetHRForException(e) & 0xFFFF; if (errorCode == ERROR_HANDLE_DISK_FULL || errorCode == ERROR_DISK_FULL) { return new() { StatusMessage = $"Operation Failed, Disk Fill", FileDownloadStatus = FileDownloadStatus.DiskFull }; } return new() { StatusMessage = e.Message, FileDownloadStatus = FileDownloadStatus.Failed }; } finally { cancellationTokenSource.Dispose(); } }
安卓端取消时抛出的异常:
Android.OS.NetworkOnMainThreadException: Exception of type 'Android.OS.NetworkOnMainThreadException' was thrown.
问题根源与解决方案
核心问题
下载命令中用MainThread.BeginInvokeOnMainThread包裹了整个异步下载逻辑,导致网络IO操作实际在主线程执行。安卓系统禁止主线程处理网络请求,取消操作触发线程状态检查时,就抛出了NetworkOnMainThreadException。
修复步骤
1. 重构下载命令,分离UI操作与下载逻辑
将命令改为async Task(避免异步void的潜在问题),仅在需要更新UI的代码块中使用主线程调用:
[RelayCommand] async Task DownloadFile(ReciterChapterDownload reciterChapterDownload) { if (ReciterChapterDownloads.Any(x => x.DownloadReciterChapterId == reciterChapterDownload.DownloadReciterChapterId) && !reciterChapterDownload.Downloaded) { // 仅UI状态更新用主线程 await MainThread.InvokeOnMainThreadAsync(() => { reciterChapterDownload.Downloading = true; reciterChapterDownload.CancellationTokenSource = new CancellationTokenSource(); }); // Progress回调默认自动在UI线程执行,无需额外处理 var progressIndicator = new Progress<double>(reciterChapterDownload.ReportProgress); // 下载逻辑在后台线程执行 var response = await _fileDownloadService .DownloadFileAsync(reciterChapterDownload.DownloadLink, reciterChapterDownload.GetDestinationDownloadPath(), progressIndicator, reciterChapterDownload.CancellationTokenSource); // 最终UI状态更新 await MainThread.InvokeOnMainThreadAsync(() => { reciterChapterDownload.Downloading = false; switch (response?.FileDownloadStatus) { case FileDownloadStatus.Completed: reciterChapterDownload.Downloaded = true; break; case FileDownloadStatus.Failed: App.CurrentShell.DisplayAlert("Operation Failed", response.StatusMessage, "OK"); break; } }); } }
2. 优化取消逻辑,依赖CancellationToken自动抛出异常
移除手动检查IsCancellationRequested的冗余代码,让异步操作自动触发OperationCanceledException,确保进入对应捕获块:
using (cancellationTokenSource) { try { var token = cancellationTokenSource.Token; token.ThrowIfCancellationRequested(); // 初始检查取消状态 using (var fileStream = FileDownloadService.OpenStream(tempFilePath)) { using var stream = await response.Content.ReadAsStreamAsync(token); var totalRead = 0L; var buffer = new byte[bufferSize]; int read; while ((read = await stream.ReadAsync(buffer, token)) > 0) { token.ThrowIfCancellationRequested(); // 每次读取前检查取消 await fileStream.WriteAsync(buffer.AsMemory(0, read), token); totalRead += read; if (canSendProgress) { progress.Report((totalRead * 1d) / totalData * 100); } } await fileStream.FlushAsync(token); } // 改用异步文件操作,保持一致性 await File.CopyAsync(tempFilePath, destinationPath, true); await FileAccessHelper.DeleteFileAsync(tempFilePath); return new() { StatusMessage = "Operation Completed Successfully", FileDownloadStatus = FileDownloadStatus.Completed }; } catch (OperationCanceledException) { // 取消时清理临时文件 if (File.Exists(tempFilePath)) File.Delete(tempFilePath); return new() { StatusMessage = "Download Operation Cancelled", FileDownloadStatus = FileDownloadStatus.Cancelled }; } catch (Exception e) { const long ERROR_HANDLE_DISK_FULL = 0x27; const long ERROR_DISK_FULL = 0x70; long errorCode = System.Runtime.InteropServices.Marshal.GetHRForException(e) & 0xFFFF; if (errorCode == ERROR_HANDLE_DISK_FULL || errorCode == ERROR_DISK_FULL) { return new() { StatusMessage = "Operation Failed, Disk Full", FileDownloadStatus = FileDownloadStatus.DiskFull }; } return new() { StatusMessage = e.Message, FileDownloadStatus = FileDownloadStatus.Failed }; } finally { cancellationTokenSource.Dispose(); } }
关键要点
- 永远不要在主线程执行网络IO或文件IO操作,仅UI更新逻辑使用
MainThread调用。 - 利用
CancellationToken的自动抛出异常机制,避免手动检查取消状态的冗余代码。 - 异步命令优先使用
async Task而非void,保证异常能被正确捕获和处理。
内容的提问来源于stack exchange,提问作者ARH
相关产品推荐
相关产品推荐

