You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 23:20:18