BeginBackgroundTaskWithExpirationHandler'ın doğru kullanımı


107

Nasıl ve ne zaman kullanacağım konusunda kafam biraz karıştı beginBackgroundTaskWithExpirationHandler .

Apple, örneklerinde bunu kullanmak için gösterir applicationDidEnterBackground , bazı önemli görevleri, genellikle bir ağ işlemini tamamlamak için daha fazla zaman kazanmak için temsilci .

Uygulamama baktığımda, ağ işlerimin çoğu önemli gibi görünüyor ve bir uygulama başlatıldığında, kullanıcı ana sayfa düğmesine basarsa onu tamamlamak istiyorum.

Öyleyse, beginBackgroundTaskWithExpirationHandlergüvenli tarafta olmak için her ağ işlemini paketlemek (ve büyük miktarda veri indirmekten bahsetmiyorum, çoğunlukla kısa xml) kabul edildi / iyi bir uygulama mı?


Ayrıca buraya
Honey

Yanıtlar:


165

Ağ işleminizin arka planda devam etmesini istiyorsanız, bunu bir arka plan görevine bağlamanız gerekir. endBackgroundTaskBitirdiğinizde aramanız da çok önemlidir - aksi takdirde uygulama, ayrılan süre dolduktan sonra öldürülür.

Benimki böyle bir şeye benziyor:

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

UIBackgroundTaskIdentifierHer arka plan görevi için bir özelliğim var


Swift'deki eşdeğer kod

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
Evet, yapıyorum ... aksi takdirde uygulama arka plana girdiğinde dururlar.
Ashley Mills

1
applicationDidEnterBackground'da herhangi bir şey yapmamız gerekiyor mu?
dips

1
Sadece bunu ağ işlemini başlatmak için bir nokta olarak kullanmak istiyorsanız . @ Eyal'ın sorusuna göre, sadece mevcut bir operasyonun tamamlanmasını istiyorsanız, applicationDidEnterBackground
Ashley Mills

2
Bu açık örnek için teşekkürler! (BackgroundUpdateTask olarak beginBackgroundUpdateTask olarak değiştirildi.)
newenglander

30
DoUpdate'i çalışma yapılmadan art arda birden çok kez çağırırsanız, self.backgroundUpdateTask'in üzerine yazarsınız, böylece önceki görevler düzgün şekilde sonlandırılamaz. Ya her seferinde görev tanımlayıcısını saklamalısınız, böylece onu düzgün bir şekilde bitirmelisiniz ya da başlangıç ​​/ bitiş yöntemlerinde bir sayaç kullanmalısınız.
thejaz

23

Kabul edilen cevap çok yardımcı oldu ve çoğu durumda iyi olmalı, ancak bu konuda beni iki şey rahatsız etti:

  1. Birkaç kişinin belirttiği gibi, görev tanımlayıcısını bir özellik olarak saklamak, yöntem birden çok kez çağrılırsa üzerine yazılabileceği anlamına gelir ve bu da, zaman sona erdiğinde işletim sistemi tarafından sona erdirilmeye zorlanana kadar asla zarif bir şekilde sonlandırılmayacak bir göreve yol açar. .

  2. Bu model, beginBackgroundTaskWithExpirationHandlerçok sayıda ağ yöntemine sahip daha büyük bir uygulamanız varsa kullanışsız görünen her çağrı için benzersiz bir özellik gerektirir .

Bu sorunları çözmek için, tüm su tesisatı ile ilgilenen ve aktif görevleri bir sözlükte izleyen bir tekil yazdım. Görev tanımlayıcılarını takip etmek için hiçbir özelliğe gerek yoktur. İyi çalışıyor gibi görünüyor. Kullanım şu şekilde basitleştirilmiştir:

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

//do stuff

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

İsteğe bağlı olarak, (yerleşik olan) görevi sonlandırmanın ötesinde bir şey yapan bir tamamlama bloğu sağlamak istiyorsanız şunları arayabilirsiniz:

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

Aşağıda ilgili kaynak kodu mevcuttur (kısalık olması için tekil şeyler hariçtir). Yorumlar / geri bildirim hoş geldiniz.

- (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
bu çözümü gerçekten beğendim. Yine de bir soru: typedefCompletionBlock nasıl / ne yaptınız ? Basitçe bu:typedef void (^CompletionBlock)();
Joseph

Anladın. typedef void (^ CompletionBlock) (void);
Joel

@joel, teşekkürler ama bu uygulama için kaynak kodunun bağlantısı nerede, i, e, BackGroundTaskManager?
Özgür

Yukarıda belirtildiği gibi "tekil şeyler kısalık için hariç tutulmuştur". [BackgroundTaskManager sharedTasks] bir tekli döndürür. Singletonun bağırsakları yukarıda verilmiştir.
Joel

Tekli kullanım için oy verildi. İnsanların sandığı kadar kötü olduklarını gerçekten düşünmüyorum!
Craig Watkinson

20

İşte bir arka plan görevinin çalıştırılmasını özetleyen bir Swift sınıfı :

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

Kullanmanın en basit yolu:

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

Bitirmeden önce temsilcinin geri aranmasını beklemeniz gerekiyorsa, şuna benzer bir şey kullanın:

class MyClass {
    backgroundTask: BackgroundTask?

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

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

Kabul edilen cevaptaki gibi aynı sorun. Süre sonu işleyicisi gerçek görevi iptal etmez, yalnızca onu bitti olarak işaretler. Daha fazla kapsülleme, bunu kendimiz yapamamamıza neden olur. Bu yüzden Apple bu işleyiciyi açığa çıkardı, bu yüzden burada kapsülleme yanlış.
Ariel Bogdziewicz

@ArielBogdziewicz Bu cevabın beginyöntemde ek temizleme fırsatı sağlamadığı doğrudur , ancak bu özelliğin nasıl ekleneceğini görmek kolaydır.
mat

6

Burada ve diğer SO sorularının yanıtlarında belirtildiği gibi, beginBackgroundTaskyalnızca uygulamanız arka plana geçeceği zaman kullanmak İSTEMEZSİNİZ; Tam tersine, bir arka plan görevi kullanmalısınız herhangi kimin tamamlama varsa uygulamanın bile sağlamak istiyoruz zaman alıcı operasyonu yapar arka gider.

Bu nedenle, kodunuzun arama beginBackgroundTaskve arama için aynı standart kodun tekrarları ile sonuçlanması muhtemeldir.endBackgroundTask tutarlı bir . Bu tekrarı önlemek için, ortak plakayı tek bir kapsüllenmiş varlığa paketlemek kesinlikle mantıklıdır.

Bunu yapmak için bazı mevcut cevapları seviyorum, ancak en iyi yolun bir Operasyon alt sınıfı kullanmak olduğunu düşünüyorum:

  • İşlemi herhangi bir OperationQueue üzerine sıraya koyabilir ve bu kuyruğu uygun gördüğünüz şekilde değiştirebilirsiniz. Örneğin, kuyruktaki mevcut işlemleri zamanından önce iptal etmekte özgürsünüz.

  • Yapacak birden fazla işiniz varsa, birden fazla arka plan görevi İşlemleri zincirleyebilirsiniz. İşlemler bağımlılıkları destekler.

  • İşlem Sırası bir arka plan sırası olabilir (ve olmalıdır); Operasyon nedeniyle böylece, görev içinde asenkron kod gerçekleştirme hakkında endişelenmeye gerek yoktur olan asenkron kodu. (Gerçekten de, bir İşlem içinde başka bir eşzamansız kod düzeyi yürütmenin bir anlamı yoktur , çünkü İşlem bu kod başlamadan önce biter. Bunu yapmanız gerekirse, başka bir İşlem kullanırsınız.)

İşte olası bir İşlem alt sınıfı:

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

Bunun nasıl kullanılacağı açık olmalı, ancak değilse, küresel bir OperationQueue'muz olduğunu hayal edin:

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

Dolayısıyla, zaman alan tipik bir kod grubu için şunu söyleyebiliriz:

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

Zaman alan kod grubunuz aşamalara bölünebiliyorsa, göreviniz iptal edilirse erkenden vazgeçmek isteyebilirsiniz. Bu durumda, kapanıştan erken dönün. Kapanış içinde göreve referansınızın zayıf olması gerektiğini, aksi takdirde bir tutma döngüsü elde edeceğinizi unutmayın. İşte yapay bir örnek:

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)

Arka plan görevinin zamanından önce iptal edilmesi durumunda yapmanız gereken temizlemeniz olması durumunda, isteğe bağlı bir cleanupişleyici özelliği sağladım (önceki örneklerde kullanılmamıştır). Diğer bazı cevaplar bunu dahil etmediği için eleştirildi.


Şimdi bunu bir github projesi olarak sağladım
matt

1

Joel'in çözümünü uyguladım. İşte tam kod:

.h dosyası:

#import <Foundation/Foundation.h>

@interface VMKBackgroundTaskManager : NSObject

+ (id) sharedTasks;

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

@end

.m dosyası:

#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
Bunun için teşekkürler. Benim hedef-c harika değil. Nasıl kullanılacağını gösteren bir kod ekleyebilir misiniz?
pomo

ur kodunun nasıl kullanılacağına dair eksiksiz bir örnek verebilir misiniz?
Amr Angry

Çok hoş. Teşekkürler.
Alyoshak
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.