25. Juli 2018

Speicherverwaltung mit ARC

Referenzzählung

Die Speicherverwaltung in iOS erfolgt mittels Referenzzählung. Für jedes Objekt wird die Anzahl der Referenzen, die auf das Objekt zeigen, gezählt. Hält ein Objekt eine Referenz auf ein anderes Objekt, so muss es dieses retainen, wodurch der Referenzzähler des referenzierten Objektes inkrementiert wird:

Fällt eine solche Referenz weg, muss das referenzierte Objekt released und damit der Referenzzähler des referenzierten Objektes dekrementiert werden:

Wird die letzte Referenz auf ein Objekt freigegeben, fällt der Referenzzähler auf 0 und das Objekt wird zerstört:

Automatische Referenzzählung (ARC)

Vor der Einführung von ARC: Automatic Reference Counting in iOS 5 musste der Aufruf von retain und release manuell geschehen, wenn ein Objekt referenziert bzw. die Referenz gelöst wurde. ARC vereinfacht die Speicherverwaltung mittels Referenzzählung, indem diese Aufrufe automatisch vom Compiler generiert werden.

Dazu wird jede Referenz auf ein Objekt standardmäßig als starke „strong“-Referenz betrachtet. Dies bewirkt, dass das referenzierte Objekt automatisch retained und released wird.

Objektzyklen

Bei der Speicherverwaltung mittels Referenzzählung ist darauf zu achten, das der Objektgraph frei von Zyklen gehalten werden muss. Retain-Zyklen zwischen Objekten verursachen Speicherlecks. Das bedeutet, Objekte halten sich gegenseitig und werden daher nie freigegeben:

class Library {
    var books = [Book]()
}

class Book {
    var library : Library?
}

Schwache Referenzen

Solche Zyklen müssen mit schwachen „weak“ Referenzen unterbrochen werden. Jede Referenz auf ein Objekt ist standardmäßig strong, und dies bedeutet, dass es das referenzierte Objekt besitzt. Das heißt, der Besitzstand von Objekten muss eindeutig geklärt sein und Objekte dürfen sich, auch nicht mittelbar, gegenseitig besitzen.

Schwache Referenzen halten ein Objekt, ohne es zu retainen und werden automatisch auf nil gesetzt, wenn das Objekt freigegeben wird:

class Library {
    var books = [Book]()
}

class Book {
    weak var library : Library?
}

Objektzyklen bei Blöcken

Da in Blöcken referenzierte Objekte an das Block-Objekt gebunden werden, führt die Verwendung von Referenzen in Blöcken zu Retain-Zyklen, sofern ein Objekt referenziert wird, welches wiederum das Block-Objekt hält:

class Library { 
    // ...
    var completionHandler : (() -> ())? 
} 
 
let library = Library() 
library.completionHandler = { NSLog("%@", "\(library.books.count) books downloaded") }

Hier ist auf die Verwendung einer weak-self-Referenz zu achten, welche in Swift mit [weak self] vor den Block-Parametern deklariert werden kann:

library.completionHandler = { [weak self] in NSLog("%@", "\( library?.books.count) books downloaded") }

Speicher-Leaks finden

Mit dem Memory Graph Debugger können zur Laufzeit Leak-Objekte ermittelt werden, die nicht mehr referenziert werden:

Debug Memory Graph

Bei dieser Auswertung handelt es sich um eine Heuristik: Nicht alle Lecks werden gefunden und nicht jedes angezeigte Speicherleck ist auch tatsächlich eines - es ist jedoch ein praktisches Tool um gravierende Speicherprobleme schnell zu finden.

Speicherwarnungen

Wenn der zur Verfügung stehende Speicher zur Neige geht, erhält die App eine Speicherwarnung. Insbesondere ViewController sollten in ihrer didReceiveMemoryWarning nicht mehr benötigte Objekte freigeben, zum Beispiel indem Caches geleert werden, und ggf. ihr View freigeben, sofern dies gerade nicht angezeigt wird:

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()

    self.someLargeObject = nil
    self.someCache.removeAllObjects()

    if self.isViewLoaded && self.view.window == nil {
        NSLog("didReceiveMemoryWarning: clearing view");
        self.view = nil
    }
}

Das AppDelegate kann in der applicationDidReceiveMemoryWarning:-Methode auf Speicherknappheit reagieren:

class AppDelegate: UIResponder, UIApplicationDelegate {
    func applicationDidReceiveMemoryWarning(application: UIApplication) {
        self.someCache.removeAllObjects()
    }
}

Dies kann ein Beenden einer aktiven oder im Hintergrund pausierten App wegen Speicherknappheit abwenden. Speicherwarnungen können im Simulator mittels Hardware » Simulate Memory Warning getestet werden.

Tutorial 1: Speicherlecks mit dem Memory Graph Debugger finden

  1. Laden, entpacken und öffnen Sie das MemoryExample-Beispielprojekt. Dies ist ein Beispielprojekt, in dem massenhaft Objekte erzeugt und aufgrund von Referenzzyklen nie wieder freigegeben werden.

  2. Öffnen Sie MemoryExampleViewController.swift und deklarieren Sie für die Library und Book-Klassen einen Deinitializer, der eine Meldung bei der Zerstörung der Objekte ausgibt:

    class Library {
        var books = [Book]()
    
        deinit {
        print("\(self) deinit")
    }
    }
    
    class Book {
        var library : Library?
    
        deinit {
        print("\(self) deinit")
    }
    
    }
  3. Starten Sie die App und betätigen Sie den Button. Prüfen Sie auf der Konsole, dass die Objekte korrekt freigegeben werden.

  4. Passen Sie die leakExample-Methode so an, dass sich die Book und Library-Objekte gegenseitig referenzieren:

    @IBAction func leakExample(_ sender: AnyObject) {
    
        let library = Library()
    
        for _ in 1..<1000 {
            let book = Book()
            book.library = library
            library.books.append(book)
        }
    
    }
  5. Starten Sie nun die App und prüfen Sie, dass keine Log-Ausgaben mehr erfolgen, obwohl die Objekte erzeugt werden: Die gegenseitige Referenz führt dazu, dass sich die Objekte bezüglich der Referenzzählung gegenseitig halten.

  6. Lassen Sie im Debug Navigator den Speicherverbrauch anzeigen: Mit jedem Tap auf den Button wird mehr Speicher verbraucht, der nicht wieder freigegeben wird:

    Speicherverbrauch im Debugger
  7. Passen Sie die leakExample-Methode so an, dass nur 3 Objekte statt 1000 erzeugt werden (der im folgenden Schritt gezeigte Memory Graph Debugger ist nicht auf solche Mengen Objekte vorbereitet):

    @IBAction func leakExample(_ sender: AnyObject) {
    
            let library = Library()
    
            for _ in 1..<3 {
                let book = Book()
                book.library = library
                library.books.append(book)
            }
    
        }
  8. Starten Sie die App und lösen Sie das Speicherleck aus, indem Sie den Button der App mehrfach betätigen. Verwenden Sie den Memory Graph Debugger um das durch die nicht freigegebenen Objekte ausgelöste Leak zu analysieren:

    Debug Memory Graph
  9. Beheben Sie das Speicherleck, indem Sie die Referenz von Book zu Library als weak deklarieren: Die Bibliothek besitzt die Buch-Objekte, aber die Buch-Objekte besitzen nicht die Bibliothek:

    class Book : NSObject {
        weak var library : Library?
        // ...
    }
  10. Starten Sie die App erneut und prüfen Sie über die Log-Ausgaben, dass die Objekte nun freigeben werden:

    Deallokierte Objekte in der Log-Ausgabe

Tutorial 2: Freigabe von UIViewController-Objekten loggen

Es empfiehlt sich mit einem Symbol Breakpoint im Debugger die Freigabe von UIViewController-Objekten zu loggen, um so Speicherverwaltungsprobleme frühzeitig zu erkennen: Werden UIViewController-Objekte nicht freigegeben, wenn sie nicht mehr benötigt werden, führt dies schnell zu Speicherknappheit.

  1. Erstellen Sie im Breakpoint navigator mit Add Symbolic Breakpoint einen neuen Haltepunkt um die Deallokation von UIViewController-Objekten auf der Konsole zu loggen:

    Xcode Debugger: Symbolic Breakpoint

    Geben Sie „[UIViewController dealloc]“ als Symbol an, aktivieren Sie die Automatically continue-Option und fügen Sie folgenden Debugger Command hinzu, um den Namen der Klasse zu loggen:

    expr -- (void) printf("[%s %s]\n", (char *)object_getClassName($arg1), $arg2)