The Danger of Invisible Pools
Do you notice any problem in the code below?
- (void)parseTitle:(NSString **)title fromText:(NSString *)text {
[text enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {
if ([line looksLikeTitle]) {
*title = line;
*stop = YES;
}
}];
}
How about this?
- (void)parseTitle:(NSString * __autoreleasing *)title fromText:(NSString *)text {
[text enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {
if ([line looksLikeTitle]) {
*title = line;
*stop = YES;
}
}];
}
How about this?
- (void)parseTitle:(NSString * __autoreleasing *)title {
[self autoreleaseFun:^(NSString *line) {
*title = line;
}];
}
- (void)autoreleaseFun:(void (^)(NSString *))block {
@autoreleasepool {
block(someString);
}
}
How about this?
- (void)parseTitle:(NSString * __autoreleasing *)title {
@autoreleasepool {
*title = someString;
}
}
What if I change the code from ARC
to MRR
?
- (void)parseTitle:(NSString **)title {
@autoreleasepool {
*title = [someString autorelease];
}
}
If you still can't see the problem, the last hint I'll show you is my crash:
EXC_BAD_ACCESS(code=1, address=0x50000010) #0 0x014cbde5 in objc_retain () #1 0x014cbe34 in objc_storeStrong () #2 0x00001ccb at the call of -parseTitle:fromText:
I reported this crash as a bug of ARC
during iOS 7 beta. Recently an Apple engineer got back to me saying:
You are misunderstanding the
__autoreleasing
property that is auto added by the compiler to yourNSString *
variables. Before returning fromparseTitle:fromText:
the value assigned totitle
is released.You need a local strong variable inside
parseTitle:fromText:
to hold onto the values passed into your block. Declare it with the__block
attribute so its strong within the scope of the method.
In fact, I know the first two pieces of code are the same. However, I don't believe the value assigned to *title
is released JUST BEFORE returning from parseTitle:fromText:
. I replied:
I don't see at which point the
autorelease pool
drains before returning fromparseTitle:fromText:
. Can you explain that? Thanks a lot!
Though I never heard from him again, I kept thinking about it, until today. I finally decided to ask my peer programmers on Stackoverflow. The aha moment came just before the end of my writing of the question post:
The
autorelease pool
must drain somewhere, but where? At the end of each enumeration block execution?
The problem is exactly that I can't see the autorelease pool
inside -[NSString enumerateLinesUsingBlock:]
. I can't see it does not mean it doesn't exist. I don't create it does not mean someone else doesn't create it. The danger is in the invisible. The danger is in regions that are out of my control.
Finally, the correct code1:
- (void)parseTitle:(NSString **)title fromText:(NSString *)text {
__block NSString *strongTitle;
[text enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {
if ([line looksLikeTitle]) {
strongTitle = line;
*stop = YES;
}
}];
*title = strongTitle;
}
Memory management is hard not because it is hard to understand. It is hard because it is hard to make sure you don't make any mistake all the time, for it is so easy to overlook something somewhere.
-
I've been using this correct code since I met the crash. That's before I reported the bogus bug. I know how to write the correct code. I just didn't know why the crashing one is wrong. ↩