GCD Timer and Leaks

GCD 的 Timer 相对于 NSTimer 更加灵活、高效(无需在主线程和后台线程之间进行切换),而且 NSTimer 需要在合适的地方调用 invalid 以避免内存泄漏,所以平时比较常用。但是发现 GCD Timer 随便调用 API 的话也会造成内存泄漏。

GCD Timer 通过 Dispatch Source 实现,调用方法如:

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 
    2.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^() {
    // do something
});
dispatch_resume(timer);

如果需要暂停定时器调用

dispatch_suspend(timer);

dispatch_suspend 和 dispatch_resume 应该是成对出现的。两者分别会减少和增加 dispatch 对象的挂起计数,但是没有 API 获取当前是挂起还是执行状态,所以需要自己记录。

dispatch_suspend 暂停队列并不意味着当前执行的 block 暂停

当暂停派发队列时需要注意,调用 dispatch_suspend 暂停一个队列,并不意味着暂停当前正在执行的 block,而是 block 可以执行完,但是接下来的 block 会被暂停,直到 dispatch_resume 被调用。

如果添加 dispatch_suspend 的调用后 timer 是无法被释放的。一般情况下会发生崩溃并报“EXC_BAD_INSTRUCTION”错误,看下 GCD 源码 Source 释放的处理 dispatch source release 的时候判断了当前是否是在暂停状态。

void _dispatch_source_xref_release(dispatch_source_t ds) {
    if (slowpath(DISPATCH_OBJECT_SUSPENDED(ds))) {
        // Arguments for and against this assert are within 6705399
        DISPATCH_CLIENT_CRASH("Release of a suspended object");
    }
    _dispatch_wakeup(ds);
    _dispatch_release(ds);
}

GCD 完整源码点击这里

dispatch_suspend 状态下无法释放

但是还有不一般的情况,如果暂停的代码加到 dispatch_source_set_event_handler 的 block 中,并不会发生崩溃,但是这个时候页面会无法释放造成内存泄漏!内存泄漏!内存泄漏!。

dispatch_source_set_event_handler(timer, ^() {
    // do something
    dispatch_suspend(timer);
});

Demo 代码点击这里,Demo 中进入“GCD Timer” 页面再退出,通过 Allocations 工具可以发现 GCDTimerViewController 没有释放,dealloc 无法执行。

怎么破?如果有需求按照上面这种方式写的话,添加个变量记录下 dispatch_suspend 是否被调用的状态,在 dealloc 时判断下,调用过 dispatch_suspend 则再调用下 dispatch_resume。或者暂停后不需要重新运行 timer 的话,取消 timer,一旦取消则不能再重新运行 timer,只能重建。

dispatch_source_cancel(timer);

参考内容

Mikeash GCD 介绍

请我喝汽水儿