autoreleasepool uses in Swift

autoreleasepool uses in Swift

Foundation's NSAutoreleasePool type, later abstracted to the @autoreleasepool block, is a very old concept in iOS development. During the Obj-C era of iOS, usage of this type was important to prevent your app's memory from blowing up in specific cases. As ARC and Swift came around and evolved, very few people still have to manually play around with memory, making seeing it become a rare occurrence.

Having developed tools that need to allocate enourmous amounts of memory, I asked myself if there's still a case where @autoreleasepool is useful in Swift. Here are the answers.

What is @autoreleasepool? (Objective-C)

In the pre-ARC Obj-C days of manual memory management, retain() and release() had to be used to control the memory flow of an iOS app. As iOS's memory management works based on the retain count of an object, users could use these methods to signal how many times an object is being referenced so it can be safely dealloced if this value ever reaches zero.

However, consider the following situation where we have a getCoolLabel method that someone can use to get a really cool label:

-(NSString *)getCoolLabel {
    NSString *label = [[NSString alloc] initWithString:@"SwiftRocks"];
    return label;
}

NSString alloc automatically calls retain() to make sure label is able to exist (retain count of 1), and the label is then returned to someone else that wants to reference it, retaining it again (retain count 2) until nobody uses it anymore.

But there's a huge problem here. After the stack that called getCoolLabel calls release() to signal that it doesn't need it anymore, the retain count won't be 0, but 1. The internal NSString *label that was created to hold is also retaining it, and it needs to be released as well if we want the NSString itself to dealloc. The thing is, as label is unreachable outside of this method, there's no way we can release it:

-(NSString *)getCoolLabel {
    NSString *label = [[NSString alloc] initWithString:@"SwiftRocks"];
    [label release]; // Oopsie, nope!
    return label;
    [label release]; // Oopsie, nope!
}

If release() is called before return, the NSString will dealloc before it can be used which will crash the app, and calling after return means it will never be executed, causing a memory leak.

The solution for this edge case is to use a neat method called autorelease():

-(NSString *)getCoolLabel {
    NSString *label = [[NSString alloc] initWithString:@"SwiftRocks"];
    return [label autorelease];
}

Instead of instantly reducing the retain count of an object, autorelease() adds the object to a pool of objects that need to be released sometime in the future, but not now. By default, the pool will release these objects at the end of the run loop of the thread being executed, which is more than enough time to cover all usages of getCoolLabel() without causing memory leaks. Great, right?

Well, kinda. This will indeed solve your problem in 99% of the times, but consider this:

-(void)emojifyAllFiles {
    int numberOfFiles = 1000000;
    for(i=0;i<numberOfFiles;i++) {
        NSString *contents = [self getFileContents:files[i]];
        NSString *emojified = [contents emojified];
        [self writeContents:contents toFile:files[i]];
    }
}

Assuming that getFileContents and emojified return autoreleased instances, the app will be holding two million instances of NSStrings at once even though the individual properties could be safely released after their respective loops!

Because autorelease defers the release of these objects, they will only be released after the run loop ends -- which is way after the execution of emojifyAllFiles. If the contents of these files are large, this would cause serious issues if not crash the app entirely.

The solution to prevent this is the @autoreleasepool block; when used, every autoreleased property defined inside of it will be released exactly at the end of block:

-(void)emojifyAllFiles {
    int numberOfFiles = 1000000;
    for(i=0;i<numberOfFiles;i++) {
        @autoreleasepool {
            NSString *contents = [self getFileContents:files[i]];
            NSString *emojified = [contents emojified];
            [self writeContents:contents toFile:files[i]];
        }
    }
}

Instead of waiting until the end of the thread's run loop, the two NSStrings now receive a release message right after writeContents is called, keeping the memory usage stable.

In fact, "releasing after the thread's run loop" isn't compiler magic, this is simply because the threads themselves are surrounded by @autoreleasepools! You can see this partially in the main.m of any Obj-C project.

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

What about Swift though?

Is @autoreleasepool needed in ARC-era Swift?

The answer is it depends.

In theory, yes, as the problem shown still exists since autorelease is still a thing, but these problems are harder to come by. The ARC optimizations for Swift evolved a lot in the past few years, and as far as I tested it seems that ARC for Swift is now smart enough to simply never call autorelease, editing your code so that objects call release multiple times instead. The language itself doesn't even appear to have a definition for autorelease outside of the Obj-C bridging -- the one we can use in Swift is in fact the one from Obj-C. This means that for pure Swift objects, @autoreleasepool appears to be useless as nothing is never going to be autoreleased. The following code stays at a stable memory level even though it's looping millions of times.

for _ in 0...9999999 {
    let obj = getGiantSwiftClass()
}

Swift code calling Foundation / Legacy Objective-C code

However, it's a different story if your code is dealing with legacy Obj-C code, specially old Foundation classes in iOS. Consider the following code that loads a big image tons of times:

func run() {
    guard let file = Bundle.main.path(forResource: "bigImage", ofType: "png") else {
        return
    }
    for i in 0..<1000000 {
        let url = URL(fileURLWithPath: file)
        let imageData = try! Data(contentsOf: url)
    }
}

Even though we're in Swift, this will result in the same absurd memory spike shown in the Obj-C example! This is because the Data init is a bridge to the original Obj-C [NSData dataWithContentsOfURL] -- which unfortunately still calls autorelease somewhere inside of it. Just like in Obj-C, you can solve this with the Swift version of @autoreleasepool; autoreleasepool without the @:

autoreleasepool {
    let url = URL(fileURLWithPath: file)
    let imageData = try! Data(contentsOf: url)
}

The memory usage will now again be at a low, stable level.

How to know if a legacy UIKit/Foundation init/method returned an autoreleased instance?

Because these frameworks are closed source at Apple, there's no way to look their source code to see where autorelease is used.

I tried many ways to automate this inside Swift with no success. My attempts included expanding NSData to contain an associated object to a dummy Swift class, but ARC was still smart enough to deinit these objects right after their loops even though the Data object is technically still alive.

I also tried to create a weak reference to the Data instance hoping that it would be still alive after its loop, but it was too set to nil. This makes me think that it's possible that the Data object is indeed deiniting after the loop and what's autoreleasing in that init is something else entirely, but there's way to confirm this without looking the source code; which we unfortunately can't. I believe that there must be an internal property in NSAutoreleasePool or NSThread that you can inspect autoreleased properties, but for now the only way I found really is to manually look the Allocations instrument.

Conclusion

To put it short, autoreleasepool is still useful in iOS/Swift development as there are still legacy Obj-C classes in UIKit and Foundation that call autorelease, but you likely don't need to worry about it when dealing with Swift classes due to ARC's optimizations.

Follow me on my Twitter (@rockbruno_), and let me know of any suggestions and corrections you want to share.