Правильное использование beginBackgroundTaskWithExpirationHandler
Я немного смущен о том, как и когда использовать beginBackgroundTaskWithExpirationHandler
.
Apple показывает в своих примерах использовать его в делегате applicationDidEnterBackground
, чтобы получить больше времени для выполнения важной задачи, обычно это сетевая транзакция.
При просмотре моего приложения кажется, что большая часть моего сетевого материала важна, и когда он запускается, я хотел бы заполнить его, если пользователь нажал кнопку "домой".
Так ли принято/хорошая практика обертывать каждую сетевую транзакцию (и я не говорю о загрузке большого фрагмента данных, в основном из небольшого xml) с beginBackgroundTaskWithExpirationHandler
, чтобы быть в безопасности?
Ответы
Ответ 1
Если вы хотите, чтобы ваша сетевая транзакция продолжалась в фоновом режиме, вам нужно будет обернуть ее в фоновую задачу. Также очень важно, чтобы вы endBackgroundTask
когда закончите, иначе приложение будет убито по истечении выделенного времени.
Мои склонны выглядеть примерно так:
- (void) doUpdate
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self beginBackgroundUpdateTask];
NSURLResponse * response = nil;
NSError * error = nil;
NSData * responseData = [NSURLConnection sendSynchronousRequest: request returningResponse: &response error: &error];
// Do something with the result
[self endBackgroundUpdateTask];
});
}
- (void) beginBackgroundUpdateTask
{
self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundUpdateTask];
}];
}
- (void) endBackgroundUpdateTask
{
[[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask];
self.backgroundUpdateTask = UIBackgroundTaskInvalid;
}
У меня есть свойство UIBackgroundTaskIdentifier
для каждой фоновой задачи
Эквивалентный код в Swift
func doUpdate () {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let taskID = beginBackgroundUpdateTask()
var response: URLResponse?, error: NSError?, request: NSURLRequest?
let data = NSURLConnection.sendSynchronousRequest(request, returningResponse: &response, error: &error)
// Do something with the result
endBackgroundUpdateTask(taskID)
})
}
func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
return UIApplication.shared.beginBackgroundTask(expirationHandler: ({}))
}
func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.shared.endBackgroundTask(taskID)
}
Ответ 2
Принятый ответ очень полезен, и в большинстве случаев это должно быть хорошо, однако мне все это беспокоило меня:
-
Как заметил ряд людей, сохранение идентификатора задачи в качестве свойства означает, что его можно перезаписать, если метод вызывается несколько раз, что приводит к задаче, которая никогда не будет законно закончена, пока не будет принудительно завершена OS в момент истечения срока действия.
-
Этот шаблон требует уникального свойства для каждого вызова beginBackgroundTaskWithExpirationHandler
, который кажется громоздким, если у вас есть более крупное приложение с множеством сетевых методов.
Чтобы решить эти проблемы, я написал синглтон, который заботится обо всех сантехнических и отслеживает активные задачи в словаре. Нет свойств, необходимых для отслеживания идентификаторов задач. Кажется, хорошо работает. Использование упрощено:
//start the task
NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTask];
//do stuff
//end the task
[[BackgroundTaskManager sharedTasks] endTaskWithKey:taskKey];
Необязательно, если вы хотите предоставить блок завершения, который делает что-то помимо завершения задачи (которая встроена), вы можете вызвать:
NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTaskWithCompletionHandler:^{
//do stuff
}];
Соответствующий исходный код, доступный ниже (одноточие исключено для краткости). Комментарии/отзывы приветствуются.
- (id)init
{
self = [super init];
if (self) {
[self setTaskKeyCounter:0];
[self setDictTaskIdentifiers:[NSMutableDictionary dictionary]];
[self setDictTaskCompletionBlocks:[NSMutableDictionary dictionary]];
}
return self;
}
- (NSUInteger)beginTask
{
return [self beginTaskWithCompletionHandler:nil];
}
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
{
//read the counter and increment it
NSUInteger taskKey;
@synchronized(self) {
taskKey = self.taskKeyCounter;
self.taskKeyCounter++;
}
//tell the OS to start a task that should continue in the background if needed
NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endTaskWithKey:taskKey];
}];
//add this task identifier to the active task dictionary
[self.dictTaskIdentifiers setObject:[NSNumber numberWithUnsignedLong:taskId] forKey:[NSNumber numberWithUnsignedLong:taskKey]];
//store the completion block (if any)
if (_completion) [self.dictTaskCompletionBlocks setObject:_completion forKey:[NSNumber numberWithUnsignedLong:taskKey]];
//return the dictionary key
return taskKey;
}
- (void)endTaskWithKey:(NSUInteger)_key
{
@synchronized(self.dictTaskCompletionBlocks) {
//see if this task has a completion block
CompletionBlock completion = [self.dictTaskCompletionBlocks objectForKey:[NSNumber numberWithUnsignedLong:_key]];
if (completion) {
//run the completion block and remove it from the completion block dictionary
completion();
[self.dictTaskCompletionBlocks removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];
}
}
@synchronized(self.dictTaskIdentifiers) {
//see if this task has been ended yet
NSNumber *taskId = [self.dictTaskIdentifiers objectForKey:[NSNumber numberWithUnsignedLong:_key]];
if (taskId) {
//end the task and remove it from the active task dictionary
[[UIApplication sharedApplication] endBackgroundTask:[taskId unsignedLongValue]];
[self.dictTaskIdentifiers removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];
}
}
}
Ответ 3
Вот класс Swift, который инкапсулирует выполнение фоновой задачи:
class BackgroundTask {
private let application: UIApplication
private var identifier = UIBackgroundTaskInvalid
init(application: UIApplication) {
self.application = application
}
class func run(application: UIApplication, handler: (BackgroundTask) -> ()) {
// NOTE: The handler must call end() when it is done
let backgroundTask = BackgroundTask(application: application)
backgroundTask.begin()
handler(backgroundTask)
}
func begin() {
self.identifier = application.beginBackgroundTaskWithExpirationHandler {
self.end()
}
}
func end() {
if (identifier != UIBackgroundTaskInvalid) {
application.endBackgroundTask(identifier)
}
identifier = UIBackgroundTaskInvalid
}
}
Самый простой способ его использования:
BackgroundTask.run(application) { backgroundTask in
// Do something
backgroundTask.end()
}
Если вам нужно дождаться обратного вызова делегата до его окончания, используйте следующее:
class MyClass {
backgroundTask: BackgroundTask?
func doSomething() {
backgroundTask = BackgroundTask(application)
backgroundTask!.begin()
// Do something that waits for callback
}
func callback() {
backgroundTask?.end()
backgroundTask = nil
}
}
Ответ 4
Я реализовал решение Joel. Вот полный код:
.h файл:
#import <Foundation/Foundation.h>
@interface VMKBackgroundTaskManager : NSObject
+ (id) sharedTasks;
- (NSUInteger)beginTask;
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
- (void)endTaskWithKey:(NSUInteger)_key;
@end
.m file:
#import "VMKBackgroundTaskManager.h"
@interface VMKBackgroundTaskManager()
@property NSUInteger taskKeyCounter;
@property NSMutableDictionary *dictTaskIdentifiers;
@property NSMutableDictionary *dictTaskCompletionBlocks;
@end
@implementation VMKBackgroundTaskManager
+ (id)sharedTasks {
static VMKBackgroundTaskManager *sharedTasks = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedTasks = [[self alloc] init];
});
return sharedTasks;
}
- (id)init
{
self = [super init];
if (self) {
[self setTaskKeyCounter:0];
[self setDictTaskIdentifiers:[NSMutableDictionary dictionary]];
[self setDictTaskCompletionBlocks:[NSMutableDictionary dictionary]];
}
return self;
}
- (NSUInteger)beginTask
{
return [self beginTaskWithCompletionHandler:nil];
}
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
{
//read the counter and increment it
NSUInteger taskKey;
@synchronized(self) {
taskKey = self.taskKeyCounter;
self.taskKeyCounter++;
}
//tell the OS to start a task that should continue in the background if needed
NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endTaskWithKey:taskKey];
}];
//add this task identifier to the active task dictionary
[self.dictTaskIdentifiers setObject:[NSNumber numberWithUnsignedLong:taskId] forKey:[NSNumber numberWithUnsignedLong:taskKey]];
//store the completion block (if any)
if (_completion) [self.dictTaskCompletionBlocks setObject:_completion forKey:[NSNumber numberWithUnsignedLong:taskKey]];
//return the dictionary key
return taskKey;
}
- (void)endTaskWithKey:(NSUInteger)_key
{
@synchronized(self.dictTaskCompletionBlocks) {
//see if this task has a completion block
CompletionBlock completion = [self.dictTaskCompletionBlocks objectForKey:[NSNumber numberWithUnsignedLong:_key]];
if (completion) {
//run the completion block and remove it from the completion block dictionary
completion();
[self.dictTaskCompletionBlocks removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];
}
}
@synchronized(self.dictTaskIdentifiers) {
//see if this task has been ended yet
NSNumber *taskId = [self.dictTaskIdentifiers objectForKey:[NSNumber numberWithUnsignedLong:_key]];
if (taskId) {
//end the task and remove it from the active task dictionary
[[UIApplication sharedApplication] endBackgroundTask:[taskId unsignedLongValue]];
[self.dictTaskIdentifiers removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];
NSLog(@"Task ended");
}
}
}
@end
Ответ 5
Как отмечено здесь и в ответах на другие вопросы SO, вы НЕ хотите использовать beginBackgroundTask
только тогда, когда ваше приложение перейдет в фоновый режим; напротив, вы должны использовать фоновую задачу для любой трудоемкой операции, завершение которой вы хотите гарантировать, даже если приложение уходит в фоновый режим.
Поэтому ваш код, вероятно, будет в конечном итоге изобиловать повторениями одного и того же стандартного кода для beginBackgroundTask
вызова beginBackgroundTask
и endBackgroundTask
. Чтобы предотвратить это повторение, безусловно, разумно хотеть упаковать шаблон в какой-то один инкапсулированный объект.
Мне нравятся некоторые из существующих ответов для этого, но я думаю, что лучший способ - это использовать подкласс Operation:
-
Вы можете ставить Операцию в очередь в любой OperationQueue и манипулировать этой очередью по своему усмотрению. Например, вы можете досрочно отменить любые существующие операции в очереди.
-
Если вам нужно выполнить несколько задач, вы можете объединить несколько операций фоновой задачи. Операции поддерживают зависимости.
-
Очередь операций может (и должна) быть фоновой очередью; таким образом, вам не нужно беспокоиться о выполнении асинхронного кода внутри вашей задачи, потому что Operation - это асинхронный код. (Действительно, нет смысла выполнять другой уровень асинхронного кода внутри Операции, так как Операция завершится еще до того, как этот код может даже запуститься. Если бы вам нужно было сделать это, вы бы использовали другую Операцию.)
Здесь возможный Операционный подкласс:
class BackgroundTaskOperation: Operation {
var whatToDo : (() -> ())?
var cleanup : (() -> ())?
override func main() {
guard !self.isCancelled else { return }
guard let whatToDo = self.whatToDo else { return }
var bti : UIBackgroundTaskIdentifier = .invalid
bti = UIApplication.shared.beginBackgroundTask {
self.cleanup?()
UIApplication.shared.endBackgroundTask(bti) // cancellation
}
guard bti != .invalid else { return }
whatToDo()
UIApplication.shared.endBackgroundTask(bti) // completion
}
}
Должно быть очевидно, как это использовать, но если это не так, представьте, что у нас есть глобальная OperationQueue:
let backgroundTaskQueue : OperationQueue = {
let q = OperationQueue()
q.maxConcurrentOperationCount = 1
return q
}()
Таким образом, для типичной длительной партии кода мы бы сказали:
let task = BackgroundTaskOperation()
task.whatToDo = {
// do something here
}
backgroundTaskQueue.addOperation(task)
Если ваш трудоемкий пакет кода можно разделить на этапы, вы можете пораньше уйти в отставку, если ваша задача будет отменена. В этом случае просто преждевременно вернитесь из закрытия. Обратите внимание, что ваша ссылка на задачу из замыкания должна быть слабой, иначе вы получите цикл сохранения. Вот искусственная иллюстрация:
let task = BackgroundTaskOperation()
task.whatToDo = { [weak task] in
guard let task = task else {return}
for i in 1...10000 {
guard !task.isCancelled else {return}
for j in 1...150000 {
let k = i*j
}
}
}
backgroundTaskQueue.addOperation(task)
Если вам нужно выполнить очистку, если сама фоновая задача отменена преждевременно, я предоставил необязательное свойство обработчика cleanup
(не использовалось в предыдущих примерах). Некоторые другие ответы были раскритикованы за то, что они не были включены.
Ответ 6
-
(void) doUpdate { dispatch_async (dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {
[self beginBackgroundUpdateTask];
NSURLResponse * response = nil;
NSError * error = nil;
NSData * responseData = [NSURLConnection sendSynchronousRequest: request returningResponse: &response error:
& ошибка];
// Do something with the result
[self endBackgroundUpdateTask];
}); }
-
(void) beginBackgroundUpdateTask { self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler: ^ { [self endBackgroundUpdateTask]; }]; }
-
(void) endBackgroundUpdateTask { [[UAppication sharedApplication] endBackgroundTask: self.backgroundUpdateTask]; self.backgroundUpdateTask = UIBackgroundTaskInvalid; }
Спасибо Эшли Миллс, он отлично работает для меня
Ответ 7
Сначала прочтите документацию: https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio
Фоновая задача должна соответствовать следующим требованиям:
- Фоновое задание должно быть сообщено как можно скорее, но это не должно быть до начала нашего реального задания. Метод
beginBackgroundTaskWithExpirationHandler:
работает асинхронно, поэтому, если он вызывается в конце applicationDidEnterBackground:
он не регистрирует фоновую задачу и немедленно вызывает обработчик истечения. - Обработчик срока действия должен отменить нашу реальную задачу и пометить фоновую задачу как завершенную. Это заставляет нас хранить идентификатор фоновой задачи, хранящийся где-то, например, как атрибут некоторого класса. Это свойство должно находиться под нашим контролем, чтобы его нельзя было перезаписать.
- Обработчик истечения выполняется из основного потока, поэтому ваша реальная задача должна быть поточно-безопасной, если вы хотите отменить ее там.
- Наша настоящая задача должна быть отменена. Это означает, что наша реальная задача должна иметь метод
cancel
. В противном случае есть риск, что он будет прерван непредсказуемым образом, даже если мы отметим фоновую задачу как завершенную. - Код, содержащий
beginBackgroundTaskWithExpirationHandler:
может вызываться везде и в любом потоке. Это не должен быть метод делегата applicationDidEnterBackground:
- Нет смысла делать это для синхронных операций короче 5 секунд в случае использования метода
applicationDidEnterBackground:
(пожалуйста, прочтите документ https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackground?language=objc) - Метод
applicationDidEnterBackground
должен быть выполнен за время менее 5 секунд, поэтому все фоновые задачи должны быть запущены во втором потоке.
Пример:
class MySpecificBackgroundTask: NSObject, URLSessionDataDelegate {
// MARK: - Properties
let application: UIApplication
var backgroundTaskIdentifier: UIBackgroundTaskIdentifier
var task: URLSessionDataTask? = nil
// MARK: - Initializers
init(application: UIApplication) {
self.application = application
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid
}
// MARK: - Actions
func start() {
self.backgroundTaskIdentifier = self.application.beginBackgroundTask {
self.cancel()
}
self.startUrlRequest()
}
func cancel() {
self.task?.cancel()
self.end()
}
private func end() {
self.application.endBackgroundTask(self.backgroundTaskIdentifier)
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid
}
// MARK: - URLSession methods
private func startUrlRequest() {
let sessionConfig = URLSessionConfiguration.background(withIdentifier: "MySpecificBackgroundTaskId")
let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
guard let url = URL(string: "https://example.com/api/my/path") else {
self.end()
return
}
let request = URLRequest(url: url)
self.task = session.dataTask(with: request)
self.task?.resume()
}
// MARK: - URLSessionDataDelegate methods
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
self.end()
}
// Implement other methods of URLSessionDataDelegate to handle response...
}
Может использоваться в нашем делегате приложения:
func applicationDidEnterBackground(_ application: UIApplication) {
let myBackgroundTask = MySpecificBackgroundTask(application: application)
myBackgroundTask.start()
}