[Из песочницы] Фоновое выполнение приложений в iOS 13

Главное событие года в мире iOS и MacOS-разработчиков, WWDC, неизменно радует всех занятых в индустрии людей выходом в свет новых версий операционной системы, фреймворков, железа — в общем, всего того, с чем собственно и предстоит работать.

Этот год исключением не стал и предметом активных обсуждений стали и новая версия iOS, и SwiftUI, и собственная ОС iPadOS для вы-сами-поняли-каких-устройств.

Однако были темы не столь освещенные, но от этого не менее интересные — например была затронута такая полезная вещь, как фоновое выполнение приложений.

Вкратце о теме


Для тех кто не совсем представляет, что такое фоновое выполнение (Background execution), поясним: выполнение в фоне означает выполнение приложением какой-либо работы в состоянии, когда физически на экране смартфона пользователя оно не запущено (т.е. не находится в foreground).

image

Данная картинка наглядно показывает, в каких состояниях приложение может пребывать и как попасть из одного в другое.

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

Что было на WWDC


Собственно докладчики начали с того же, с чего и я — вкратце рассказали про работу в фоне и описали задачи, предполагающие свое выполнение там.

image

Двигаемся ближе к разработке. Есть 3 основных соображения Apple, когда речь заходит о фоновом исполнении:

  • мощность
  • производительность
  • конфиденциальность


Все довольно просто — если задача приложения выполняется в фоновом режиме, это выполнение может происходить во время работы другого приложения на переднем плане (foreground). Таким образом, существует ограничение ресурсов, наложенное на вас, чтобы гарантировать, что ресурсы не будут чрезмерно потребляться. При превышении этих ограничений, ваше приложение может быть принудительно прекращено Apple, что приведет к замедлению дальнейшего времени запуска. По поводу конфиденциальности — компания настойчиво рекомендует разработчикам использовать все возможности их API для информирования пользователя о том, какие данные приложение использует в своей работе (даже в фоне).

Кстати список этих самых API выкатили довольно внушительный — обзор по ним отдельно делать не стали, но я советую обратить внимание в их сторону.

Да и на самой сессии на примере приложения-мессенджера разработчики Apple показали краткий обзор возможностей (видео с WWDC посмотреть стоит).

BackgroundTasks


Данному фреймворку посвящена чуть ли не половина доклада в этом году.

2 вида заданий, которые предоставляет эта новинка:

  • App Refresh Task
  • Background Processing Task


App Refresh Task


BG App Refresh Task — специальный тип фоновой задачи, который мы можем использовать для обновления данных приложения. Одна вещь, которая делает этот тип задачи очень особенным, — это поведение пользователя. iOS узнает, как часто и в какое время пользователь запускает ваше приложение, и пытается запустить BGAppRefreshTask в то время, когда пользователь вряд ли будет использовать приложение.

image

Да, дольше 30 секунд работать не получится — особенность весьма неприятная, оставшаяся с прошлых версий ОС.

Рассмотрим пример кода с просторов Интернета.

import UIKit
import BackgroundTasks

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: "pl.snowdog.example.refresh",
            using: DispatchQueue.global()
        ) { task in
            self.handleAppRefresh(task)
        }
        return true
    }

    private func handleAppRefresh(_ task: BGTask) {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1
        let appRefreshOperation = AppRefreshOperation()
        queue.addOperation(appRefreshOperation)

        task.expirationHandler = {
            queue.cancelAllOperations()
        }

        let lastOperation = queue.operations.last
        lastOperation?.completionBlock = {
            task.setTaskCompleted(success: !(lastOperation?.isCancelled ?? false))
        }

        scheduleAppRefresh()
    }
}


Есть несколько моментов, на которых надо остановиться и рассмотреть получше:

  • Установите обработчик истечения срока действия на объект задачи (expirationHandler), так ка система предоставляет ограниченное время для завершения работы, и при его превышении, вам придется очистить ресурсы.
  • Убедитесь, что метод setTaskCompleted был вызван, как только работа будет завершена.
  • BGTaskScheduler — это основной класс, который использeуется для планирования фоновой работы.


Background Processing Task


Другим типом фоновых тасок является BG Processing Task. Вы можете использовать его для обучения модели ML на устройстве или сделать очистку в базе данных. Apple обещает, что на этот вид задач система способна выделять до нескольких минут времени, что является очень важным нововведением для тяжеловесных работ, не укладывающихся в жалкие 30 секунд.

Снова пример:

import UIKit
import BackgroundTasks

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: "pl.snowdog.example.train",
            using: DispatchQueue.global()
        ) { task in
            self.handleMLTrain(task)
        }
        return true
    }

    private func scheduleMLTrain() {
        do {
            let request = BGProcessingTaskRequest(identifier: "pl.snowdog.example.train")
            request.requiresExternalPower = true
            request.requiresNetworkConnectivity = true
            try BGTaskScheduler.shared.submit(request)
        } catch {
            print(error)
        }
    }
}


Важные моменты:

  • requiesExternalPower — логическое значение, указывающее, требуется ли для задачи обработки устройство, подключенное к источнику питания.
  • requiresNetworkConnectivity — логическое значение, указывающее, требуется ли для задачи обработки подключение к сети.


Отладка


Здесь тоже не без приятностей. Задержка между запланированным временем выполнения фоновой задачи и моментом, когда система запускает приложение для ее выполнения, может составлять несколько часов. При разработке приложения можно использовать две частные функции для запуска задачи и принудительного ее досрочного завершения в соответствии с выбранной временной шкалой. Функции отладки работают только на устройствах.

  • Запуск задачи:
    e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"TASK_IDENTIFIER"]
  • Принудительное досрочное прекращение выполнения задания:
    e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"TASK_IDENTIFIER"]


В заключение


BackgroundTasks — это отличный способ планирования вашей тяжелой работы с лучшим пользовательским опытом, используя условия среды. Учитывая то, что с 13-й версии системы многие методы для работы в фоне становятся устаревшими, фреймворк все равно будет пользоваться большой популярностью в приложениях. Учитывая вышеописанные возможности, это вполне логично.

Рекомендую также посмотреть видео с WWDC, описанные там примеры весьма интересны.

© Habrahabr.ru