Sneaky Reference Cycles in Swift Instance Methods

What if I told you the Swift compiler is adding strong reference cycles into your code without telling you, and laughing behind your back? Why, you might think I'm a conspiracy nut, tell your friends I'm a madman and run me out of town!

Which is all well and good, but then you'll never find those reference cycles, and eventually, not wanting to be seen with a notorious memory-leaker, your friends will all abandon you, leaving you alone and surrounded by the ghosts of objects you can never free.

Perhaps, then, you should hear me out. I swear I can prove everything (well, except for the laughing part). Take this perfectly safe code, for example. The defaultAction doesn't capture anything, so we shouldn't have any memory issues here, right?

class Actor {
  var action: () -> Void

  init() {
    action = {}
  }

  func defaultAction() -> Void {
    print("Default action")
  }

  func setDefaultAction() -> Void {
    action = defaultAction
  }
}

let someActor = Actor()
someActor.setDefaultAction()
someActor.action()

Nope, That's a Reference Cycle

Alas, there is a strong reference cycle right underneath our noses! How can this be? Well, it turns out that in Swift, instance methods are actually implemented as type methods that take in an instance as the first argument. The reason you never see that first argument is that these methods are curried. This basically means that you can call a function with only some of its arguments, and it will return another function to you. This function will be equivalent to the original, except with the arguments you specified already set, and removed from the parameter list.

How does this affect our reference cycle? Well, it means that

someActor.defaultAction()

is actually this:

Actor.defaultAction(someActor)()

Note the argument in the first parentheses; that's the curried one which is added automatically under-the-hood. So when someActor saves this method in its action variable, it is creating a strong reference to a closure that it has actually just captured itself in! It's a sneaky trick, since none of this is visible in your own code.

A Reusable Solution

Now that we know where and why the reference cycle is occurring, we can apply the normal technique to prevent it. Ian Keen had a trick up his sleeve just for this occasion: a weakify function.

func weakify<A: AnyObject, B>(obj: A, target: ((A) -> (B) -> Void)?) -> ((B) -> Void) {
  return { [weak obj] value in
    guard let obj = obj else { return }
    target?(obj)(value)
  }
}

This will turn our curried self-reference into a weak one. Let's use it to fix our Actor class.

class SafeActor {
  var action: () -> Void

  init() {
    action = {}
  }

  func defaultAction() -> Void {
    print("Default action")
  }

  func setDefaultAction() -> Void {
    action = weakify(obj: self, target: SafeActor.defaultAction)
  }
}

let someActor = SafeActor()
someActor.setDefaultAction()
someActor.action()

I've put up a gist here that shows safe and unsafe examples side-by-side, so you can see proof that the unsafe one is not being deallocated. Now you can outsmart the Swift compiler, and use its own tricks against it!

Leave A Comment

Your email address will not be published. Required fields are marked *