การใช้ startBackgroundTaskWithExpirationHandler อย่างเหมาะสม


107

beginBackgroundTaskWithExpirationHandlerฉันบิตสับสนเกี่ยวกับวิธีการและเมื่อใช้

Apple แสดงให้เห็นในตัวอย่างเพื่อใช้ในการapplicationDidEnterBackgroundมอบหมายเพื่อให้มีเวลามากขึ้นในการทำงานที่สำคัญบางอย่างโดยปกติจะเป็นธุรกรรมเครือข่าย

เมื่อดูในแอปของฉันดูเหมือนว่าสิ่งต่างๆในเครือข่ายส่วนใหญ่ของฉันมีความสำคัญและเมื่อมีการเริ่มต้นฉันต้องการดำเนินการให้เสร็จสิ้นหากผู้ใช้กดปุ่มโฮม

ดังนั้นจึงเป็นที่ยอมรับ / แนวทางปฏิบัติที่ดีในการรวมทุกธุรกรรมเครือข่าย (และฉันไม่ได้พูดถึงการดาวน์โหลดข้อมูลจำนวนมากส่วนใหญ่เป็น xml สั้น ๆ ) beginBackgroundTaskWithExpirationHandlerเพื่อให้ปลอดภัย


ยังเห็นที่นี่
น้ำผึ้ง

คำตอบ:


165

หากคุณต้องการให้ธุรกรรมเครือข่ายของคุณดำเนินการต่อในพื้นหลังคุณจะต้องรวมไว้ในงานเบื้องหลัง สิ่งสำคัญมากที่คุณต้องโทรหา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)
}

1
ใช่ฉันทำ ... มิฉะนั้นพวกเขาจะหยุดเมื่อแอปเข้าสู่พื้นหลัง
Ashley Mills

1
เราต้องทำอะไรใน applicationDidEnterBackground ไหม
จุ่ม

1
เฉพาะในกรณีที่คุณต้องการใช้เป็นจุดเริ่มต้นการทำงานของเครือข่าย หากคุณต้องการให้การดำเนินการที่มีอยู่แล้วเสร็จสมบูรณ์ตามคำถามของ @ Eyal คุณไม่จำเป็นต้องทำอะไรใน applicationDidEnterBackground
Ashley Mills

2
ขอบคุณสำหรับตัวอย่างที่ชัดเจนนี้! (เพิ่งเปลี่ยน BeingBackgroundUpdateTask เป็น beginBackgroundUpdateTask)
newenglander

30
หากคุณเรียกใช้ doUpdate หลายครั้งติดต่อกันโดยที่งานไม่เสร็จสิ้นคุณจะเขียนทับ self.backgroundUpdateTask เพื่อไม่ให้งานก่อนหน้านี้สิ้นสุดลงอย่างถูกต้อง คุณควรจัดเก็บตัวระบุงานทุกครั้งเพื่อให้คุณสิ้นสุดอย่างถูกต้องหรือใช้ตัวนับในวิธีการเริ่มต้น / สิ้นสุด
thejaz

23

คำตอบที่ได้รับการยอมรับนั้นมีประโยชน์มากและในกรณีส่วนใหญ่ควรจะใช้ได้ดี แต่มีสองสิ่งที่ทำให้ฉันรำคาญ:

  1. ตามที่หลายคนตั้งข้อสังเกตการจัดเก็บตัวระบุงานเป็นคุณสมบัติหมายความว่าสามารถเขียนทับได้หากมีการเรียกเมธอดหลายครั้งซึ่งจะนำไปสู่งานที่จะไม่มีวันจบลงอย่างสง่างามจนกว่าระบบปฏิบัติการจะบังคับให้สิ้นสุดในเวลาที่หมดอายุ .

  2. รูปแบบนี้ต้องการคุณสมบัติเฉพาะสำหรับการโทรทุกครั้ง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]];

        }

    }
}

1
ชอบวิธีนี้มาก คำถามหนึ่งข้อ: คุณทำtypedefสำเร็จอย่างไร/ ในฐานะอะไร? เพียงแค่นี้:typedef void (^CompletionBlock)();
โจเซฟ

คุณเข้าใจแล้ว typedef โมฆะ (^ CompletionBlock) (โมฆะ);
Joel

@joel ขอบคุณ แต่ลิงค์ของซอร์สโค้ดสำหรับการใช้งานนี้อยู่ที่ไหน i, e, BackGroundTaskManager
Özgür

ดังที่ระบุไว้ข้างต้น "สิ่งที่เป็นซิงเกิลตันยกเว้นสำหรับความกะทัดรัด" [BackgroundTaskManager sharedTasks] ส่งคืนซิงเกิลตัน ความกล้าของซิงเกิลตันมีไว้ด้านบน
Joel

ได้รับการโหวตให้ใช้ซิงเกิลตัน ฉันไม่คิดว่าพวกเขาจะเลวร้ายอย่างที่คนอื่นทำ!
Craig Watkinson

20

นี่คือคลาส 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
    } 
}

ปัญหาเดียวกันเช่นในคำตอบที่ยอมรับ ตัวจัดการการหมดอายุไม่ได้ยกเลิกงานจริง แต่จะทำเครื่องหมายว่าสิ้นสุดเท่านั้น มากกว่าการห่อหุ้มทำให้เราไม่สามารถทำสิ่งนั้นได้ด้วยตัวเอง นั่นเป็นเหตุผลที่ Apple เปิดเผยตัวจัดการนี้ดังนั้นการห่อหุ้มจึงผิดที่นี่
Ariel Bogdziewicz

@ArielBogdziewicz เป็นความจริงที่ว่าคำตอบนี้ไม่มีโอกาสสำหรับการล้างข้อมูลเพิ่มเติมในbeginวิธีการนี้ แต่สามารถดูวิธีเพิ่มคุณสมบัตินั้นได้อย่างง่ายดาย
แมต

6

ตามที่ระบุไว้ที่นี่และในคำตอบสำหรับคำถาม SO อื่น ๆ คุณไม่ต้องการใช้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?()
            self.cancel()
            UIApplication.shared.endBackgroundTask(bti) // cancellation
        }
        guard bti != .invalid else { return }
        whatToDo()
        guard !self.isCancelled else { return }
        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คุณสมบัติตัวจัดการที่เป็นทางเลือก(ไม่ได้ใช้ในตัวอย่างก่อนหน้านี้) คำตอบอื่น ๆ บางคำถูกวิพากษ์วิจารณ์ว่าไม่รวมถึงคำตอบนั้น


ตอนนี้ฉันได้จัดเตรียมสิ่งนี้เป็นโครงการ github แล้ว: github.com/mattneub/BackgroundTaskOperation
แมตต์

1

ฉันใช้โซลูชันของ Joel นี่คือรหัสที่สมบูรณ์:

ไฟล์. h:

#import <Foundation/Foundation.h>

@interface VMKBackgroundTaskManager : NSObject

+ (id) sharedTasks;

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

@end

ไฟล์. 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 ของฉันไม่ดี คุณช่วยเพิ่มรหัสที่แสดงวิธีการใช้งานได้ไหม
pomo

โปรดยกตัวอย่างที่สมบูรณ์เกี่ยวกับการใช้รหัสของคุณ
Amr Angry

ดีมาก. ขอบคุณ.
Alyoshak
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.