> On Apr 25, 2017, at 9:48 PM, Quincey Morris 
> <[email protected]> wrote:
> 
> But put that aside. The *real* truth is that MRR memory management has always 
> been broken, because we relied on a heuristic for ensuring that memory 
> deallocations don’t happen at inopportune times. In particular, even if a 
> getter returns an autoreleased reference, you must still retain the returned 
> value *if there’s a possibility that the autorelease pool will be drained* 
> while the returned value is still in use though un-re-retained. Luckily, this 
> was an uncommon scenario.

Not so uncommon, in fact. Here’s a really easy one to get burned by:

> - (BOOL)doSomethingAndReturnError:(NSError **)error {
>       NSArray *things = [self getSomeThings];
>       
>       // Try to do something for each thing. If one of the things fails, 
> error out.
> 
>       for (Thing *eachThing in things) {
>               if (![eachThing doSomethingAndReturnError:error]) {
>                       return NO;
>               }
>       }
> 
>       return YES;
> }

This works. But then, it turns out that the app consumes an unreasonable amount 
of memory when you have a large number of things. Upon further analysis, it 
turns out that -[Thing doSomethingAndReturnError:] generates a lot of 
autoreleased objects, and since those never get drained until the loop is done, 
it creates a massive memory spike. Uh oh! Well, that’s okay, all we need to do 
is an an @autoreleasepool to the loop, right?

> - (BOOL)doSomethingAndReturnError:(NSError **)error {
>       NSArray *things = [self getSomeThings];
>       
>       // Try to do something for each thing. If one of them fails, error out.
> 
>       for (Thing *eachThing in things) @autoreleasepool {
>               if (![eachThing doSomethingAndReturnError:error]) {
>                       return NO;
>               }
>       }
> 
>       return YES;
> }

You build this, send it to the testers, everyone tries it with a bunch of 
different collections of things of different sizes, it all seems to work. Ship 
that sucker!

And then one of your users finally gets the rare runtime error that -[Thing 
doSomethingAndReturnError:] hardly ever throws, and your program crashes, 
because the error got reaped by the autorelease pool. D’oh.

This can bite you in ARC, too, because by-reference object parameters are 
autoreleasing by convention, to preserve compatible with MRC clients. It’s 
tough to work around, too. Look at the above. You’d think you’d just be able to 
fix it like this, right?

> - (BOOL)doSomethingAndReturnError:(NSError **)error {
>       NSArray *things = @[[Thing new], [Thing new], [Thing new]];
>       NSError *theError = nil;
>       
>       for (Thing *eachThing in things) @autoreleasepool {
>               if (![eachThing doSomethingAndReturnError:&theError]) {
>                       if (error) *error = theError;
>                       return NO;
>               }
>       }
>       
>       return YES;
> }

I’ve got an error variable declared outside the autoreleasepool, I assign to 
it, it gets retained, right? Wrong, because our local retain is balanced by a 
release when the method returns, and when it gets assigned to ‘error’, it gets 
autoreleased—inside our @autoreleasepool block. D’oh. So you’ve either got to 
declare the error parameter strong to keep the error out of the autorelease 
pool, potentially tripping up any MRC clients and causing them to leak:

> - (BOOL)doSomethingAndReturnError:(NSError * __strong *)error {
>       NSArray *things = @[[Thing new], [Thing new], [Thing new]];
>       NSError *theError = nil;
>       
>       for (Thing *eachThing in things) @autoreleasepool {
>               if (![eachThing doSomethingAndReturnError:&theError]) {
>                       if (error) *error = theError;
>                       return NO;
>               }
>       }
>       
>       return YES;
> }

Or, get rid of the early returns and keep a single return value that you don’t 
return until the end, which might get unclear and unwieldy if the method is 
long:

> - (BOOL)doSomethingAndReturnError:(NSError **)error {
>       NSArray *things = @[[Thing new], [Thing new], [Thing new]];
>       
>       BOOL failed = NO;
>       NSError *theError = nil;
>       
>       for (Thing *eachThing in things) @autoreleasepool {             
>               if (![eachThing doSomethingAndReturnError:&theError]) {
>                       failed = YES;
>                       break;
>               }
>       }
>       
>       if (failed && error != NULL) {
>               *error = theError;
>       }
>       
>       return !failed;
> }

Or use a goto:

> - (BOOL)doSomethingAndReturnError:(NSError **)error {
>       NSArray *things = @[[Thing new], [Thing new], [Thing new]];
>       NSError *theError = nil;
>       
>       @try {
>               for (Thing *eachThing in things) @autoreleasepool {
>                       if (![eachThing doSomethingAndReturnError:&theError]) {
>                               return NO;
>                       }
>               }
>               
>               return YES;
>       }
>       @finally {
>               if (error) *error = theError;
>       }
> }

None of these are particularly pleasing to the eye, but the most insidious 
thing is that it’s dead easy to miss that you need to do this if the method in 
question hardly ever throws an error.

Summary: I agree with you that MRC is/was broken. It’s a shame that we’re stuck 
with the concept of autorelease in ARC Objective-C, and even in Swift to some 
extent. I’m certain that if we’d had ARC from the beginning, autorelease and 
its assorted array of problems would never have existed.

Charles

 _______________________________________________
Do not post admin requests to the list. They will be ignored.
Objc-language mailing list      ([email protected])
Help/Unsubscribe/Update your Subscription:
https://lists.apple.com/mailman/options/objc-language/archive%40mail-archive.com

This email sent to [email protected]

Reply via email to