3

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)

    }
}
LShi
  • 1,500
  • 16
  • 29

2 Answers2

1

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.
        }
DavidPhillipOster
  • 4,195
  • 1
  • 19
  • 17
  • Yes. It’s just a little counterintuitive that removeAllBehaviors() doesn’t release the behavior. – LShi Feb 07 '19 at 02:45
0

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)
    ...
DavidPhillipOster
  • 4,195
  • 1
  • 19
  • 17
  • I ran the code with [weak self]. It doesn't help. In my understanding, the cycle should be broken when `self.anim.removeAllBehaviors();self.mv!.removeFromSuperview()` is executed. – LShi Feb 03 '19 at 02:03
  • 1
    Well then, I was probably wrong about that. Xcode has, next to the (step over) (step out) (step in) icons near the bottom of Xcode's main window while a program is being debugged, an icon like: > with little circles at the vertices. This is the memory viewer. In my experience, if you aim the memory viewer at an object that is leaking, it will show you the retain cycle as a loop of objects that all retain each other. Did you try that? – DavidPhillipOster Feb 04 '19 at 06:03
  • Thank you very much! I have tried the “Debug Memory Graph” tool that you mentioned. There were no cycle. The behavior, which retains the view, is just retained by the animator even after the behavior has been removed. I have updated the question earlier. – LShi Feb 04 '19 at 06:13