iOS — запуск сетевого запроса в фоновом режиме при закрытом приложении

#ios #objective-c #swift #network-programming #taskscheduler

#iOS #swift #objective-c #сетевое программирование #запланированные задачи

Вопрос:

В настоящее время я работаю над кроссплатформенным (родным Java и Swift / Objective-C) мобильным приложением, в котором пользователи часто могут иметь ограниченный доступ к сетевому подключению, но мы хотим записывать и отправлять статистику, когда сеть становится доступной, даже если наше приложение было закрыто. Мы уже довольно легко сделали это на Android с их AndroidX.WorkManager библиотекой, например:

 String URL = "https://myexampleurl.com";
Data inputData = new Data.Builder()
    .putString("URL", URL)
    .build();

Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();

OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(NetworkWorker.class)
    .setInputData(inputData)
    .setConstraints(constraints)
    .build();

WorkManager.getInstance(context).enqueueUniqueWork("com.lkuich.exampleWorker", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest);
  

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

Однако я изо всех сил пытался найти эквивалент iOS для этого API, и я довольно новичок в разработке iOS. Мы включили возможность фоновой задачи и зарегистрировали наши идентификаторы Plist.info в нашем проекте.

Сначала мы попробовали URLSession «фоновый» API следующим образом:

 class NetworkController: NSObject, URLSessionTaskDelegate {
    func buildTask() -> URLSession {
        let config = URLSessionConfiguration.background(withIdentifier: "com.lkuich.exampleWorker")
        config.waitsForConnectivity = true
        config.shouldUseExtendedBackgroundIdleMode = true
        
        return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
    }
    
    func makeNetworkRequest(session: URLSession) {
        let url = URL(string: "https://myexampleurl.com")!
        
        let task = session.dataTask(with: url)
        task.resume()
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        print("We're done here")
    }
}
  

Это хорошо работает, когда Wi-Fi отключен и снова включен, но если приложение закрыто, запрос, похоже, никогда не отправляется.

Итак, мы попробовали BGTaskScheduler вместо этого, но мне сложно проверить, работает ли он в фоновом режиме с закрытым приложением, поскольку на его фактический запуск часто может потребоваться несколько часов (принудительный запуск при работе с сетевым подключением):

 static let bgIdentifier = "com.lkuich.exampleWorker"

// Invoked on app start (didFinishLaunchingWithOptions)
static func registerBackgroundTasks() {
    BGTaskScheduler.shared.register(forTaskWithIdentifier: bgIdentifier, using: nil) { (task) in        
        task.expirationHandler = {
            task.setTaskCompleted(success: false)
        }
        
        // Make my network request in BG
        let url = URL(string: "https://myexampleworker.com")!
        let t = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
            // Print the response
            print(response)

            task.setTaskCompleted(success: true)
        })
        t.resume()
    }
}

static func makeRequest() {
    do {
        let bgRequest = BGProcessingTaskRequest(identifier: bgIdentifier)
            bgRequest.requiresNetworkConnectivity = true
            bgRequest.requiresExternalPower = false
            
        try BGTaskScheduler.shared.submit(bgRequest)
        print("Submitted task request")
    } catch {
        print("Failed to submit BGTask")
    }
}
  

Можно ли запланировать выполнение сетевого запроса в фоновом режиме, когда сеть доступна, даже если вызывающее приложение закрыто? И если да, то как мне передать входные данные (например, аргументы URL, например) в мою фоновую задачу, поскольку моя задача должна быть зарегистрирована при запуске приложения?

Ответ №1:

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

Это важно, потому что, когда приложение закрыто, вы ничего не можете делать в фоновом режиме. Исключение составляют только приложения VoIP (использование PushKit, которое теперь разрешено использовать, если приложение не имеет функций VoIP, вы не получите доступ к AppStore) и осложнения watchOS. Но для всех других приложений, когда приложение отключено, у вас вообще нет вариантов. Это просто стандарт Apple, они не хотят, чтобы приложения, которые не запущены, могли запускать код по соображениям безопасности.

Вы можете зарегистрировать некоторые задачи, которые необходимо выполнить, когда приложение все еще работает в фоновом режиме, используя упомянутый вами API BGTask. Я использовал тот же API для одного из своих приложений и обнаружил, что он чрезвычайно изменчив. Иногда задача выполняется каждые 30 минут, а затем она не будет выполняться до завтрашнего утра, а затем в какой-то момент она просто перестанет выполняться вообще.

Поэтому я думаю, что тихие push-уведомления — лучший выход, но они также работают только тогда, когда приложение все еще находится в фоновом режиме, а не убито. Если в вашем приложении нет функций VoIP, в этом случае вы можете использовать PushKit.

Комментарии:

1. Спасибо за ваш ответ, Андре, я бы хотел использовать URLSession метод ( URLSessionConfiguration.background ), но, похоже, он никогда не запускается в фоновом режиме. Связано ли это с теми же проблемами планирования, что и фоновые задачи, созданные с BGTaskScheduler помощью?

2. Вы не можете планировать URLSession само по себе, оно должно быть частью BGTask , чтобы быть запланированным для выполнения в фоновом режиме. И это должно быть URLSessionConfiguration.background . Я столкнулся с проблемой, когда фоновое выполнение приостанавливалось на случайной строке до следующего вызова. Таким образом, в следующий раз, когда система запустит приложение в фоновом режиме, выполнение продолжится с того места, где оно остановилось. Странно, не смог понять, почему это происходит.