Я следую этому руководству Apple по Загрузке файлов в фоновом режиме и задаюсь вопросом, как правильно проверить, что все шаги работают правильно. (также посредством модульного тестирования, если у вас есть ресурсы, поделитесь ими, но не основной целью этого вопроса).
Согласно документации , для этого необходимо выполнить несколько шагов (не перечисляя стандартные делегаты URLSessionDownloadTask, только фоновые требования):
URLSessionConfiguration.background(withIdentifier: "MyBackgroundDownload")
handleEventsForBackgroundURLSession
urlSessionDidFinishEvents
didCompleteWithError
Однако при тестировании приложения, завершаемого системой, я все еще получаю успешный вызов моей реализации urlSession:downloadTask:didFinishDownloadingTo
, даже без выполнения шагов 2 и 3.
Если он все еще работает без этих шагов, как я узнаю, что они вообще что-то делают? Нужны ли мне вообще эти шаги? Это не придает мне уверенности.
Чтобы система убила приложение, я использую Инструмент разработчика Fast App Termination на устройстве, поэтому я НЕ провожу отладку непосредственно в Xcode (так как меня беспокоило, что это может каким-то образом поддерживать работоспособность приложения).
Что вам кажется неправильным? Документация или мое заявление о том, что загрузка завершается даже без выполнения задач 2 и 3 (как указано в моем вопросе)?
Вы спрашивали:
Как правильно протестировать фон
URLSessionDownloadTask
?
Как вы заметили, вы не можете проверить, что происходит с приостановленным/завершенным приложением, с помощью отладчика Xcode (поскольку отладчик будет поддерживать работу приложения). Итак, просто установите приложение на устройство, а затем запустите его непосредственно на устройстве, а не из Xcode.
Возникает вопрос, как можно подтвердить, что происходит во время фонового выполнения. В частности, вопрос в том, как отслеживать происходящее без использования консоли Xcode или приложения, работающего на переднем плане.
Существует множество методов, которые я использую для мониторинга того, что происходит во время фонового выполнения:
Я заставлю приложение отображать «уведомление пользователя», когда загрузка будет выполняться в фоновом режиме (по крайней мере, во время его тестирования). Таким образом, я получаю визуальное подтверждение, даже если приложение работает в фоновом режиме.
Итак, когда приложение находится на переднем плане, я запрашиваю разрешения на уведомления пользователей:
import UserNotifications
И
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UNUserNotificationCenter.current().requestAuthorization(options: .alert) { granted, error in
print(granted, String(describing: error))
}
}
А затем, когда загрузка завершится, я отправляю пользователю уведомление:
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)
}
}
Другой подход — просмотр сообщений журнала iOS из macOS Console
. Для этого создайте Регистратор:
import os.log
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "BackgroundSession")
И тогда у меня будут ключевые сообщения журнала событий.
extension BackgroundSession: URLSessionDelegate {
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
logger.debug(#function)
…
}
}
Затем я смогу наблюдать за этими событиями iOS в своем приложении для MacOS Console
. См. обсуждение Logger
в Swift: print() vs println() vs NSLog().
Если эти загрузки занимают так много времени, что смотреть на macOS Console
нецелесообразно, я иногда записываю эти события в какое-нибудь постоянное хранилище (например, SwiftData/CoreData или просто записываю их в текстовый файл или файл JSON). Затем я смогу загрузить это из окна «Устройства» Xcode позже. (Для фонового URLSession
вышеупомянутого подхода Console
достаточно, но этот метод может быть очень полезен для фоновых событий, время которых менее быстрое/предсказуемое, таких как BGProcessingTask
, BGAppRefreshTask
и т. д.)
Вы упоминаете настройку разработчика «Быстрое завершение приложения» на устройстве. Нет необходимости тестировать функцию «перезапуска в фоновом режиме». Да, полезно протестировать сценарий «что, если приложение было закрыто в ходе его обычного жизненного цикла» (чтобы убедиться, что фон URLSession
правильно создан заново). Но не обязательно просто проверять функцию «перезапустить приложение в фоновом режиме».
Вы сказали, что это работает, несмотря на отсутствие реализации handleEventsForBackgroundURLSession и urlSessionDidFinishEvents.
Вам не нужно их реализовывать, если вы не хотите, чтобы ваше приложение просыпалось после завершения фоновых запросов (т. е. вы явно установили для sessionSendsLaunchEvents значение false
).
Но если вы хотите, чтобы ваше приложение запускалось в фоновом режиме после завершения сетевого запроса, Apple совершенно конкретно указывает, что мы должны реализовать эти два метода. Идея этого шаблона заключается в том, что если приложение запускается в фоновом режиме, мы хотим сообщить ОС, когда мы закончим, и приложение можно снова приостановить. В документации не всегда четко описываются последствия невозможности реализации всех необходимых методов делегата, но обычно приложения, которые не выполняют свои обязательства по фоновому выполнению, могут стать неприемлемыми для фонового выполнения в будущем. Apple ужесточает строгие требования к приложениям, злоупотребляющим фоновым выполнением, поэтому я бы посоветовал внедрить API, как рекомендовано в документации .
Вы поделились цитатой из документации, в которой говорится:
Как только ваше возобновленное приложение вызывает обработчик завершения, задача загрузки завершает свою работу и вызывает метод делегата
urlSession(_:downloadTask:didFinishDownloadingTo:)
.
Казалось бы, это означает, что DidFinishDownloadingTo вызывается после вызова замыкания. Это не относится к делу. Как указано ниже, urlSessionDidFinishEvents
(именно здесь мы часто вызываем сохраненное замыкание) вызывается только после того, как были вызваны все методы делегата задачи (включая didFinishDownloadingTo
), а не раньше.
Более того, правильная последовательность событий изложена ранее в том же документе:
Когда все события доставлены, система вызывает метод
urlSessionDidFinishEvents(forBackgroundURLSession:)
URLSessionDelegate
. На этом этапе извлеките [сохраненный обработчик завершения], хранящийся делегатом приложения.
Я только что провел эмпирический тест, чтобы проверить последовательность событий. Это подтверждает, что если ваше приложение просыпается после завершения загрузки/выгрузки, последовательность событий следующая:
Пока приложение работает, создайте фон URLSession
и начните загрузку. Я просто выхожу из приложения, пока они выполняются, чтобы приостановить его. Несколько наблюдений:
false
.handleEventsForBackgroundURLSession делегата приложения вызывается, если приложение не работало после завершения загрузки и поэтому приложение было перезапущено в фоновом режиме, чтобы вы могли обработать задачи. Если и когда этот метод вызывается, мы
Сохраните замыкание для дальнейшего использования и используйте его на шаге 4 ниже.
Если приложение ранее было закрыто, мы, очевидно, воссоздаем фоновый сеанс URLSession
, используя тот же идентификатор. Если приложение было просто приостановлено (что более вероятно), то, очевидно, у нас все еще работает существующий фон URLSession
.
Очевидно, это вызывается только в том случае, если приложение просыпается и запускается в фоновом режиме.
Излишне говорить, что если приложение работало на переднем плане после завершения загрузки, это событие не вызывается. Но вы все равно должны реализовать этот метод для поддержки сценария приостановки/закрытия приложения. (Если вас не беспокоит, что приложение было приостановлено или прекращено во время загрузки, вы, вероятно, вообще не будете использовать фоновый режим URLSession
.)
Все методы делегата задачи вызываются (например, DidFinishDownloadingTo, DidCompleteWithError и т. д.) для завершенных фоновых URLSession
задач.
Только когда все эти события будут выполнены, будет вызван urlSessionDidFinishEvents. Обычно именно здесь мы обычно вызываем сохраненное закрытие обработчика завершения, которое мы получили на шаге 2. (Некоторые могут заметить, что если вы запустили дополнительные асинхронные события в результате шага 3, то вы должны отложить вызов закрытия до тех пор, пока все это тоже сделано, но это своего рода крайний случай.)
Обратите внимание: я поменял местами 3 и 4 из исходного списка, так как методы делегата задачи вызываются до urlSessionDidFinishEvents.
@Rob Вы говорите, что моя загрузка завершится в любом случае, и что задачи 2 и 3 предназначены просто для помощи ОС и не требуются для завершения загрузки. Если это так, то то, что я вижу в своих тестах, имеет смысл. Однако, согласно документам:
Once your resumed app calls the completion handler, the download task finishes its work and calls the delegate’s urlSession(_:downloadTask:didFinishDownloadingTo:) method.
Это задокументированное утверждение просто неверно или я его неправильно понял?