You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

iOS锁屏时Xamarin.Forms下载中断及后台任务实现求助

解决Xamarin.Forms iOS锁屏后下载中断的问题

首先得说清楚你之前踩的几个坑:

  • WebClient不适合iOS后台场景:WebClient基于旧的网络实现,iOS系统在应用进入后台/锁屏后会直接挂起这类非系统认可的网络请求,这就是为什么Android正常但iOS中断的核心原因。
  • BeginBackgroundTask用错了场景:这个API是给短时间任务(比如保存用户数据,最多几分钟)用的,根本撑不住持续的下载需求;而且如果没正确配对调用EndBackgroundTask,系统可能直接杀了你的应用。
  • Background Fetch不对症:它是用来定期唤醒应用拉取少量更新的,不是用来做持续下载的,完全不匹配你的需求。

正确解决方案:iOS后台URL会话(NSUrlSession)

苹果专门给后台下载/上传设计了NSUrlSession的后台会话机制——即使应用被挂起、锁屏甚至被系统杀死,系统会接管下载任务,完成后再唤醒你的应用处理结果。这是唯一符合苹果规则的后台下载方案。

步骤1:配置iOS项目后台权限

在你的iOS项目里,打开Info.plist(或者通过项目属性的iOS选项卡),添加后台模式权限:

  • 勾选「Background fetch」和「Remote notifications」(这两个是后台会话正常工作的必要配置)
  • 对应的plist代码会自动生成:
<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>
    <string>remote-notification</string>
</array>

步骤2:实现iOS原生下载服务(依赖注入)

先在共享代码里定义一个下载服务的接口:

public interface IDownloadService
{
    // 启动下载:参数包含下载地址、本地保存路径、进度回调、完成回调
    void StartDownload(string downloadUrl, string localFilePath, Action<double> progressCallback, Action<string> completionCallback);
    // 取消下载
    void CancelDownload();
}

然后在iOS项目里实现这个接口,用NSUrlSession后台会话:

using Foundation;
using System;
using UIKit;
using Xamarin.Forms;

[assembly: Dependency(typeof(YourAppName.iOS.Services.iOSBackgroundDownloadService))]
namespace YourAppName.iOS.Services
{
    public class iOSBackgroundDownloadService : IDownloadService
    {
        private NSUrlSession _backgroundSession;
        private Action<double> _progressCallback;
        private Action<string> _completionCallback;
        private string _targetFilePath;

        public iOSBackgroundDownloadService()
        {
            // 创建唯一标识的后台会话配置,这个ID要固定,系统靠它恢复会话
            var config = NSUrlSessionConfiguration.CreateBackgroundSessionConfiguration("com.yourapp.background.download");
            // 初始化会话,绑定自定义代理
            _backgroundSession = NSUrlSession.FromConfiguration(config, new DownloadSessionDelegate(this), null);
        }

        public void StartDownload(string downloadUrl, string localFilePath, Action<double> progressCallback, Action<string> completionCallback)
        {
            _progressCallback = progressCallback;
            _completionCallback = completionCallback;
            _targetFilePath = localFilePath;

            var url = NSUrl.FromString(downloadUrl);
            var request = NSUrlRequest.FromUrl(url);
            // 创建下载任务并启动
            var downloadTask = _backgroundSession.CreateDownloadTask(request);
            downloadTask.Resume();
        }

        public void CancelDownload()
        {
            _backgroundSession?.InvalidateAndCancel();
        }

        // 自定义代理类,处理下载的进度、完成、失败事件
        private class DownloadSessionDelegate : NSUrlSessionDownloadDelegate
        {
            private readonly iOSBackgroundDownloadService _service;

            public DownloadSessionDelegate(iOSBackgroundDownloadService service)
            {
                _service = service;
            }

            // 下载进度更新
            public override void DidWriteData(NSUrlSession session, NSUrlSessionDownloadTask downloadTask, long bytesWritten, long totalBytesWritten, long totalBytesExpectedToWrite)
            {
                double progress = (double)totalBytesWritten / totalBytesExpectedToWrite;
                // 必须回到主线程更新UI
                UIApplication.SharedApplication.InvokeOnMainThread(() =>
                {
                    _service._progressCallback?.Invoke(progress);
                });
            }

            // 下载完成:系统会把文件存在临时路径,需要手动移动到目标位置
            public override void DidFinishDownloading(NSUrlSession session, NSUrlSessionDownloadTask downloadTask, NSUrl tempLocation)
            {
                var targetUrl = NSUrl.FromFilename(_service._targetFilePath);
                NSError error = null;
                NSFileManager.DefaultManager.MoveItem(tempLocation, targetUrl, out error);

                UIApplication.SharedApplication.InvokeOnMainThread(() =>
                {
                    if (error == null)
                    {
                        _service._completionCallback?.Invoke(targetUrl.Path);
                    }
                    else
                    {
                        _service._completionCallback?.Invoke(null);
                    }
                });
            }

            // 下载失败处理
            public override void DidCompleteWithError(NSUrlSession session, NSUrlSessionTask task, NSError error)
            {
                UIApplication.SharedApplication.InvokeOnMainThread(() =>
                {
                    _service._completionCallback?.Invoke(null);
                });
            }
        }
    }
}

步骤3:在AppDelegate中处理后台唤醒回调

当系统完成下载后,会唤醒你的应用,你需要在AppDelegate里处理这个回调,告诉系统任务已完成:

public override void HandleEventsForBackgroundUrl(UIApplication application, string sessionIdentifier, Action completionHandler)
{
    // 找到对应的后台会话(确保和之前创建的ID一致)
    var session = NSUrlSession.FromSessionIdentifier(sessionIdentifier);
    if (session != null)
    {
        // 这里可以做一些额外的会话恢复操作,比如重新绑定代理
        // 最后必须调用completionHandler,告诉系统处理完成
        completionHandler();
    }
}

步骤4:在Xamarin.Forms共享代码中调用

在你的页面ViewModel或代码里,通过依赖注入获取服务并调用:

var downloadService = DependencyService.Get<IDownloadService>();
downloadService.StartDownload(
    downloadUrl: "你的下载链接",
    localFilePath: "本地保存路径(比如Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "/file.zip")",
    progressCallback: progress => {
        // 更新UI进度条,比如ProgressBar.Progress = progress;
    },
    completionCallback: filePath => {
        if (!string.IsNullOrEmpty(filePath))
        {
            // 下载成功,处理文件
        }
        else
        {
            // 下载失败,提示用户
        }
    }
);

关键注意事项

  1. 必须用真机测试:模拟器的后台行为和真机差异很大,很多后台功能在模拟器上无法正常触发。
  2. 会话ID要固定:后台会话的唯一标识不能变,否则系统无法恢复之前的下载任务。
  3. UI更新必须在主线程:iOS不允许在子线程操作UI,所以所有回调里的UI更新都要通过InvokeOnMainThread处理。
  4. 苹果的资源限制:后台下载不是无限制的,如果滥用(比如同时下载大量大文件),可能会被系统限制甚至拒审。

内容的提问来源于stack exchange,提问作者cheran

火山引擎 最新活动