我正在遵循Downloading Files in the Background上的Apple指南,并想知道如何正确测试所有步骤是否正常工作.(也通过单元测试,如果您有资源,请分享,但不是这个问题的主要焦点).

根据documentation,有多个步骤可以实现这一目标(没有列出标准的URLSessionDownloader任务委托,仅列出后台要求):

  1. URLSessionConfiguration.background(withIdentifier: "MyBackgroundDownload")人一起创建背景会议
  2. 器具handleEventsForBackgroundURLSession
  3. 器具urlSessionDidFinishEvents
  4. (可选错误捕获)实现didCompleteWithError

然而,当测试应用程序被系统终止时,即使没有实现步骤2和3,我仍然成功调用了我的urlSession:downloadTask:didFinishDownloadingTo实现.

如果没有这些步骤它仍然有效,我怎么知道他们正在做任何事情?我需要这些步骤吗?这并没有给我信心.

为了让应用程序被系统杀死,我在设备上使用了Fast App Termination developer tool,所以我不会直接在Xcode中调试(因为我担心这可能会以某种方式保持应用程序的活力).

推荐答案

你问:

如何正确测试背景URLSessionDownloadTask

如您所指出的,您无法从Xcode调试器测试挂起/终止的应用程序会发生什么(因为调试器将保持应用程序运行).因此,只需在设备上安装应用程序,然后直接在设备上启动它,而不是从Xcode启动.

因此,这就引出了一个问题,即如何确认后台执行期间发生的事情.具体来说,问题是在没有Xcode控制台或前台运行的应用程序的情况下如何监控正在发生的事情.

我使用多种技术来监控后台执行期间发生的事情:

  1. 当在后台完成下载时(至少在测试时),我会让应用程序呈现"用户通知".通过这种方式,即使应用程序在后台运行,我也会获得视觉确认.

    因此,当应用程序处于前台时,我请求用户通知权限:

    import UserNotifications
    

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    
        UNUserNotificationCenter.current().requestAuthorization(options: .alert) { granted, error in
            print(granted, String(describing: error))
        }
    }
    

    和 then, when the download is done, I post a user notification:

    extension BackgroundSession: URLSessionDelegate {
        func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    
            UNUserNotificationCenter.current().add(title: "Downloads finished", body: "Hurray")
    
            // then continue doing other stuff; e.g., in this method, I 
            // would often call the saved completion handler, if any
            //
            // DispatchQueue.main.async {
            //     self.savedCompletionHandler?()
            //     self.savedCompletionHandler = nil
            // }
        }
    }
    
    extension UNUserNotificationCenter {
        func add(title: String, body: String) {
            let content = UNMutableNotificationContent()
            content.title = title
            content.body = body
            let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
            let request = UNNotificationRequest(identifier: Bundle.main.bundleIdentifier!, content: content, trigger: trigger)
    
            add(request)
        }
    }
    
  2. 另一种方法是观看来自macOS Console的iOS日志(log)消息.为此,请创建Logger:

    import os.log
    
    private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "BackgroundSession")
    

    和 then I will have key events log messages.

    extension BackgroundSession: URLSessionDelegate {
        func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
            logger.debug(#function)
    
            …
        }
    }
    

    然后我可以在我的macOS Console应用程序上观看这些iOS活动.请参阅Swift: print() vs println() vs NSLog()中的Logger讨论.

  3. 如果这些下载需要很长时间,以至于盯着macOS Console不切实际,我有时会将这些事件记录到某个持久性存储中(例如,无论是SwiftData/CoreData还是只是将它们写入文本或杨森文件).然后我可以稍后从Xcode"设备"窗口下载该内容.(对于背景URLSession,上述Console方法就足够了,但该技术对于计时不太及时/可预测的背景事件(例如BGProcessingTaskBGAppRefreshTask等)非常有用.)


您提到了设备上的"快速应用程序终止"开发人员设置.测试这种"后台重新启动"功能没有必要.是的,测试"如果应用程序在其正常生命周期中被终止怎么办"工作流(以确保后台URLSession被正确重新实例化)的场景是有用的.但没有必要简单地验证"后台重新启动应用程序"功能.


您说尽管没有实现handleEventsForBackgroundURLSessionurlSessionDidFinishEvents,但它似乎有效.

如果您不希望您的应用程序在后台请求完成时被唤醒,则无需实现这些功能(即,您明确将sessionSendsLaunchEvents设置为false).

但如果您确实希望在网络请求完成时在后台启动您的应用程序,Apple非常明确地表示我们应该实现这两种方法.这种模式背后的 idea 是,如果应用程序在后台启动,我们希望在完成时告诉操作系统,并且应用程序可以再次暂停.文档并不总是明确说明未能实现所有必要的委托方法的影响,但通常,未能履行后台执行义务的应用程序future 可能没有资格进行后台执行.苹果对滥用后台执行的应用程序越来越严格,因此我建议按照documentation中的建议实施API.


您分享了文档中的一句话,其中写道:

恢复的应用程序调用完成处理程序后,下载任务就会完成其工作并调用委托的urlSession(_:downloadTask:didFinishDownloadingTo:)方法.

这似乎意味着didFinishDownloadingTo被称为after,你称之为关闭.事实并非如此.如下所述,urlSessionDidFinishEvents(这是我们经常调用保存的Close的地方)只有在所有任务委托方法(包括didFinishDownloadingTo)被调用后才会调用,而不是在之前.

此外,正确的事件顺序已在同一document个中概述:

当所有事件都已交付时,系统将调用URLSessionDelegateurlSessionDidFinishEvents(forBackgroundURLSession:)方法.此时,获取应用程序委托存储的[已保存的完成处理程序].


我刚刚做了一个实证测试来验证事件的顺序.这证实了如果您的应用程序在上传/下载完成时被唤醒,则事件顺序如下:

  1. 当应用程序运行时,创建背景URLSession并开始下载.我只是在他们暂停应用程序时离开应用程序.一些观察:

    • 请注意,当您执行此操作时,您无法连接到Xcode调试器,因为这会人为地使应用程序在后台运行.
    • 不用说,如果您将sessionSendsLaunchEvents配置设置改写为false,该应用程序将不会重新启动.
  2. 如果下载完成时应用程序未运行,则会调用应用程序委托的handleEventsForBackgroundURLSession,因此应用程序将在后台重新启动以让您处理任务.如果调用此方法时,我们

    • 保存关闭以供将来参考,并将在下面的步骤4中使用它.

    • 如果应用程序之前已被终止,那么我们显然会使用相同的标识符重新创建后台URLSession会话.如果应用程序只是被暂停(这更有可能),那么显然我们仍然在运行现有的后台URLSession.

    • 显然,只有在应用程序被唤醒并在后台启动时,才会调用此操作.

    不用说,如果下载完成时应用程序恰好在前台运行,则不会调用此事件.但您仍然必须实现此方法才能支持应用程序已暂停/终止的情况.(如果您不担心应用程序在下载过程中被暂停/终止,那么您可能根本不会使用后台URLSession.)

  3. 调用所有任务委托方法(例如,didFinishDownloadingTodidCompleteWithError等)用于已完成的后台URLSession任务.

  4. 只有当所有这些活动结束后,urlSessionDidFinishEvents人才会被召集.这通常是我们在步骤2中收到的保存的完成处理程序关闭的地方.(有些人可能会注意到,如果您在步骤3中启动了额外的同步事件,那么您会推迟对关闭的调用,直到所有这些都完成,但这有点边缘情况.)

请注意:我已经从您的原始列表中交换了3 4,因为任务委托方法称为before urlSessionDidFinishEvents is.

Ios相关问答推荐

preferredCompactColumn作为NavigationSplitViewColumn.sidebar传递,但仍能在iOS上看到详细信息页面

为什么导航栏在与Style.Page的选项卡栏一起使用时停止工作?

无法在所有窗口上方显示图像

在金属中设置纹理的UV坐标以编程方式变形纹理

分配给指针会通过 class_addMethod 导致 EXC_BAD_ACCESS

如何创建装饰视图?

在 SwiftUI 中,绑定应该放在 ViewModel 中吗?

为什么@FetchRequest 在删除时不更新和重绘视图?

发生异常:需要 issuerId

NSOperation 有延迟 - 它是异步的还是同步的?

如何使用 AppIntents 在 SwiftUI 应用程序中打开特定视图

帧过渡动画有条件地工作

如何让我的 iOS 应用程序与 api 连接?

iOS,Swift 5,AppDelegate open url / .onOpenUrl - 如何找到调用我的自定义方案的调用应用程序

如何将标准化坐标系中的点的位置转换为具有相对位置的常规坐标系?

如何在 iOS 的 xib 中设置十六进制 colored颜色 代码

如何在 iPhone 中为透明的 PNG 图像着色?

用 PC 调试 ipad safari

使用未知类型创建图像格式是一个错误与 UIImagePickerController

iOS:如何从 URL 调试新启动应用程序