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 { // 下载失败,提示用户 } } );
关键注意事项
- 必须用真机测试:模拟器的后台行为和真机差异很大,很多后台功能在模拟器上无法正常触发。
- 会话ID要固定:后台会话的唯一标识不能变,否则系统无法恢复之前的下载任务。
- UI更新必须在主线程:iOS不允许在子线程操作UI,所以所有回调里的UI更新都要通过
InvokeOnMainThread处理。 - 苹果的资源限制:后台下载不是无限制的,如果滥用(比如同时下载大量大文件),可能会被系统限制甚至拒审。
内容的提问来源于stack exchange,提问作者cheran




