这个答案必须在这里发布:取消 dispatch_after() 方法?,但这是作为重复关闭的(实际上不是)。无论如何,这是谷歌为“dispatch_after cancel”返回的地方,所以......
这个问题非常基础,我敢肯定有些人想要一个真正通用的解决方案,而无需求助于各种平台特定的东西,如运行循环计时器、包含实例的布尔值和/或重块魔法。GCD 可以用作常规的 C 库,并且可能根本没有计时器之类的东西。
幸运的是,有一种方法可以取消任何生命周期方案中的任何调度块。
- 我们必须为传递给 dispatch_after(或 dispatch_async,并不重要)的每个块附加一个动态句柄。
- 此句柄必须存在,直到块被实际触发。
- 这个句柄的内存管理并不那么明显——如果块释放了句柄,那么我们可以稍后取消对悬空指针的引用,但如果我们释放它,块可能会在以后这样做。
- 因此,我们必须按需传递所有权。
- 有 2 个块 - 一个是无论如何都会触发的控制块,第二个是可以取消的有效载荷。
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
}
}
而已。
要点是“所有者”应该收集句柄,直到它不再需要它们为止。dispatch_cancel_h() 用作句柄的[可能延迟的]析构函数。
C 所有者示例:
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 示例:
- (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.
}
笔记:
- dispatch_after() 本来是保留队列的,所以会一直存在,直到所有控制块都执行完。
- 如果有效负载被取消(或所有者的生命周期结束)并且控制块被执行,则 async_handles 被释放。
- 与 dispatch_after() 和 dispatch_queue_t 的内部结构相比,async_handle 的动态内存开销绝对很小,后者保留了要提交的实际块数组并在适当时将它们出列。
- 你可能注意到 shouldCall 和 shouldFree 实际上是同一个倒置标志。但是如果这些不依赖于“自我”或其他与所有者相关的数据,您的所有者实例可能会传递所有权甚至 -[dealloc] 本身而不实际取消有效负载块。这可以通过 dispatch_cancel_h() 的附加 shouldCallAnyway 参数来实现。
- 警告说明:此解决方案还缺少 didXYZ 标志的同步,并可能导致控制块和取消例程之间的竞争。使用 OSAtomicOr32Barrier() & co 进行同步。