NSURLSession 后台传输:回调每个从队列下载的视频

NSURLSession background transfer : Callback for each video downloaded from a queue

我正在使用后台传输服务通过 NSURLSession 下载多个视频。当应用程序处于后台模式时,下载工作正常,我对此感到满意。我的问题是,我想为从队列中下载的每个视频回调。

我期待为每个下载的视频调用以下方法:

-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier
 completionHandler:(void (^)())completionHandler

当系统在后台传输后没有更多消息要发送到我们的应用程序时,采用以下方法:

-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session

但是,当所有下载完成时,这两种方法都会被调用。 我放了 3 个视频供下载,然后将 App 置于后台。下载完所有 3 个视频后调用这两种方法。


这是我在这些方法中所做的:

AppDelegate

-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier 
 completionHandler:(void (^)())completionHandler
{    
    self.backgroundTransferCompletionHandler = completionHandler;
}

下载ViewController

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];

    if (appDelegate.backgroundTransferCompletionHandler) 
    {
        void (^completionHandler)() = appDelegate.backgroundTransferCompletionHandler;
        appDelegate.backgroundTransferCompletionHandler = nil;
        completionHandler();
    }

    NSLog(@"All tasks are finished");
}

是否可以在下载每个视频时向用户显示本地通知?或者,我必须等到所有视频在后台完成下载?

如果答案是,那么我的问题是这两个不同回调的目的是什么?是什么将它们彼此分开?

这里的问题是您正在使用 NSURLSessionDelegate,它为您提供有关当前下载会话的信息。 但是,您想知道有关单个任务的信息,而不是整个会话的信息。 因此,您应该查看 NSURLSessionTaskDelegateNSURLSessionDownloadDelegate

具体来说,使用 NSURLSessionDownloadDelegate,您应该实现此委托方法:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location

如果应用程序在下载完成时处于后台模式,则不会自动调用此方法。但是,系统会调用 application:handleEventsForBackgroundURLSession:completionHandler:,让您有机会重建会话并响应事件(例如,按照您的要求触发通知) 更多信息 here:

In iOS, when a background transfer completes or requires credentials, if your app is no longer running, your app is automatically relaunched in the background, and the app’s UIApplicationDelegate is sent an application:handleEventsForBackgroundURLSession:completionHandler: message. This call contains the identifier of the session that caused your app to be launched. Your app should then store that completion handler before creating a background configuration object with the same identifier, and creating a session with that configuration. The newly created session is automatically reassociated with ongoing background activity.

Last,几年前我做了一个开源项目,一个 NSURLSession 的包装器。该项目是为 iOS 7 制作的,因此它可能使用了一些已弃用的方法,但是此答案涵盖的部分仍然有效。 Link to FLDownloader

编辑 在 Rob 的回答之后,我做了一些检查。暂停状态的应用程序和终止状态的应用程序之间的行为似乎不同。

  • 似乎,当应用程序关闭(杀死)时,系统只会在所有下载完成后调用 application:handleEventsForBackgroundURLSession:completionHandler: 唤醒它。我尝试将 XCode 附加到我的 iPhone,它似乎是正确的。
  • 然而,在link,在"Background Transfer Considerations",似乎如果应用程序处于"suspended"状态,它说:

If any task completed while your app was suspended, the delegate’s URLSession:downloadTask:didFinishDownloadingToURL: method is then called with the task and the URL for the newly downloaded file associated with it.

编辑

最后一个断言,event if confirmed by Apple docs,似乎是错误的。我亲自检查了 iPhone 6S、iOS 9.3.2、XCode 和 Instruments。我开始了两次下载并关闭了应用程序(暂停状态,由 Istruments activity 监视器确认 - 进程仍然存在但没有消耗 cpu-时间)但未调用 URLSession:downloadTask:didFinishDownloadingToURL: 方法.但是,当两个下载都完成时,调用了 application:handleEventsForBackgroundURLSession:completionHandler:

Is it possible to show user a local notification on downloading of each video ? Or, I will have to wait til all videos complete downloading in the background ?

只有在与该会话相关的所有下载都完成后,应用程序才会在后台 handleEventsForBackgroundURLSession 重新启动,而不是一个接一个地重新启动。后台会话的想法是尽量减少在后台保持 运行 的电池消耗(或重复启动然后暂停),而是让后台守护进程为你做这件事并让你知道什么时候一切都完成了.

理论上,您可以为每个实例化一个单独的后台会话,但这让我觉得这是对后台会话的滥用(其目的是减少启动应用程序所花费的时间,运行 它在后台),如果 Apple 不赞成这种做法,我也不会感到惊讶。它还需要一个笨拙的实现(具有多个 NSURLSession 个对象)。

If the answer is NO, then my question is what is the purpose of these two different callbacks ? What separates them from each other ?

单独回调的目的是,一旦您的应用再次 运行,它就可以执行每次下载所需的任何 post 处理(例如,将文件从他们的临时位置到他们的最终位置)。每次下载都需要单独的回调,即使当应用程序在后台模式下重新启动时它们会被快速连续调用。另外,如果该应用恰好 运行 已经在前台,您可以在下载完成时处理单独的下载。


顺便说一句,LombaX 是正确的,handleEventsForBackgroundURLSession 应该启动后台会话。就个人而言,我将 completionHandler 设为 NSURLSession 对象包装器的 属性,因此 handleEventsForBackgroundURLSession 将实例化它(准备好调用其委托方法),然后保存completionHandler 那里。这是保存完成处理程序的合乎逻辑的地方,无论如何您都必须实例化 NSURLSession 及其委托,并且它使 URLSessionDidFinishEventsForBackgroundURLSession 无需返回到应用程序委托来获取已保存的完成处理程序。

对不对,我的典型实现是把后台的NSURLSession对象做成单例。因此我最终得到类似的东西:

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {    
    [BackgroundSession sharedSession].savedCompletionHandler = completionHandler;
}

一石二鸟,启动后台NSURLSession,保存completionHandler.