Penggunaan beginBackgroundTaskWithExpirationHandler dengan benar

107

Saya agak bingung bagaimana dan kapan harus menggunakannya beginBackgroundTaskWithExpirationHandler.

Apple menunjukkan dalam contoh mereka untuk menggunakannya dalam applicationDidEnterBackgrounddelegasi, untuk mendapatkan lebih banyak waktu untuk menyelesaikan beberapa tugas penting, biasanya transaksi jaringan.

Saat melihat aplikasi saya, sepertinya sebagian besar jaringan saya penting, dan saat dimulai, saya ingin menyelesaikannya jika pengguna menekan tombol beranda.

Jadi apakah diterima / praktik yang baik untuk membungkus setiap transaksi jaringan (dan saya tidak berbicara tentang mengunduh sebagian besar data, sebagian besar beberapa xml pendek) dengan beginBackgroundTaskWithExpirationHandleraman?

Eyal
sumber
Juga lihat di sini
Madu

Jawaban:

165

Jika Anda ingin transaksi jaringan Anda berlanjut di latar belakang, Anda harus membungkusnya di tugas latar belakang. Sangat penting juga bagi Anda untuk menelepon endBackgroundTasksetelah selesai - jika tidak, aplikasi akan mati setelah waktu yang ditentukan telah kedaluwarsa.

Punyaku cenderung terlihat seperti ini:

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

Saya memiliki UIBackgroundTaskIdentifierproperti untuk setiap tugas latar belakang


Kode yang setara di 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)
}
Ashley Mills
sumber
1
Ya, saya lakukan ... jika tidak, mereka berhenti saat aplikasi memasuki latar belakang.
Ashley Mills
1
apakah kita perlu melakukan sesuatu di applicationDidEnterBackground?
turun
1
Hanya jika Anda ingin menggunakannya sebagai titik untuk memulai operasi jaringan. Jika Anda hanya ingin operasi yang ada selesai, sesuai pertanyaan @ Eyal, Anda tidak perlu melakukan apa pun di applicationDidEnterBackground
Ashley Mills
2
Terima kasih atas contoh yang jelas ini! (Baru saja berubah menjadiBackgroundUpdateTask menjadi beginBackgroundUpdateTask.)
newenglander
30
Jika Anda memanggil doUpdate beberapa kali berturut-turut tanpa pekerjaan selesai, Anda akan menimpa self.backgroundUpdateTask sehingga tugas sebelumnya tidak dapat diakhiri dengan benar. Anda harus menyimpan pengenal tugas setiap kali sehingga Anda mengakhirinya dengan benar atau menggunakan penghitung dalam metode mulai / akhir.
thejaz
23

Jawaban yang diterima sangat membantu dan seharusnya baik-baik saja dalam banyak kasus, namun ada dua hal yang mengganggu saya tentang hal itu:

  1. Seperti yang telah dicatat oleh sejumlah orang, menyimpan pengenal tugas sebagai properti berarti dapat ditimpa jika metode dipanggil beberapa kali, yang mengarah ke tugas yang tidak akan pernah berakhir dengan baik hingga dipaksa untuk diakhiri oleh OS pada saat kedaluwarsa .

  2. Pola ini memerlukan properti unik untuk setiap panggilan beginBackgroundTaskWithExpirationHandleryang tampaknya rumit jika Anda memiliki aplikasi yang lebih besar dengan banyak metode jaringan.

Untuk mengatasi masalah ini, saya menulis sebuah singleton yang menangani semua pipa ledeng dan melacak tugas aktif dalam kamus. Tidak ada properti yang diperlukan untuk melacak pengidentifikasi tugas. Sepertinya bekerja dengan baik. Penggunaan disederhanakan menjadi:

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

//do stuff

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

Secara opsional, jika Anda ingin menyediakan blok penyelesaian yang melakukan sesuatu selain mengakhiri tugas (yang sudah ada di dalamnya), Anda dapat memanggil:

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

Kode sumber yang relevan tersedia di bawah ini (hal-hal tunggal dikecualikan agar singkatnya). Komentar / umpan balik diterima.

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

        }

    }
}
Joel
sumber
1
sangat menyukai solusi ini. satu pertanyaan: bagaimana / seperti apa Anda typedefCompletionBlock? Sederhananya ini:typedef void (^CompletionBlock)();
Joseph
Anda mengerti. typedef void (^ CompletionBlock) (void);
Joel
@joel, terima kasih, tapi di mana link kode sumber untuk implementasi ini, i, e, BackGroundTaskManager?
Özgür
Seperti disebutkan di atas "hal-hal tunggal dikecualikan agar singkat". [BackgroundTaskManager sharedTasks] mengembalikan singleton. Nyali singleton disediakan di atas.
Yoel
Suara positif untuk menggunakan singleton. Saya benar-benar tidak berpikir mereka seburuk yang dilihat orang!
Craig Watkinson
20

Berikut adalah kelas Swift yang merangkum menjalankan tugas latar belakang:

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

Cara termudah untuk menggunakannya:

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

Jika Anda perlu menunggu callback delegasi sebelum Anda mengakhiri, gunakan sesuatu seperti ini:

class MyClass {
    backgroundTask: BackgroundTask?

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

    func callback() {
        backgroundTask?.end()
        backgroundTask = nil
    } 
}
phatmann.dll
sumber
Masalah yang sama seperti dalam jawaban yang diterima. Penangan kedaluwarsa tidak membatalkan tugas sebenarnya, tetapi hanya menandainya sebagai berakhir. Terlebih lagi enkapsulasi menyebabkan kita tidak dapat melakukannya sendiri. Itu sebabnya Apple mengekspos handler ini, jadi enkapsulasi salah di sini.
Ariel Bogdziewicz
@ArielBogdziewicz Memang benar bahwa jawaban ini tidak memberikan kesempatan untuk pembersihan tambahan dalam beginmetode ini, tetapi mudah untuk melihat bagaimana menambahkan fitur itu.
matt
6

Seperti disebutkan di sini dan dalam jawaban atas pertanyaan SO lainnya, Anda TIDAK ingin menggunakan beginBackgroundTaskhanya saat aplikasi Anda akan masuk ke latar belakang; sebaliknya, Anda harus menggunakan tugas latar belakang untuk setiap operasi memakan waktu yang selesai Anda ingin memastikan bahkan jika aplikasi tidak masuk ke latar belakang.

Oleh karena itu, kode Anda kemungkinan besar akan dibumbui dengan pengulangan kode boilerplate yang sama untuk memanggil beginBackgroundTaskdan secara endBackgroundTaskkoheren. Untuk mencegah pengulangan ini, tentu masuk akal untuk ingin mengemas boilerplate menjadi beberapa entitas terenkapsulasi tunggal.

Saya suka beberapa jawaban yang ada untuk melakukan itu, tetapi menurut saya cara terbaik adalah dengan menggunakan subkelas Operasi:

  • Anda dapat memasukkan Operation ke dalam OperationQueue mana pun dan memanipulasi antrean itu sesuai keinginan Anda. Misalnya, Anda bebas membatalkan sebelum waktunya semua operasi yang ada di antrian.

  • Jika Anda memiliki lebih dari satu hal yang harus dilakukan, Anda dapat merangkai beberapa Operasi tugas latar belakang. Dependensi dukungan operasi.

  • Operation Queue dapat (dan seharusnya) menjadi antrian latar belakang; jadi, tidak perlu khawatir untuk melakukan kode asinkron di dalam tugas Anda, karena Operasi adalah kode asinkron. (Memang, tidak masuk akal untuk mengeksekusi level lain dari kode asinkron di dalam Operasi, karena Operasi akan selesai bahkan sebelum kode itu dapat dimulai. Jika Anda perlu melakukannya, Anda akan menggunakan Operasi lain.)

Berikut subclass Operation yang mungkin:

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

Seharusnya jelas bagaimana menggunakan ini, tetapi jika tidak, bayangkan kita memiliki OperationQueue global:

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

Jadi untuk kumpulan kode yang memakan waktu, kami akan mengatakan:

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

Jika kumpulan kode Anda yang memakan waktu dapat dibagi menjadi beberapa tahap, Anda mungkin ingin mundur lebih awal jika tugas Anda dibatalkan. Dalam hal ini, kembalilah sebelum waktunya dari penutupan. Perhatikan bahwa referensi Anda ke tugas dari dalam closure harus lemah atau Anda akan mendapatkan siklus retensi. Berikut ilustrasi buatannya:

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)

Jika Anda memiliki pembersihan yang harus dilakukan jika tugas latar belakang itu sendiri dibatalkan sebelum waktunya, saya telah menyediakan cleanupproperti penangan opsional (tidak digunakan dalam contoh sebelumnya). Beberapa jawaban lain dikritik karena tidak memasukkan itu.

Matt
sumber
Saya sekarang menyediakan ini sebagai proyek github: github.com/mattneub/BackgroundTaskOperation
matt
1

Saya menerapkan solusi Joel. Berikut kode lengkapnya:

file .h:

#import <Foundation/Foundation.h>

@interface VMKBackgroundTaskManager : NSObject

+ (id) sharedTasks;

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

@end

file .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
vomako
sumber
1
Terima kasih untuk ini. Tujuan-c saya tidak bagus. Bisakah Anda menambahkan beberapa kode yang menunjukkan cara menggunakannya?
pomo
bisakah Anda memberikan contoh lengkap tentang cara menggunakan kode ur
Amr Angry
Sangat bagus. Terima kasih.
Alyoshak