Sử dụng đúng beginBackgroundTaskWithExpirationHandler


107

Tôi hơi bối rối về cách sử dụng và khi nào beginBackgroundTaskWithExpirationHandler.

Apple cho thấy trong các ví dụ của họ để sử dụng nó trong applicationDidEnterBackgroundủy quyền, để có thêm thời gian hoàn thành một số nhiệm vụ quan trọng, thường là giao dịch mạng.

Khi nhìn vào ứng dụng của tôi, có vẻ như hầu hết các nội dung mạng của tôi đều quan trọng và khi một nội dung được bắt đầu, tôi muốn hoàn thành nó nếu người dùng nhấn nút trang chủ.

Vì vậy, có được chấp nhận / thực hành tốt để kết thúc mọi giao dịch mạng (và tôi không nói về việc tải xuống một lượng lớn dữ liệu, chủ yếu là một số xml ngắn) beginBackgroundTaskWithExpirationHandlerđể được an toàn?


Cũng xem ở đây
Honey

Câu trả lời:


165

Nếu bạn muốn giao dịch mạng của mình tiếp tục ở chế độ nền, thì bạn sẽ cần kết thúc nó trong một tác vụ nền. Điều rất quan trọng là bạn phải gọi endBackgroundTaskkhi bạn hoàn thành - nếu không ứng dụng sẽ bị giết sau khi thời gian quy định của nó đã hết.

Xu hướng của tôi trông giống như thế này:

- (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;
}

Tôi có một thuộc UIBackgroundTaskIdentifiertính cho mỗi nhiệm vụ nền


Mã tương đương trong 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)
}

1
Có, tôi làm ... nếu không thì họ sẽ dừng lại khi ứng dụng vào nền.
Ashley Mills

1
chúng ta có cần làm gì trong applicationDidEnterBackground không?
giảm

1
Chỉ khi bạn muốn sử dụng nó như một điểm để bắt đầu hoạt động mạng. Nếu bạn chỉ muốn một hoạt động hiện có để hoàn thành, theo @ câu hỏi Eyal, bạn không cần phải làm bất cứ điều gì trong applicationDidEnterBackground
Ashley Mills

2
Cảm ơn vì ví dụ rõ ràng này! (Vừa thay đổi đang làBackgroundUpdateTask thành beginBackgroundUpdateTask.)
newenglander

30
Nếu bạn gọi doUpdate nhiều lần liên tiếp mà công việc không được hoàn thành, bạn sẽ ghi đè lên self.backgroundUpdateTask để các tác vụ trước đó không thể kết thúc đúng cách. Bạn nên lưu trữ mã định danh nhiệm vụ mỗi lần để bạn kết thúc nó đúng cách hoặc sử dụng bộ đếm trong phương thức bắt đầu / kết thúc.
thejaz

23

Câu trả lời được chấp nhận là rất hữu ích và sẽ ổn trong hầu hết các trường hợp, tuy nhiên có hai điều khiến tôi bận tâm về nó:

  1. Như một số người đã lưu ý, việc lưu trữ mã định danh tác vụ dưới dạng thuộc tính có nghĩa là nó có thể bị ghi đè nếu phương thức được gọi nhiều lần, dẫn đến tác vụ sẽ không bao giờ kết thúc một cách duyên dáng cho đến khi hệ điều hành buộc phải kết thúc vào thời điểm hết hạn. .

  2. Mẫu này yêu cầu một thuộc tính duy nhất cho mọi cuộc gọi đến beginBackgroundTaskWithExpirationHandler, điều này có vẻ cồng kềnh nếu bạn có một ứng dụng lớn hơn với nhiều phương thức mạng.

Để giải quyết những vấn đề này, tôi đã viết một singleton xử lý tất cả các hệ thống ống nước và theo dõi các tác vụ đang hoạt động trong một từ điển. Không cần thuộc tính để theo dõi các định danh nhiệm vụ. Có vẻ hoạt động tốt. Cách sử dụng được đơn giản hóa thành:

//start the task
NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTask];

//do stuff

//end the task
[[BackgroundTaskManager sharedTasks] endTaskWithKey:taskKey];

Theo tùy chọn, nếu bạn muốn cung cấp một khối hoàn thành thực hiện điều gì đó ngoài việc kết thúc nhiệm vụ (được tích hợp sẵn), bạn có thể gọi:

NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTaskWithCompletionHandler:^{
    //do stuff
}];

Mã nguồn có liên quan có sẵn bên dưới (loại trừ nội dung singleton cho ngắn gọn). Nhận xét / phản hồi hoan nghênh.

- (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]];

        }

    }
}

1
thực sự thích giải pháp này. Tuy nhiên, một câu hỏi: bạn đã typedefhoàn thành CompletionBlock như thế nào? Đơn giản thế này:typedef void (^CompletionBlock)();
Joseph

Bạn đã hiểu. typedef void (^ CompletionBlock) (void);
Joel

@joel, cảm ơn nhưng đâu là liên kết của mã nguồn cho việc triển khai này, tôi, e, BackGroundTaskManager?
Özgür

Như đã lưu ý ở trên "nội dung singleton bị loại trừ cho ngắn gọn". [BackgroundTaskManager sharedTasks] trả về một singleton. Ruột của singleton được cung cấp ở trên.
Joel

Đã ủng hộ cho việc sử dụng một singleton. Tôi thực sự không nghĩ rằng họ xấu như mọi người nhận ra!
Craig Watkinson

20

Đây là một lớp Swift đóng gói việc chạy một tác vụ nền:

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
    }
}

Cách đơn giản nhất để sử dụng nó:

BackgroundTask.run(application) { backgroundTask in
   // Do something
   backgroundTask.end()
}

Nếu bạn cần đợi lệnh gọi lại của người được ủy quyền trước khi kết thúc, hãy sử dụng một cái gì đó như sau:

class MyClass {
    backgroundTask: BackgroundTask?

    func doSomething() {
        backgroundTask = BackgroundTask(application)
        backgroundTask!.begin()
        // Do something that waits for callback
    }

    func callback() {
        backgroundTask?.end()
        backgroundTask = nil
    } 
}

Vấn đề tương tự như trong câu trả lời được chấp nhận. Trình xử lý hết hạn không hủy tác vụ thực mà chỉ đánh dấu nó là đã kết thúc. Nhiều nguyên nhân hơn là do đóng gói mà chúng ta không thể tự làm điều đó. Đó là lý do tại sao Apple để lộ trình xử lý này, vì vậy việc đóng gói là sai ở đây.
Ariel Bogdziewicz

@ArielBogdziewicz Đúng là câu trả lời này không tạo cơ hội cho việc dọn dẹp bổ sung trong beginphương thức, nhưng có thể dễ dàng xem cách thêm tính năng đó.
matt

6

Như đã lưu ý ở đây và trong câu trả lời cho các câu hỏi SO khác, bạn KHÔNG muốn beginBackgroundTaskchỉ sử dụng khi ứng dụng của bạn chuyển sang chế độ nền; ngược lại, bạn nên sử dụng một nhiệm vụ nền cho bất kỳ hoạt động tốn nhiều thời gian mà kết thúc bạn muốn đảm bảo ngay cả khi ứng dụng không đi vào nền.

Do đó, mã của bạn có khả năng kết thúc với sự lặp lại của cùng một mã soạn sẵn để gọi beginBackgroundTaskendBackgroundTaskmạch lạc. Để ngăn chặn sự lặp lại này, chắc chắn là hợp lý khi bạn muốn đóng gói bảng soạn sẵn thành một thực thể được đóng gói duy nhất nào đó.

Tôi thích một số câu trả lời hiện có để làm điều đó, nhưng tôi nghĩ cách tốt nhất là sử dụng lớp con Hoạt động:

  • Bạn có thể xếp hàng đợi Hoạt động vào bất kỳ Hàng đợi Hoạt động nào và thao tác hàng đợi đó khi bạn thấy phù hợp. Ví dụ: bạn có thể tự do hủy sớm bất kỳ hoạt động nào hiện có trên hàng đợi.

  • Nếu bạn có nhiều việc phải làm, bạn có thể xâu chuỗi nhiều Hoạt động tác vụ nền. Hoạt động hỗ trợ phụ thuộc.

  • Hàng đợi Hoạt động có thể (và nên) là một hàng đợi nền; do đó, không cần phải lo lắng về việc thực hiện mã không đồng bộ bên trong tác vụ của bạn, bởi vì Hoạt động mã không đồng bộ. (Thật vậy, không có ý nghĩa gì khi thực thi một mức mã không đồng bộ khác bên trong một Thao tác, vì Thao tác sẽ kết thúc trước khi mã đó có thể bắt đầu. Nếu bạn cần làm điều đó, bạn sẽ sử dụng một Thao tác khác.)

Đây là một lớp con Hoạt động có thể có:

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?()
            self.cancel()
            UIApplication.shared.endBackgroundTask(bti) // cancellation
        }
        guard bti != .invalid else { return }
        whatToDo()
        guard !self.isCancelled else { return }
        UIApplication.shared.endBackgroundTask(bti) // completion
    }
}

Rõ ràng là làm thế nào để sử dụng điều này, nhưng trong trường hợp không, hãy tưởng tượng chúng ta có một OperationQueue toàn cầu:

let backgroundTaskQueue : OperationQueue = {
    let q = OperationQueue()
    q.maxConcurrentOperationCount = 1
    return q
}()

Vì vậy, đối với một loạt mã tốn thời gian điển hình, chúng tôi sẽ nói:

let task = BackgroundTaskOperation()
task.whatToDo = {
    // do something here
}
backgroundTaskQueue.addOperation(task)

Nếu lô mã tốn thời gian của bạn có thể được chia thành các giai đoạn, bạn có thể muốn bỏ cuộc sớm nếu nhiệm vụ của bạn bị hủy. Trong trường hợp đó, chỉ cần trả lại sớm sau khi đóng cửa. Lưu ý rằng tham chiếu của bạn đến nhiệm vụ từ bên trong quá trình đóng cần phải yếu hoặc bạn sẽ nhận được chu kỳ lưu giữ. Đây là một minh họa nhân tạo:

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)

Trong trường hợp bạn phải dọn dẹp trong trường hợp bản thân tác vụ nền bị hủy sớm, tôi đã cung cấp một thuộc tính cleanuptrình xử lý tùy chọn (không được sử dụng trong các ví dụ trước). Một số câu trả lời khác đã bị chỉ trích vì không bao gồm điều đó.


Hiện tôi đã cung cấp dự án này dưới dạng một dự án github: github.com/mattneub/BackgroundTaskOperation
matt

1

Tôi đã triển khai giải pháp của Joel. Đây là mã hoàn chỉnh:

.h tệp:

#import <Foundation/Foundation.h>

@interface VMKBackgroundTaskManager : NSObject

+ (id) sharedTasks;

- (NSUInteger)beginTask;
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
- (void)endTaskWithKey:(NSUInteger)_key;

@end

tệp .m:

#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

1
Cảm ơn vì điều đó. Mục tiêu-c của tôi không tuyệt. Bạn có thể thêm một số mã hướng dẫn cách sử dụng nó không?
pomo

bạn có thể xin vui lòng cho một ví dụ hoàn chỉnh về cách sử dụng mã ur
Amr Angry

Rất đẹp. Cảm ơn.
Alyoshak
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.