基本思想如下:
关键问题在于,使用后台下载时,您的应用实际上可能会在下载进行过程中终止(例如,由于内存不足而被抛弃)。幸运的是,完成后台下载后,您的应用程序再次启动,但是您最初提供的任何任务级关闭都已一去不复返了。为了解决这个问题,在使用后台会话时,应该依靠委托方法使用的会话级关闭。
import UIKit
import Alamofire
import UserNotifications
fileprivate let backgroundIdentifier = ...
fileprivate let notificationIdentifier = ...
final class BackgroundSession {
/// Shared singleton instance of BackgroundSession
static let shared = BackgroundSession()
/// AlamoFire `SessionManager`
///
/// This is `private` to keep this app loosely coupled with Alamofire.
private let manager: SessionManager
/// Save background completion handler, supplied by app delegate
func saveBackgroundCompletionHandler(_ backgroundCompletionHandler: @escaping () -> Void) {
manager.backgroundCompletionHandler = backgroundCompletionHandler
}
/// Initialize background session
///
/// This is `private` to avoid accidentally instantiating separate instance of this singleton object.
private init() {
let configuration = URLSessionConfiguration.background(withIdentifier: backgroundIdentifier)
manager = SessionManager(configuration: configuration)
// specify what to do when download is done
manager.delegate.downloadTaskDidFinishDownloadingToURL = { _, task, location in
do {
let destination = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent(task.originalRequest!.url!.lastPathComponent)
try FileManager.default.moveItem(at: location, to: destination)
} catch {
print("\(error)")
}
}
// specify what to do when background session finishes; i.e. make sure to call saved completion handler
// if you don't implement this, it will call the saved `backgroundCompletionHandler` for you
manager.delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] _ in
self?.manager.backgroundCompletionHandler?()
self?.manager.backgroundCompletionHandler = nil
// if you want, tell the user that the downloads are done
let content = UNMutableNotificationContent()
content.title = "All downloads done"
content.body = "Whoo, hoo!"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let notification = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(notification)
}
// specify what to do upon error
manager.delegate.taskDidComplete = { _, task, error in
let filename = task.originalRequest!.url!.lastPathComponent
if let error = error {
print("\(filename) error: \(error)")
} else {
print("\(filename) done!")
}
// I might want to post some event to `NotificationCenter`
// so app UI can be updated, if it's in foreground
}
}
func download(_ url: URL) {
manager.download(url)
}
}
Run Code Online (Sandbox Code Playgroud)然后,我可以启动这些下载。请注意,在启动下载时,我没有指定任何特定于任务的关闭,而是仅使用上面的会话级关闭,这些关闭使用的详细信息URLSessionTask来确定要执行的操作:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// request permission to post notification if download finishes while this is running in background
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
if let error = error, !granted {
print("\(error)")
}
}
}
@IBAction func didTapButton(_ sender: Any) {
let urlStrings = [
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/s72-55482.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo10/hires/as10-34-5162.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo-soyuz/apollo-soyuz/hires/s75-33375.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-134-20380.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-140-21497.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-148-22727.jpg"
]
let urls = urlStrings.flatMap { URL(string: $0) }
for url in urls {
BackgroundSession.shared.download(url)
}
}
}
Run Code Online (Sandbox Code Playgroud)如果下载完成后您的应用程序未运行,则iOS需要在重新启动应用程序后知道该操作何时完成,并且可以安全地挂起您的应用程序。因此,在handleEventsForBackgroundURLSession捕获该闭包时:
class AppDelegate: UIResponder, UIApplicationDelegate {
...
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
BackgroundSession.shared.saveBackgroundCompletionHandler(completionHandler)
}
}
Run Code Online (Sandbox Code Playgroud)
sessionDidFinishEventsForBackgroundURLSession在步骤1中被所使用。
两个观察:
仅当下载完成后您的应用未运行时才调用此方法。
但是,如果要进行后台会话,则必须捕获此闭包并在处理完后台会话委托方法之后调用它。
因此,回顾一下,后台会话的基本限制是:
您只能在应用程序处于后台状态时使用下载和上传任务;
您只能依赖会话级委托,因为自发起请求以来,该应用可能已终止;和
在iOS中,您必须实现handleEventsForBackgroundURLSession,捕获该完成处理程序,并在后台进程完成后调用它。
我还必须指出,尽管Alamofire是一个很棒的库,但实际上并没有增加太多价值(超出了URLSession本后台下载过程所提供的价值)。如果仅执行简单的上载/下载,则可以考虑直接使用URLSession。但是,如果您已经在项目中使用Alamofire,或者您的请求包含更复杂的application/x-www-form-urlencoded请求(或其他任何请求),这些优点值得Alamofire的优点,那么以上内容概述了该过程中涉及的关键活动部分。
| 归档时间: |
|
| 查看次数: |
1870 次 |
| 最近记录: |