8

Say you have an everyday CADisplayLink

class Test: UIViewController {

    private var _ca : CADisplayLink?

    @IBAction func frames() {

        _ca?.invalidate()
        _ca = nil

        _ca = CADisplayLink(
            target: self,
            selector: #selector(_step))
        _ca?.add(to: .main, forMode: .commonModes)
    }

    @objc func _step() {

        let s = Date().timeIntervalSince1970
        someAnime.seconds = CGFloat(s)
    }

Eventually the view controller is dismissed.

Does anyone really definitively know,

do you have to explicitly call .invalidate() (and indeed nil _ca) when the view controller is dismissed?

(So perhaps in deinit, or viewWillDisappear, or whatever you prefer.)

The documentation is worthless, and I'm not smart enough to be able to look in to the source. I've never found anyone who truly, definitively, knows the answer to this question.

Do you have to explicitly invalidate, will it be retained and keep running if the VC goes away?

Fattie
  • 27,874
  • 70
  • 431
  • 719

2 Answers2

17

A run loop keeps strong references to any display links that are added to it. See add(to:forMode:) documentation:

The run loop retains the display link. To remove the display link from all run loops, send an invalidate() message to the display link.

And a display link keeps strong reference to its target. See invalidate() documentation:

Removing the display link from all run loop modes causes it to be released by the run loop. The display link also releases the target.

So, you definitely have to invalidate(). And if you're using self as the target of the display link, you cannot do this in deinit (because the CADisplayLink keeps a strong reference to its target).


A common pattern if doing this within a view controller is to set up the display link in viewDidAppear and remove it in viewDidDisappear.

For example:

private weak var displayLink: CADisplayLink?

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    startDisplayLink()
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    stopDisplayLink()
}

private func startDisplayLink() {
    stopDisplayLink()  // stop previous display link if one happens to be running

    let link = CADisplayLink(target: self, selector: #selector(handle(displayLink:)))
    link.add(to: .main, forMode: .commonModes)
    displayLink = link
}

private func stopDisplayLink() {
    displayLink?.invalidate()
}

@objc func handle(displayLink: CADisplayLink) {
    // do something
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
8

Method definition of invalidate():

Removing the display link from all run loop modes causes it to be released by the run loop. The display link also releases the target.

For me this means that displaylink holds the target and run loop holds DispayLink.

Also, according to this link I found, it looks like its rather important to call invalidate() for cleanup of CADisplayLink.

We can actually validate using XCode's wonderful Memory graph debugger:

I have created a test project where a DetailViewController is being pushed on a navigation stack.

class DetailViewController: UIViewController {

    private var displayLink : CADisplayLink?

    override func viewDidAppear() {
        super.viewDidAppear()

        startDisplayLink()
    }

    func startDisplayLink() {
        startTime = CACurrentMediaTime()

        displayLink = CADisplayLink(target: self, 
                                  selector: #selector(displayLinkDidFire))

        displayLink?.add(to: .main, forMode: .commonModes)
    }
}

This initiates CADispalyLink when view gets appeared.

enter image description here

If we check the memory graph, we can see that DetailViewController is still in the memory and CADisplayLink holds its reference. Also, the DetailViewController holds the reference to CADisplayLink.

enter image description here enter image description here

If we now call invalidate() on viewDidDisappear() and check the memory graph again, we can see that the DetailViewController has been deallocated successfully.

This to me suggests that invalidate is a very important method in CADisplayLink and should be called to dealloc CADisplayLink to prevent retain cycles and memory leaks. enter image description here

Puneet Sharma
  • 9,369
  • 1
  • 27
  • 33
  • Ah, you're saying this sentence - **"Removing the display link from all run loop modes causes it to be released by the run loop."** implies that: **The run loop DOES hold it strongly.** That is really interesting. – Fattie Nov 18 '17 at 18:15
  • 4
    If you’re going to remove display link in `viewDidDisappear`, make sure to add it in `viewDidAppear`, not in `viewDidLoad`, to keep them balanced. For example, if you present a subsequent view controller, the current one can disappear (stopping the display link), but you presumably want a display link going when you return back to this current view controller. – Rob Nov 19 '17 at 12:13