background
The question of whether GCD’s block capture of self creates circular references is a subject of much debate on the Web, especially in iOS interviews. Let’s start with an Issue in YYKit to see if GCD and self can cause circular references.
The Issue originates from a piece of code in YYKit:
- (void)_trimInBackground {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
/* **/
});
}
Copy the code
It can be seen that YY Dashen uses strong-weak dance in GCD in order to avoid circular reference. However, netizens put forward in this Issue that Apple’s dispatch_async function Block_release the block after completion of the task, and does not create a circular reference. Strong-weak dance may cause self to be freed before the block executes.
YY argues that since self holds a _queue variable, and _queue holds the block, capturing self directly within the block creates a circular reference. (self – > _queue – > block – > self)
But is this really a memory leak?
explore
Let’s create a simple Demo with the following code:
@interface ViewController2(a)
@property (nonatomic.strong) dispatch_queue_t queue;
@end
@implementation ViewController2
- (void)viewDidLoad {
[super viewDidLoad];
self.queue = dispatch_queue_create("com.mayc.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(self.queue, ^{
[self test];
});
}
- (void)test {
NSLog(@"test");
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
Copy the code
In the Demo, ViewController2 holds a queue variable, and the dispatch_async block captures self. We open up a ViewController2 page and close it; If dispatch_async strong-capture self causes a memory leak, then ViewController2’s dealloc method must not execute. The result is as follows:
2020- 03- 11 15:36:35.352789+0800 MCDemo[83661:22062265] test
2020- 03- 11 15:36:36.922477+0800 MCDemo[83661:22062108] dealloc
Copy the code
You can see that ViewController2 is released normally, which means there is no memory leak.
Source code analysis
Dispatch_async: dispatch_async: dispatch_async
void dispatch_async(dispatch_queue_t dq, dispatch_block_t work) {
dispatch_continuation_t dc = _dispatch_continuation_alloc();
uintptr_t dc_flags = DC_FLAG_CONSUME;
dispatch_qos_t qos;
// Wrap work (the task block we passed in) as dispatch_continuation_t
qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
_dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}
Copy the code
As you can see, blocks passed by dispatch_async are eventually wrapped with dispatch_continuation_t as other parameters. Let’s focus on this wrapped code (the code below is simplified) :
static inline dispatch_qos_t
_dispatch_continuation_init(dispatch_continuation_t dc,
dispatch_queue_class_t dqu, dispatch_block_t work,
dispatch_block_flags_t flags, uintptr_t dc_flags)
{
/ / copy block
void *ctxt = _dispatch_Block_copy(work);
dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED;
dispatch_function_t func = _dispatch_Block_invoke(work);
if (dc_flags & DC_FLAG_CONSUME) {
// As the name implies, the block is executed and then released.
func = _dispatch_call_block_and_release;
}
return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags);
}
// the source code for _dispatch_call_block_and_release is as follows
void _dispatch_call_block_and_release(void *block)
{
void (^b)(void) = block;
b(); / / execution
Block_release(b); / / release
}
Copy the code
As you can see from the Apple documentation, dispatch_async releases the block after it completes execution. So the _self->queue->block->self loop reference is only temporary (the block is freed after completion, breaking the loop reference).
warren
dispatch_sync
Since the block capture self of dispatch_async does not cause a circular reference, what about dispatch_sync instead?
self.queue = dispatch_queue_create("com.mayc.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(self.queue, ^{
[self test];
});
Copy the code
Dispatch_sync is not a problem either. We replaced dispatch_async with dispatch_sync in the Demo, and you can see that there is no memory leak either.
2020- 03- 11 17:05:18.840834+0800 MCDemo[5437:69508] test
2020- 03- 11 17:05:20.419588+0800 MCDemo[5437:68626] dealloc
Copy the code
Dispatch_sync does not cause a _self->queue->block->self loop reference because dispatch_async is released after execution. Let’s take a look at the dispatch_sync documentation.
Unlike with
dispatch_async
, no retain is performed on the target queue. Because calls to this function are synchronous, it “borrows” the reference of the caller. Moreover, noBlock_copy
is performed on the block.
Basically, the queue does not hold blocks and does not Block_copy. Since queue -> block does not exist, there is no circular reference.
Dispatch_after and other GCD apis
Dispatch_async dispatch_group_async dispatch_group_async dispatch_group_async
_This function performs a
Block_copy
andBlock_release
on behalf of the caller. _
As you can see, these GCD API methods are also released, so the rest of them don’t cause a circular reference by capturing self.
expand
Since self holding a queue doesn’t cause a loop reference to the GCD, what if self holds a block directly from the GCD?
dispatch_queue_t queue = dispatch_queue_create("com.mayc.concurrent", DISPATCH_QUEUE_CONCURRENT);
self.block = ^{
[self test];
};
dispatch_async(queue, self.block);
Copy the code
emm… If you have to do so, will certainly be a memory leak…. This is because the block is held directly by self and a Block_copy operation is performed in GCD with a reference counter of 2. After the block task is completed, Block_release is performed, and the reference counter is 1. In this case, the block will not be cleaned.