Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

28 August, 2010

Memory Management in Objective-C

Programmiersprachen

Die meisten modernen Programmiersprachen haben eine dynamische Speicherverwaltung, die von der VM oder dem Interpreter gesteuert wird. Manuelle Speicherreservierung (malloc) und Freigabe des nicht mehr verwendeten Speichers (free) sieht man heutzutage fast nur noch in historischen Projekten und an Stellen an denen der Speicher knapp ist.

In Objective-C wird dem Programmierer die Wahl gelassen, ob er den Speicher selbst verwalten oder es dem Garbage Collector überlassen will. Die Programme können sogar so geschrieben werden, dass sie beide Modis unterstützen um Abwärtskompatibel zu bleiben. (Bspw. um eine Library auf dem iPhone und in einer Desktop-Anwendung zu nutzen)

Die Einstellung wird in XCode mit einem Doppelklick auf das Build-Target erreicht:

Garbage Collector Schalter

Wird der Garbage Collector ausgestellt, ist der Programmierer selbst für die Speicherallokation zuständig. Objective-C ähnelt etwas dem der Java VM. Jedes Objekt hält intern einen Referenzzähler (retain-count genannt), der angibt, wie viele andere Objekte dieses Objekt benötigen. Sinkt dieser Zähler auf 0, wird das Objekt aus dem Speicher entfernt.

Um den retain-counter zu inkrementieren, muss eine retain-Nachricht an das Objekt gesendet werden. Diese kann von allen Objekten entgegengenommen werden, die von NSObject abgeleitet wurden, also so gut wie alle. Zum Dekrementieren wird release verwendet. Ein kleines Beispiel:

NSArray *users = [[NSArray alloc] init]; // retain-count = 1
// Do some stuff
[users release] // retain-count = 0

Hierbei gibt es zwei wichtige Faustregeln (beide aus Memory Management in Cocoa entnommen):

You should never release an object that you have not retained or created.

und

Make sure that there are as many release or autorelease messages sent to objects as there are alloc, copy, mutableCopy, or retain messages sent. In other words, make sure that the code you write is balanced.

Interessant wird es, sobald Objekte an andere Objekte gesendet werden. Hier hat das ursprüngliche Objekt keine Kontrolle mehr darüber, was mit dem Objekt passiert, und kann es deshalb auch nicht mehr sauber aus dem Speicher räumen.

Hier kommt der Autorelease Pool ins Spiel: In ihm lassen sich zur Speicherfreigabe vorgesehene Objekte vormerken, um dann an einer geeigneten Stelle die release-Nachrichten zu verschicken. Dies ist aufs Erste Mal nicht so ganz einleuchtend, deshalb wieder ein Beispiel:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSNumber *fourtytwo = [NSNumber numberWithFloat:42];
// Some stuff ...
[pool release];

In der ersten Zeile wird der Autorelease Pool erstellt. Er enthält alle Referenzen zu den Objekten, die aus dem Speicher entfernt werden können. Auch diejenigen, die von anderen Methoden erzeugt werden. In diesem Fall ist das das NSNumber-Objekt, das nicht über den Standard-Konstruktor init erzeugt wurde, also nicht in der aktuellen Methode sondern an einem anderen Ort. Da wir in unserer aktuellen Methode nicht wissen, ob das NSNumber-Objekt noch anderweitig verwendet wird (da wir es ja nicht direkt erstellt haben), können wir es auch nicht mit [fourtytwo release] bedenkenlos aus dem Ram löschen. Dafür ist der Autorelease Pool zuständig. Sobald das NSNumber-Objekt an keiner Stelle mehr verwendet wird, landet es in einem AutoreleasePool. Rufen wir dann [pool release] auf, wird an allen enthaltenen Objekten die release-Nachricht gesendet, welche einen release-count von 0 vorweisen.

Wenn der Speicher manuell verwaltet wird, treten meist ziemlich hässliche Fehler auf, die sehr schwer zu finden sind. Speicherlecks sind tückische Biester, die sporadisch auftauchen und nicht selten lange unentdeckt bleiben. Um dem entgegenzuwirken und das Debugging nach einem SegFault etwas einfacher zu machen, können diese beiden Optionen gesetzt werden (Doppelklick aufs Executable in XCode):

Speicherlecks besser finden

Damit bricht das Programm kontrolliert ab und XCode springt direkt in die Zeile in der versucht wird auf ein Objekt zuzugreifen, dass zuvor schon aus dem Speicher entfernt wurde.