Care and Feeding of Closures

We’re going to see closures in several more places in the iOS SDK, so it’s a good idea to make sure we’re fully comfortable with them before we move on.

Trailing Closure Syntax

One little change we can make right off the bat will make our code easier to read. In Swift, if the last argument to a method or function is a closure, we can omit its label and put it directly after the closing parenthesis of the declaration. In other words, if we start with this:

 function(param1: foo, param2: { bar in
  ...
 })

We can instead write:

 function(param1: foo) { bar in
  ...
 }

This is much easier to read and eliminates the weird trailing }) when the closure and parameter list end. We’ll use this syntax from here on out.

Closures and Memory Management

Our code has a more serious and very subtle problem. Back in The Swift Programming Language, we discussed how Swift deals with the memory management of reference types—as opposed to structs, enums, and the numeric types, which are all value types—with a system of “reference counting.”

The way reference counting works is that each object is created with a reference count of 1. Objects are freed from memory if their count ever goes to 0. For example, if the code that creates an object doesn’t do anything with it by the time it goes out of scope, the reference count will go down by one, reach 0, and the object will be destroyed.

For throwaway objects referred to by local variables within a method, that’s fine. And when we want to hold onto an object for longer, we can assign it to a property. The property assignment increments the reference count, so it stays around as long as the property needs it to. Adding the object to a collection like an array also increments the reference count, so it works the same way.

It’s a pretty good system, because it’s faster and more deterministic than the popular system of garbage collection used in other languages: memory can be reclaimed immediately when it’s not needed, and the CPU doesn’t have to go out and look for unreferenced objects it can free.

images/closures/arc-diagrams-reference-cycle.png

But there’s a fairly big problem with reference counting: what happens when two objects refer to each other? Consider the diagram pictured here, where object A has a reference count of 1, because some other object has it as a property or in a collection. If object A creates object B and holds on to it with a property, B’s reference count is 1. If B then adds a reference back to A, A’s reference count becomes 2.

Now imagine the object originally referring to A removes its reference (or is freed from memory itself). With nothing else referring to A, we should expect it to be cleaned up, which will nil out any properties as it deinits, thereby freeing up B. However, this can never happen. A cannot be freed because it still has a reference count of 1, and that’s because it is a property of B. But B cannot be freed either, because A has a reference to it, so B’s reference count will never go to 0.

This is called a reference cycle, and it is a serious problem because it means our app will consume more and more memory for no good reason, since we’re unable to free objects that we don’t actually want around anymore.

We’ve unwittingly walked into this situation ourselves: player is a property of our ViewController, and it has the closure we passed to it for the periodic observer. However, that closure has a reference to self, which is the ViewController. Therefore, reference counting can’t free either object: ViewController forces the player to remain in memory, and the closure we passed to the player keeps the ViewController in memory.

The way to break a retain cycle is to have different kinds of references. In Swift, these are strong and weak, where strong is the default and what we’ve been using all along. The difference is that a weak reference does not increment the reference count. It still points to the other object, but it doesn’t force it to stick around in memory. If the referred-to object has its reference count go to 0, the weak reference just becomes nil.

images/closures/arc-diagrams-reference-cycle-broken.png

Let’s think about this in our A and B example. A creates B as before, but B creates a weak reference back to A. This means that A’s reference count remains at 1, even after B adds it as a (weak) property. So when the one object that was referring to A drops its reference, A’s reference count becomes 0. Because it’s 0, A can now be freed from memory. In A’s deinit, all of its properties are nil’ed out, reducing their reference counts by 1. That means B’s reference count is now 0, and it, too, can be freed.

So how do we fix our code? For a property, we can declare it to be weak simply by putting the weak keyword before the var, just like we do with other annotations like @IBOutlet. But for us, the problem is the self that is captured by the closure, so it’s not clear where we declare it to be weak.

The solution is that the closure syntax also allows us to provide a closure list, indicating the memory-management for variables being captured by the closure. This list appears in square braces, prior to the closure’s parameters, like this:

 { [​weak​ capturedVar1, ...] paramName1, ... -> returnType ​in​ code... }

So combining this fix, and the trailing closure syntax mentioned at the beginning of this section, here’s the final version of how we create the closure and pass it to player:

 playerPeriodicObserver =
  player?.addPeriodicTimeObserver(forInterval: interval,
  queue: ​nil​)
  { [​weak​ ​self​] currentTime ​in
 self​?.updateTimeLabel(currentTime)
 }

Notice that inside the closure, our reference to self now has the optional-chaining operator (?). That’s because making self a weak variable necessarily forces it to become an optional—the main selling point of a weak variable is that it can become nil if it has to, and “can become nil” is the very definition of an optional.

And with this change, our memory leak is now fixed. It wouldn’t have caused any trouble in the single-screen app we have now, but later on when we create a new player screen each time we play an episode, we would be piling up these scenes in memory, one after another, with no way of freeing them up…possibly leading to slow-downs or even a crash.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset