Prevent dispatch_after() background task from being executed
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.
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.
- We have to attach a dynamic handle to each block we pass to dispatch_after (or dispatch_async, not really matters).
- This handle must exist until the block is actually fired.
- 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.
- So, we have to pass ownership on demand.
- 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.
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];
Why even use GCD? You could just use an NSTimer
and invalidate it when your app returns to the foregound.