This question relates to iOS apps in Objective-C using MRR (not ARC), and GCD (Grand Central Dispatch).
The Concurrency Programming Guide states this (emphasis mine):
For blocks that you plan to perform asynchronously using a dispatch queue, it is safe to capture scalar variables from the parent function or method and use them in the block. However, you should not try to capture large structures or other pointer-based variables that are allocated and deleted by the calling context. By the time your block is executed, the memory referenced by that pointer may be gone. Of course, it is safe to allocate memory (or an object) yourself and explicitly hand off ownership of that memory to the block.
This seems to contradict the Blocks and Variables doc which states:
When a block is copied, it creates strong references to object variables used within the block.
The latter statement seems to be describing how a block captures pointer-based variables (objects). In other words, the block is implicitly taking ownership, not the parent method explicitly handing off ownership to the block.
How would one explicitly hand off ownership of an object to a block? Is there really a way to do that, and is it needed in certain cases?
Here's a test.
Given a simple data class named Employee with a few properties:
Employee.h:
#import <Foundation/Foundation.h>
@interface Employee : NSObject
@property (nullable, nonatomic, retain) NSNumber* empID;
@property (nullable, nonatomic, retain) NSString* firstName;
@property (nullable, nonatomic, retain) NSString* lastName;
@end
A sample app calls dispatchAfterTest to test memory management in asynchronously dispatched blocks. The test was much simpler to begin with, but kept getting expanded to explore different possibilities.
- (void)dispatchAfterTest {
NSLog(@"%s started", __func__);
Employee* employee = [Employee new];
employee.empID = @(1001);
employee.firstName = @"First Name";
employee.lastName = @"Last Name";
NSMutableArray<Employee*>* employeeMutableArray = [NSMutableArray<Employee*> new];
[employeeMutableArray addObject:employee];
[employee release];
employee = [Employee new];
employee.empID = @(1002);
employee.firstName = @"Adam";
employee.lastName = @"Zam";
[employeeMutableArray addObject:employee];
Employee* employee3 = [Employee new];
employee3.empID = @(1003);
employee3.firstName = @"John";
employee3.lastName = @"Kealson";
[employeeMutableArray addObject:employee3];
NSArray<Employee*>* employeeArray = [[NSArray<Employee*> alloc] initWithArray:employeeMutableArray];
NSArray<Employee*>* autoreleasedArray = [NSArray<Employee*> arrayWithArray:employeeMutableArray];
// dispatch a block asynchronously that will use the employee object, employeeMutableArray, and employeeArray
NSLog(@"%s calling dispatch_after()", __func__);
//dispatch_queue_t queue = dispatch_queue_create("com.example.MyQueue", NULL);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
double delayInSeconds = 5;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, queue, ^(void){
NSLog(@"%s start of dispatch_after block", __func__);
NSLog(@"%s empID=%@, firstName=%@, lastName=%@", __func__, employee.empID, employee.firstName, employee.lastName); // Expecting EXC_BAD_ACCESS error if employee object has been deallocated.
NSLog(@"%s employeeMutableArray:", __func__);
for (Employee* emp in employeeMutableArray) {
NSLog(@"%s ID: %@ Name: %@ %@", __func__, emp.empID, emp.firstName, emp.lastName);
}
NSLog(@"%s employeeArray:", __func__);
for (Employee* emp in employeeArray) {
NSLog(@"%s ID: %@ Name: %@ %@", __func__, emp.empID, emp.firstName, emp.lastName);
}
NSLog(@"%s autoreleasedArray:", __func__);
for (Employee* emp in autoreleasedArray) {
NSLog(@"%s ID: %@ Name: %@ %@", __func__, emp.empID, emp.firstName, emp.lastName);
}
employee.lastName = @"Zammal";
NSLog(@"%s employee.lastName changed to %@", __func__, employee.lastName);
NSLog(@"%s employeeMutableArray[1].lastName=%@", __func__, employeeMutableArray[1].lastName);
NSLog(@"%s employeeArray[1].lastName=%@", __func__, employeeArray[1].lastName);
NSLog(@"%s autoreleasedArray[1].lastName=%@", __func__, autoreleasedArray[1].lastName);
NSLog(@"%s end of dispatch_after block", __func__);
});
NSLog(@"%s changing 1st employee's name to John Smith", __func__);
employeeMutableArray[0].firstName = @"John";
employeeMutableArray[0].lastName = @"Smith";
//NSLog(@"%s releasing serial dispatch queue", __func__);
//dispatch_release(queue); // Not needed for global dispatch queues, including the concurrent dispatch queues or the main dispatch queue.
NSLog(@"%s releasing employee objects and arrays (except autoreleasedArray)", __func__);
[employee release];
[employee3 release];
[employeeMutableArray release];
[employeeArray release];
/*
for (int ii = 0; ii < 10; ii++) {
// The following NSLog() throws an EXC_BAD_ACCESS runtime error after the dispatch_after block finishes.
NSLog(@"%s accessing employee object after release: ID=%@, firstName=%@, lastName=%@", __func__, employee.empID, employee.firstName, employee.lastName);
NSLog(@"%s sleeping 1 seconds", __func__);
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
}
*/
NSLog(@"%s finished", __func__);
}
And here's the output:
2016-03-06 14:49:56.127 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController dispatchAfterTest] started
2016-03-06 14:49:56.127 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController dispatchAfterTest] calling dispatch_after()
2016-03-06 14:49:56.127 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController dispatchAfterTest] changing 1st employee's name to John Smith
2016-03-06 14:49:56.127 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController dispatchAfterTest] releasing serial dispatch queue
2016-03-06 14:49:56.127 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController dispatchAfterTest] releasing employee objects and arrays (except autoreleasedArray)
2016-03-06 14:49:56.127 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController dispatchAfterTest] finished
2016-03-06 14:49:56.139 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController tableView:viewForHeaderInSection:]
2016-03-06 14:50:01.619 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke start of dispatch_after block
2016-03-06 14:50:01.620 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke empID=1002, firstName=Adam, lastName=Zam
2016-03-06 14:50:01.620 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke employeeMutableArray:
2016-03-06 14:50:01.620 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke ID: 1001 Name: John Smith
2016-03-06 14:50:01.621 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke ID: 1002 Name: Adam Zam
2016-03-06 14:50:01.621 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke ID: 1003 Name: John Kealson
2016-03-06 14:50:01.621 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke employeeArray:
2016-03-06 14:50:01.621 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke ID: 1001 Name: John Smith
2016-03-06 14:50:01.622 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke ID: 1002 Name: Adam Zam
2016-03-06 14:50:01.622 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke ID: 1003 Name: John Kealson
2016-03-06 14:50:01.622 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke autoreleasedArray:
2016-03-06 14:50:01.622 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke ID: 1001 Name: John Smith
2016-03-06 14:50:01.622 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke ID: 1002 Name: Adam Zam
2016-03-06 14:50:01.623 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke ID: 1003 Name: John Kealson
2016-03-06 14:50:01.623 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke employee.lastName changed to Zammal
2016-03-06 14:50:01.623 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke employeeMutableArray[1].lastName=Zammal
2016-03-06 14:50:01.623 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke employeeArray[1].lastName=Zammal
2016-03-06 14:50:01.623 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke autoreleasedArray[1].lastName=Zammal
2016-03-06 14:50:01.623 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke end of dispatch_after block
Some things to note.
- Because of the 5 second delay before the block is executed, the employee, employeeMutableArray, and employeeArray objects were clearly released well before the block was executed, which proves that it did indeed implicitly take ownership of them. For some test runs the delay was increased to 120 seconds or more and got the same results.
- If the
forloop at the end of dispatchAfterTest is uncommented, it throws an EXC_BAD_ACCESS runtime error as expected after the dispatched block finishes. - Changing the 1st employee's name to John Smith in the parent method after dispatching the block shows that the block did not simply take a copy of the data at the time it was dispatched.
At one point early in testing the block threw an NSInvalidArgumentException when I think it was trying to access employeeArray that was autoreleased (i.e. created via [NSArray arrayWithArray:]). I've not been able to replicate that exception, and unfortunately do not have a copy of the test code as it was at the time.
Here's the exception details:
2016-02-28 10:21:48.235 StudyObjC_MRR_CoreData[669:48826] __54-[ContactsTableViewController dispatchAsyncExperiment]_block_invoke start of dispatch_after block
2016-02-28 10:21:48.236 StudyObjC_MRR_CoreData[669:48826] __54-[ContactsTableViewController dispatchAsyncExperiment]_block_invoke empID=1002, firstName=Adam, lastName=Zam
2016-02-28 10:21:48.236 StudyObjC_MRR_CoreData[669:48826] __54-[ContactsTableViewController dispatchAsyncExperiment]_block_invoke employeeMutableArray:
2016-02-28 10:21:48.236 StudyObjC_MRR_CoreData[669:48826] __54-[ContactsTableViewController dispatchAsyncExperiment]_block_invoke ID: 1001 Name: First Name Last Name
2016-02-28 10:21:48.236 StudyObjC_MRR_CoreData[669:48826] __54-[ContactsTableViewController dispatchAsyncExperiment]_block_invoke ID: 1002 Name: Adam Zam
2016-02-28 10:21:48.237 StudyObjC_MRR_CoreData[669:48826] -[CALayer countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance 0x7f80b8f3d390
2016-02-28 10:21:48.241 StudyObjC_MRR_CoreData[669:48826] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CALayer countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance 0x7f80b8f3d390'
First throw call stack:
(
0 CoreFoundation 0x00000001040d0e65 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x00000001037c0deb objc_exception_throw + 48
2 CoreFoundation 0x00000001040d948d -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
3 CoreFoundation 0x000000010402690a ___forwarding___ + 970
4 CoreFoundation 0x00000001040264b8 _CF_forwarding_prep_0 + 120
5 StudyObjC_MRR_CoreData 0x00000001032bcc5b __54-[ContactsTableViewController dispatchAsyncExperiment]_block_invoke + 955
My first guess was that the block hadn't retained the array since it was an "autoreleased" object, so I changed it to be initialized with initWithArray: and manually released it after the dispatch_after() call.
autoreleasedArray was added later to try to replicate the exception, but it is working fine.
Restating the question (if I may turn one question into many):
- Is the test method using a safe way to pass local object variables to a block that will be executed asynchronously, or does ownership need to be handed off explicitly to the block as the Concurrency guide states?
- If ownership needs to be explicitly handed off to the block, how is that done?
- [Bonus Question] If the current method isn't safe, how can it be modified to prove it isn't safe (i.e. cause a deallocation issue) while essentially still using the same method (local object variables implicitly captured by the block)?