Prevent dispatch_after() background task from being executed
Asked Answered
G

11

12

This is my issue. When my application enters background I want it to perform a function after certain period of time. This is what I do:

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    isRunningInBackground = YES;

    taskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];

    int64_t delayInSeconds = 30;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
    dispatch_after(popTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void)
    {
        [self doSomething];
    });
}

- (void)doSomething
{
   NSLog(@"HELLO");
}

taskIdentifier variable is declared in myAppDelegate.h file like this:

UIBackgroundTaskIdentifier taskIdentifier;

Everything works as it supposed to, I see that console prints HELLO just right after 30 seconds are gone. But I don't want doSomething to be executed if the app enters foreground until 30 seconds are over. So I need to cancel it. This is how i do that:

- (void)applicationWillEnterForeground:(UIApplication *)application
{    
    isRunningInBackground = NO;
    [self stopBackgroundExecution];
}

- (void)stopBackgroundExecution
{
    [[UIApplication sharedApplication] endBackgroundTask:taskIdentifier];
    taskIdentifier = UIBackgroundTaskInvalid;
}

But unfortunately it doesn't cancel doSomething, it is still performed. What am I doing wrong? How do I cancel that function?

Grandpa answered 18/9, 2012 at 10:46 Comment(0)
D
14

Why even use GCD? You could just use an NSTimer and invalidate it when your app returns to the foregound.

Deranged answered 18/9, 2012 at 11:36 Comment(2)
Thanks a lot! So simple! I should have thought about that myselfGrandpa
Any code plz...Fidgety
M
12

A bit different approach OK, so, with all answers collected, and possible solutions, seems like the best one for this case (preserving simplicity) is calling performSelector:withObject:afterDelay: and cancelling it with cancelPreviousPerformRequestsWithTarget: call when desired. In my case - just before scheduling next delayed call:

[NSObject cancelPreviousPerformRequestsWithTarget: self selector:@selector(myDelayedMethod) object: self];

[self performSelector:@selector(myDelayedMethod) withObject: self afterDelay: desiredDelay];
Minerva answered 4/2, 2015 at 14:17 Comment(0)
F
10

I answered the question about cancel dispatch_after here. But when i google to find a solution it also return me to this thread, so...

iOS 8 and OS X Yosemite introduced dispatch_block_cancel that allow you to cancel a block before they start executing. You can view detail about that answer here

Using dispatch_after get benefit about using variables that you created in that function and look seamless. If you use NSTimer then you must create a Selector and send variables that you need into userInfo or turn that variables into global variables.

Frankfurter answered 15/6, 2016 at 8:41 Comment(1)
This is actually the correct answer now, as the original answer is essentially "Don't do that. Do it this way instead."Brooklet
P
6

This answer must be posted here: cancel dispatch_after() method?, but that is closed as a duplicate (it really isn't). Anyway, this is a place that google returns for "dispatch_after cancel", so...

This question is pretty fundamental and I'm sure there are people who want a truly generic solution without resorting to various platform-specifics like runloop timers, instance-contained booleans and/or heavy block magic. GCD may be used as a regular C library and there may be no such thing as a timer all in all.

Luckily, there is a way to cancel any dispatch block in any lifetime scheme.

  1. We have to attach a dynamic handle to each block we pass to dispatch_after (or dispatch_async, not really matters).
  2. This handle must exist until the block is actually fired.
  3. Memory management for this handle is not so obvious – if block frees the handle, then we may dereference dangling pointer later, but if we free it, block may do that later.
  4. So, we have to pass ownership on demand.
  5. There are 2 blocks – one is a control block that fires anyway and second is a payload that may be canceled.

struct async_handle {
    char didFire;       // control block did fire
    char shouldCall;    // control block should call payload
    char shouldFree;    // control block is owner of this handle
};

static struct async_handle *
dispatch_after_h(dispatch_time_t when,
                 dispatch_queue_t queue,
                 dispatch_block_t payload)
{
    struct async_handle *handle = malloc(sizeof(*handle));

    handle->didFire = 0;
    handle->shouldCall = 1; // initially, payload should be called
    handle->shouldFree = 0; // and handles belong to owner

    payload = Block_copy(payload);

    dispatch_after(when, queue, ^{
        // this is a control block

        printf("[%p] (control block) call=%d, free=%d\n",
            handle, handle->shouldCall, handle->shouldFree);

        handle->didFire = 1;
        if (handle->shouldCall) payload();
        if (handle->shouldFree) free(handle);
        Block_release(payload);
    });

    return handle; // to owner
}

void
dispatch_cancel_h(struct async_handle *handle)
{
    if (handle->didFire) {
        printf("[%p] (owner) too late, freeing myself\n", handle);
        free(handle);
    }
    else {
        printf("[%p] (owner) set call=0, free=1\n", handle);
        handle->shouldCall = 0;
        handle->shouldFree = 1; // control block is owner now
    }
}

That's it.

The main point is that "owner" should collect handles until it doesn't need them anymore. dispatch_cancel_h() works as a [potentially deferred] destructor for a handle.

C owner example:

size_t n = 100;
struct after_handle *handles[n];

for (size_t i = 0; i < n; i++)
    handles[i] = dispatch_after_h(when, queue, ^{
        printf("working\n");
        sleep(1);
    });

...

// cancel blocks when lifetime is over!

for (size_t i = 0; i < n; i++) {
    dispatch_cancel_h(handles[i]);
    handles[i] = NULL; // not our responsibility now
}

Objective-C ARC example:

- (id)init
{
    self = [super init];
    if (self) {
        queue = dispatch_queue_create("...", DISPATCH_QUEUE_SERIAL);
        handles = [[NSMutableArray alloc] init];
    }
    return self;
}

- (void)submitBlocks
{
    for (int i = 0; i < 100; i++) {
        dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (random() % 10) * NSEC_PER_SEC);

        __unsafe_unretained id this = self; // prevent retain cycles

        struct async_handle *handle = dispatch_after_h(when, queue, ^{
            printf("working (%d)\n", [this someIntValue]);
            sleep(1);
        });
        [handles addObject:[NSValue valueWithPointer:handle]];
    }
}

- (void)cancelAnyBlock
{
    NSUInteger i = random() % [handles count];
    dispatch_cancel_h([handles[i] pointerValue]);
    [handles removeObjectAtIndex:i];
}

- (void)dealloc
{
    for (NSValue *value in handles) {
        struct async_handle *handle = [value pointerValue];
        dispatch_cancel_h(handle);
    }
    // now control blocks will never call payload that
    // dereferences now-dangling self/this.
}

Notes:

  • dispatch_after() originally retains the queue, so it will exist until all control blocks are executed.
  • async_handles are freed if payload is cancelled (or owner's lifetime was over) AND control block was executed.
  • async_handle's dynamic memory overhead is absolutely minor compared to dispatch_after()'s and dispatch_queue_t's internal structures, which retain an actual array of blocks to be submitted and dequeue them when appropriate.
  • You may notice that shouldCall and shouldFree is really the same inverted flag. But your owner instance may pass the ownership and even -[dealloc] itself without actually canceling payload blocks, if these do not depend on "self" or other owner-related data. This could be implemented with additional shouldCallAnyway argument to dispatch_cancel_h().
  • Warning note: this solution also lacks synchronization of didXYZ flags and may cause a race between control block and cancellation routine. Use OSAtomicOr32Barrier() & co to synchronize.
Piperidine answered 14/5, 2015 at 23:27 Comment(1)
Note: code in the answer was written for ARC. In MRC or pure C, payload should be explicitly Block_retain'ed in dispatch_after_h() and Block_release'd in control block to fix premature deallocation.Piperidine
I
5

Since iOS 10 and Swift 3 GCD DispatchWorkItem are cancellable. Just keep an instance to the work item and check if it hasn't been cancelled and then cancel it:

// Create a work item
let work = DispatchWorkItem {
    print("Work to be done or cancelled")
}

// Dispatch the work item for executing after 2 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: work)

// Later cancel the work item
if !work.isCancelled {
    work.cancel()
}
Identification answered 23/10, 2016 at 18:35 Comment(0)
D
3

endBackgroundTask does not cancel a background task. It tells the system that your background task has finished. So you should call this after "doing something". To prevent doSomething from being executed if your app is in the foreground again, you could use your isRunningInBackground flag:

dispatch_after(popTime, dispatch_get_global_queue(...), ^(void) {
    if (isRunningInBackground) {
        [self doSomething];
    }
    [[UIApplication sharedApplication] endBackgroundTask:taskIdentifier];
});
Dykstra answered 18/9, 2012 at 11:25 Comment(0)
F
2

I think you can't cancel it, but you can check the task state before executing the doSomething

dispatch_after(popTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void)
  {

    if(taskIdentifier != UIBackgroundTaskInvalid) {
        [self doSomething];
    }

  });
Fredela answered 18/9, 2012 at 11:27 Comment(0)
V
2

https://developer.apple.com/documentation/dispatch/1431058-dispatch_block_cancel

dispatch_block_cancel Cancels the specified dispatch block asynchronously.

void dispatch_block_cancel(dispatch_block_t block);

(iOS 8.0+, macOS 10.10+, tvOS 9.0+, watchOS 2.0+)

Vacuva answered 28/5, 2019 at 13:13 Comment(0)
O
1

You can absolutely cancel it with a flag. I wrote a small function to do it, basically we pass a BOOL pointer to control whether the block is cancelled.

void dispatch_with_cancellation(void (^block)(), BOOL* cancellation) {
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
    dispatch_after(time, dispatch_get_main_queue(), ^{
        if (!*cancellation) {
            block();
        }
    });
}

int main(int argc, char *argv[]) {
    @autoreleasepool {
        void (^block)() = ^{
            NSLog(@"%@", @"inside block");
        };
        BOOL cancellation;
        dispatch_with_cancellation(block, &cancellation);
        // cancel the block by setting the BOOL to YES.
        *&cancellation = YES;
        [[NSRunLoop currentRunLoop] run];
    }
}
Organization answered 6/5, 2016 at 23:49 Comment(0)
L
1

This is a somewhat more generic response, although I think it still answers your question reasonably well. Instead of "isRunningInBackground" keep the time you last backgrounded/foregrounded; use the time you were backgrounding as a local variable to the dispatch_after. Check inside your dispatch_after before calling doSomething. My more specific problem below....

I'm doing a long pile of animations that need to launch at various times and would stomp all over each other if I used setBeginTime while making sure the model layer updated to the presentation layer at the right time, etc... so I started using dispatch_after, except couldn't "cancel" them (which mattered for me especially when I wanted to restart the series of animations).

I'm keeping a CFTimeInterval startCalled; on my UIView instance, and then inside of my -(void) start I have:

startCalled = CACurrentMediaTime();
CFTimeInterval thisStartCalled = startCalled;

At the beginning of each dispatch_after block, I then have:

if (thisStartCalled != startCalled) return;

This lets me set up everything in one go, but only have my model layers update inside of their CATransaction blocks at the time they're supposed to begin.

Lieabed answered 19/7, 2016 at 23:44 Comment(0)
D
0

I used below approach for debounce the search without blocking the main thread.

Create a global variable to store the block

dispatch_block_t searchBlock;

Check if it exists and cancel it (only if it was not yet dispatched)

   if (searchBlock) {
        dispatch_block_cancel(searchBlock);
    }

Define the block

    searchBlock = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, ^{
        //Do Something
    });

Execute it

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), serialQueue, searchBlock);
Dryad answered 6/4, 2021 at 11:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.