Swift e Timer, attenzione ai retain cycle

La creazione di un timer è un’operazione che, prima o dopo, qualsiasi sviluppatore iOS si trova a fare.

C’è una piccola insidia, tuttavia, nella gestione della memoria che può portare ad avere dei retain cycle all’interno della classe/controller in cui vi trovate a definire il timer.

La documentazione di Apple definisce così la classe Timer (NSTimer per i nostalgici):

Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

Un timer in automatico inserito nel run loop in cui viene istanziato (generalmente il main thread), questo però comporta un’insidia non da poco. L’ownership non è (solo) della classe in cui andate a definirlo, ma bisogna agire manualmente per invalidarlo e annullare il suo riferimento.

Proviamo a spiegare il tutto con un semplice esempio.

Creiamo un timer ripetuto

Supponiamo di avere un semplicissimo UIViewController che istanzia un timer:

import UIKit

class SampleViewController: UIViewController {

    private var timer: Timer?

    // MARK: - Init

    convenience init() {
        self.init(nibName: nil, bundle: nil)
        view.backgroundColor = .white
    }

    deinit {
        print("[SampleViewController] deinit called")
    }

    // MARK: - View Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()

        timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerFired(_:)), userInfo: nil, repeats: true)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        timer = nil
    }

    // MARK: - Private

    @objc private func timerFired(_ sender: Timer) {
        print("[SampleViewController] timer fired")
    }
}

Da un altro controller istanziamo SampleViewController e lo dismettiamo con un semplice drag verso il basso.

let controller = SampleViewController()
present(controller, animated: true)

Retain cycle in agguato

Ci sembrerebbe di aver fatto tutto in maniera corretta, ma guardando la console notiamo che il deinit non viene chiamato dismettendo il controller:

[SampleViewController] timer fired
[SampleViewController] timer fired
[SampleViewController] timer fired

Analizzando il memory graph, notiamo effettivamente che il controller è ancora presente in menoria, mantenuto in vita da _NSCFTimer:

La chiave per risolvere il problema sta nel chiamare il metodo invalidate del timer:

This method is the only way to remove a timer from an RunLoop object. The RunLoopobject removes its strong reference to the timer, either just before the invalidate()method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

In questo caso la documentazione è abbastanza chiara: il timer viene rimosso dal relativo RunLoop e, cosa importante, viene rimosso il riferimento strong all’oggetto target del timer.

Andiamo quindi a modificare il nostro viewWillDisappear:

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        timer?.invalidate()
        timer = nil
    }

Boom! Il nostro controller viene ora deallocato in maniera corretta:

[SampleViewController] timer fired
[SampleViewController] timer fired
[SampleViewController] timer fired
[SampleViewController] deinit called

Conclusioni

Nonostante la documentazione si chiara a riguardo, è un problema che trovo spesso nei progetti su cui mi trovo a lavorare e che, ammetto, mi capita di fare.

Piccola nota, il tutto è valido per i timer ripetitivi, ovvero che hanno parametro repeat: true. Per i timer che vengono eseguiti una sola volta, l’ownership viene rilasciata automaticamente.

Spero di non scordarmelo più dopo averci fatto anche un post a riguardo 😎

Ingegnere informatico e sviluppatore freelance, mi occupo da anni di sviluppo per iOS (ma non solo). Dal 2008 scrivo su questo piccolo blog (con qualche lunga pausa), in cui parlo di programmazione e di qualsiasi altra cosa che mi diverta.

Leave a reply:

Your email address will not be published.

Site Footer