Как правильно протестировать фоновую задачу URLSessionDownloadTask?

Я следую этому руководству Apple по Загрузке файлов в фоновом режиме и задаюсь вопросом, как правильно проверить, что все шаги работают правильно. (также посредством модульного тестирования, если у вас есть ресурсы, поделитесь ими, но не основной целью этого вопроса).

Согласно документации , для этого необходимо выполнить несколько шагов (не перечисляя стандартные делегаты URLSessionDownloadTask, только фоновые требования):

  1. Создайте фоновый сеанс с помощью URLSessionConfiguration.background(withIdentifier: "MyBackgroundDownload")
  2. Реализовать handleEventsForBackgroundURLSession
  3. Реализовать urlSessionDidFinishEvents
  4. (необязательная отлов ошибок) Реализуйте didCompleteWithError

Однако при тестировании приложения, завершаемого системой, я все еще получаю успешный вызов моей реализации urlSession:downloadTask:didFinishDownloadingTo, даже без выполнения шагов 2 и 3.

Если он все еще работает без этих шагов, как я узнаю, что они вообще что-то делают? Нужны ли мне вообще эти шаги? Это не придает мне уверенности.

Чтобы система убила приложение, я использую Инструмент разработчика Fast App Termination на устройстве, поэтому я НЕ провожу отладку непосредственно в Xcode (так как меня беспокоило, что это может каким-то образом поддерживать работоспособность приложения).

@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. Это задокументированное утверждение просто неверно или я его неправильно понял?

teradyl 15.04.2024 01:10

Что вам кажется неправильным? Документация или мое заявление о том, что загрузка завершается даже без выполнения задач 2 и 3 (как указано в моем вопросе)?

teradyl 16.04.2024 00:24
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
97
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы спрашивали:

Как правильно протестировать фон 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))
        }
    }
    

    А затем, когда загрузка завершится, я отправляю пользователю уведомление:

    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. Другой подход — просмотр сообщений журнала 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().

  3. Если эти загрузки занимают так много времени, что смотреть на 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. На этом этапе извлеките [сохраненный обработчик завершения], хранящийся делегатом приложения.


Я только что провел эмпирический тест, чтобы проверить последовательность событий. Это подтверждает, что если ваше приложение просыпается после завершения загрузки/выгрузки, последовательность событий следующая:

  1. Пока приложение работает, создайте фон URLSession и начните загрузку. Я просто выхожу из приложения, пока они выполняются, чтобы приостановить его. Несколько наблюдений:

    • Обратите внимание: при этом вы не можете подключиться к отладчику Xcode, поскольку это искусственно поддерживает работу приложения в фоновом режиме.
    • Излишне говорить, что приложение не будет перезапущено, если вы переопределите настройку конфигурации sessionSendsLaunchEvents на false.
  2. handleEventsForBackgroundURLSession делегата приложения вызывается, если приложение не работало после завершения загрузки и поэтому приложение было перезапущено в фоновом режиме, чтобы вы могли обработать задачи. Если и когда этот метод вызывается, мы

    • Сохраните замыкание для дальнейшего использования и используйте его на шаге 4 ниже.

    • Если приложение ранее было закрыто, мы, очевидно, воссоздаем фоновый сеанс URLSession, используя тот же идентификатор. Если приложение было просто приостановлено (что более вероятно), то, очевидно, у нас все еще работает существующий фон URLSession.

    • Очевидно, это вызывается только в том случае, если приложение просыпается и запускается в фоновом режиме.

    Излишне говорить, что если приложение работало на переднем плане после завершения загрузки, это событие не вызывается. Но вы все равно должны реализовать этот метод для поддержки сценария приостановки/закрытия приложения. (Если вас не беспокоит, что приложение было приостановлено или прекращено во время загрузки, вы, вероятно, вообще не будете использовать фоновый режим URLSession.)

  3. Все методы делегата задачи вызываются (например, DidFinishDownloadingTo, DidCompleteWithError и т. д.) для завершенных фоновых URLSession задач.

  4. Только когда все эти события будут выполнены, будет вызван urlSessionDidFinishEvents. Обычно именно здесь мы обычно вызываем сохраненное закрытие обработчика завершения, которое мы получили на шаге 2. (Некоторые могут заметить, что если вы запустили дополнительные асинхронные события в результате шага 3, то вы должны отложить вызов закрытия до тех пор, пока все это тоже сделано, но это своего рода крайний случай.)

Обратите внимание: я поменял местами 3 и 4 из исходного списка, так как методы делегата задачи вызываются до urlSessionDidFinishEvents.

Другие вопросы по теме