Cancel dispatch_after

Joel and I have been working on a project recently that relies pretty heavily on the delayed execution of blocks. It became evident pretty quickly that we needed a way to cancel these blocks. We worked around the problem in kludgy ways initially, but because of the high memory usage of this app, we began running into crashes in cases where delayed blocks were retaining their objects far longer than we would have liked them to. I finally had to bite the bullet, and wrote a simple wrapper function that would allow us to cancel blocks that had been delayed using dispatch_after();

We’ve decided to put this code up on GitHub, and you can find it at our new repository.

The function is extremely easy to use, and tends to have a usage style similar to the block based API for NSNotificationCenter. Simply put, the perform_block_after_delay function returns a block handle that allows for the delayed execution to be canceled at any time.

@interface SMViewController : UIViewController
{
    __block SMDelayedBlockHandle _delayedBlockHandle;
}
@end

@implementation SMViewController

- (void)delayBlock
{
    SMDelayedBlockHandle handle = perform_block_after_delay(2.0f, ^{
                // Work
                [_delayedBlockHandle release];
                _delayedBlockHandle = nil;
            });
    _delayedBlockHandle = [handle retain];
}

- (void)cancelBlock
{
    if (nil == _delayedBlockHandle) {
        return;
    }

    _delayedBlockHandle(YES);
    [_delayedBlockHandle release];
    _delayedBlockHandle = nil;
}

@end

Under The Hood

Under the hood, there’s not much taking place outside of a little block juggling. The block passed to the perform_block_after_delay function is copied, and wrapped in a cancel block. The cancel block takes one argument – BOOL cancel. A third block literal is executed using the dispatch_after method, and does nothing more than execute the cancel handle, passing an argument of NO.

The magic mostly lies in the cancel handle. When the cancel block is executed, if and only if the passed argument is NO, the original delayed block is executed – next, regardless of the argument passed, the original block and the cancel handle are released and set to nil. This means that if the handle is executed at any time, the original block is potentially executed, and then all blocks are cleaned up. There is no way to stop a block from executing that was dispatched with dispatch_after (there wouldn’t be a point for this post if there were), so we get around this by gutting the handle after it’s canceled which ensures that the delayed block never has any real code to perform.

The major upshot to all of this is that once a block is executed or canceled, all retains are cleaned up and memory has the potential to be cleaned up; or at least, memory is no longer being retained by a block waiting to execute…..eventually.

About Jerry Jones

Co-Founder of Spaceman Labs, Inc. Formerly of Mellmo, Inc. iOS Developer since 2007. You can find me on LinkedIn, Twitter, and doing backflips on jet skis.
This entry was posted in Code and tagged , , , . Bookmark the permalink.