beginBackgroundTaskWithExpirationHandler의 적절한 사용


107

사용 방법과시기에 대해 약간 혼란 스럽습니다 beginBackgroundTaskWithExpirationHandler.

Apple은 예제에서 applicationDidEnterBackground대리자 에서 사용하고 일반적으로 네트워크 트랜잭션과 같은 중요한 작업을 완료하는 데 더 많은 시간을 확보 하는 것을 보여줍니다 .

내 앱을 볼 때 대부분의 네트워크 항목이 중요한 것처럼 보이며 하나가 시작될 때 사용자가 홈 버튼을 눌렀을 때 완료하고 싶습니다.

따라서 모든 네트워크 트랜잭션을 포장하는 것이 허용 / 좋은 관행 beginBackgroundTaskWithExpirationHandler입니까 (그리고 큰 데이터 청크 다운로드에 대해 말하는 것이 아니라 대부분 짧은 xml) .


또한 여기를
Honey

답변:


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 :와의 작업을 수행 할 필요가 없습니다
애슐리 밀스

2
이 명확한 예에 감사드립니다! (그냥 beingBackgroundUpdateTask를 beginBackgroundUpdateTask로 변경했습니다.)
newenglander

30
작업이 완료되지 않은 상태에서 doUpdate를 연속으로 여러 번 호출하면 self.backgroundUpdateTask를 덮어 쓰므로 이전 작업을 제대로 종료 할 수 없습니다. 작업 식별자를 매번 저장하여 올바르게 종료하거나 begin / end 메소드에서 카운터를 사용해야합니다.
thejaz

23

받아 들여진 대답은 매우 도움이되며 대부분의 경우 괜찮을 것입니다. 그러나 두 가지가 저를 괴롭 혔습니다.

  1. 많은 사람들이 언급했듯이 작업 식별자를 속성으로 저장한다는 것은 메서드가 여러 번 호출되면 덮어 쓸 수 있음을 의미하며 시간 만료시 OS가 강제로 종료 할 때까지 작업이 정상적으로 종료되지 않습니다. .

  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
이 솔루션을 정말 좋아합니다. 하지만 한 가지 질문 : typedefCompletionBlock을 어떻게 / 무엇으로 했 습니까? 간단하게이 :typedef void (^CompletionBlock)();
조셉

맞아요. typedef void (^ CompletionBlock) (void);
Joel

@joel, 감사합니다. 그러나이 구현에 대한 소스 코드의 링크, 즉 BackGroundTaskManager는 어디에 있습니까?
Özgür 2015

위에서 언급했듯이 "간결성을 위해 제외 된 단일 항목". [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앱이 백그라운드로 전환 될 때만 사용하고 싶지는 않습니다 . 반대로, 당신을 위해 백그라운드 작업을 사용해야합니다 어느 누구의 완료하면 앱이 경우에도 보장하려면 시간이 많이 걸리는 작업 않는 배경으로 이동합니다.

따라서 코드는 호출 beginBackgroundTaskendBackgroundTask일관성을 위해 동일한 상용구 코드의 반복으로 끝날 수 있습니다. 이러한 반복을 방지하려면 상용구를 캡슐화 된 단일 엔티티로 패키지화하는 것이 합리적입니다.

이를 수행하는 기존 답변 중 일부가 마음에 들지만 가장 좋은 방법은 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
matt

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

당신은 UR 코드를 사용하는 방법에 대한 완벽한 예를 적어주세요 수
아 므르 화가

아주 좋아. 감사.
Alyoshak
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.