Why is there memory leak / retain cycle after the action closure of UIDynamicBehavior is called?

927 views Asked by At

The idea of the code here is to remove a view (self.mv) when it has been animated out of screen by a UIDynamicAnimator.

The code below is based on examples from chapter 4 of the book Programming iOS 12 by Matt Neuburg. The author says both the behavior and the view (self.mv in the code) won't be de-allocated. But he didn't elaborate extensively on this.

My questions are:

  1. Who still retains the behavior after self.anim.removeAllBehaviors()?

  2. Who still retains self.mv?

I used Instruments, but I don't quite understand the output. Does it mean the animator retains it? But there are only green checkmarks.

Instruments screen shot

With the "Debug Memory Graph" tool in XCode, I saw UIGravityBehavior is still retained by the animator even after self.anim.removeAllBehaviors() is called.

Debug Memory Graph

class MyView : UIView {
    deinit {
        print("dddddddd")
    }
}

class ViewController: UIViewController {

    var anim : UIDynamicAnimator!

    weak var mv : MyView?

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        let v = MyView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))

        v.backgroundColor = .red

        self.view.addSubview(v)

        self.mv = v

        let grav = UIGravityBehavior()

        self.anim = UIDynamicAnimator(referenceView: self.view)

        self.anim.addBehavior(grav)
        grav.action = {
            let items = self.anim.views(in: self.view.bounds)

            let idx = items.firstIndex(of: self.mv!)

            if idx == nil {
                self.anim.removeAllBehaviors()
                self.mv!.removeFromSuperview()
                // self.anim = nil // without this, the `MyView` is not deallocated.
            }
        }

        grav.addItem(v)

    }
}
2

There are 2 answers

3
DavidPhillipOster On

self owns anim which owns grav which owns the action block which retains self.

That's a retain loop, so the reference count of self will never be decremented to zero, so self will leak.

You need to do the weakself dance to fix this.

{[weak self] in
  if let strongSelf = self {
    let items = strongSelf.anim.views(in: strongSelf.view.bounds)
    ...
1
DavidPhillipOster On

You have:

var anim : UIDynamicAnimator!

if you make it:

var anim : UIDynamicAnimator?

and nil it in the callback when you are done animating, that should fix your extra retain:

        if idx == nil {
            self.anim?.removeAllBehaviors()
            self.mv!.removeFromSuperview()
            self.anim = nil // without this, the `MyView` is not deallocated.
        }