Memory Management in Objective-C
vom 29. August 2010Die 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:

Wird der Garbage Collector ausgestellt, ist der Programmier 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.
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):

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.
del.icio.us,
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
Kommentare
Jaja, Seele verkauft ;-)
Zum einen frag ich mich, wenn ich das so lese, ob Objective-C wirklich nur mit reference-counting arbeitet. Gerade wegen dem Problem von zyklischen Referenzen setzt ja zum Beispiel die JVM auf markieren im Graphen. Alleiniges reference-counting reicht eben nicht.
Was ich etwas ungünstig finde ist das anscheinend manuelle Zählen der Referenzen. Dann kann ich auch gleich mich um das korrekte löschen der Objekte kümmern. Das ist auch nicht so furchtbar kompliziert wenn man ein paar grundsätzliche Regeln beachtet.
Das mit dem AutoreleasePool verstehe ich nicht ganz. Wird dann alles was zwischen dem initialisieren des Pools und dem dazugehörigen release alloziert wird von dem Pool verwaltet? Wie ist das wenn ich mehrere Threads habe? Dann ist der Ablauf zwischen einem init und einem release ja nicht mehr linear? Werden dann mehrere AutoreleasePool's verwendet? Laufen dann neben den 10 Threads nochmal 10 Threads welche die ersten zehn überwachen und ab und zu aufräumen? Lässt sich das aufräumen beeinflussen?
Was passiert wenn ich beim compile den GC deaktiviere aber einen AutoreleasePool verwende? Ist dann jeder alloc gleichzeitig ein leak?
PS: Dein Flattr ist deaktiviert? Doch nicht so toll oder eher mit der Zeit zu teuer?
Was das Reference-Counting angeht: Ich bin da noch nicht all zu tief drin, allerdings habe ich bis jetzt keine anderen Mechanismen gefunden. Beim Multi-Threading sieht es denke ich mit der Speicherverwaltung auch ganz anders aus. Ich habe gerade etwas nachgeschaut: Es sieht so aus als würde das über "atomic" Properties gelöst, wodurch dann der Zugriff sequenziell abgearbeitet wird. So ganz habe ich das mit dem Properties noc hnicht durchdrungen, wenn ich mehr weiß, werde ich wieder darüber bloggen :)
Trotzdem vielen Dank für die interessanten Denkanstöße! (Der AutoreleasePool hat nichts mit dem automatischen GC zu tun, er ist nur ein "bequemes Feature" wenn man den Speicher selbst verwalten will.
@Flattr: Ja, das hat sich nicht wirklich gelohnt und ich habe ehrlich gesagt auch selten bei anderen geklickt.
Seit Objective-C 2.0 gibt es eine einfache Möglichkeit zur Deklaration und Implementierung der getter und setter Methoden der Instanzvariablen. Das ganze erfolgt mit Hilfe von declared properties, die unbedingt verstanden werden sollten.
Die Spezifikation der Zugriffsmethoden erfolgt mittels Attribute, die nach dem Schlüsselwort @property in runden Klammern umschlossen werden. Diese lassen sich hinsichtlich folgender Aspekte unterteilen:
1) Schreibschutz: Mit Hilfe des Attributes readwrite wird sowohl die setter als auch die getter Methode einer Instanzvariable generiert. Wenn jedoch nur die getter Methode verfügbar und somit die Variable schreibgeschützt sein soll, kann dies mit dem Attribut readonly erreicht werden.
2) Speicherverwaltung: Für die Implementierung einer setter Methode stehen die Attribute assign, retain und copy zur Verfügung. Die Verwendung des assign Attributes bewirkt, dass der übergebene Wert der Instanzvariable einfach zugewiesen wird. Bei einem int wird der Wert des Integers zugewiesen, bei einem Zeiger jedoch eine Adresse. Das retain Attribut bewirkt, dass zunächst der Referenzzähler des Objekts, auf das die Instanzvariable momentan zeigt, dekrementiert wird. Anschließend wird die Adresse des übergebenen Objekts der Instanzvariable zugewiesen und zuletzt dessen Referenzzähler inkrementiert. Bei der Verwendung des copy Attributes wird im Speicher eine Kopie des übergebenen Objekts erzeugt und der Instanzvariable zugewiesen. Der Referenzzähler des alten Objekts wird aber davor dekrementiert.
3) Atomarität: Standardmäßig ist eine Zugriffsmethode atomic, wobei dieser Schlüsselwort nicht explizit existiert. Eine Methode sollte als atomic deklariert sein, wenn die getter Methode thread-safe sein soll. Ansonsten kann eine Methode auch als nonatomic deklariert werden, was bei Variablen, auf die der Zugriff nur im Main Thread erfolgt, sinnvoll ist. (Wesentlich mehr Informationen findest du in der Dokumentation von Apple.)
4) Methodenname: Es besteht auch die Möglichkeit den Namen einer Zugriffsmethode zu ändern. Üblicherweise lautet der Name der getter Methode nameDerVariable und die der setter Methode setNameDerVariable:. In manchen Situationen macht es aber Sinn, den Namen einer Methode zu ändern. Die getter Methode bspw. der Variable cancelled kann von cancelled in isCancelled umbenannt werden, was bei einer Methode, die als Antwort YES oder NO zurückgibt, sinnvoll ist.
Ich hoffe, dass dir das beim Verstehen weiterhilft.
PS: iOS (früher iPhone OS) verfügt über keinen GC, doch das Mac OS X schon. Fürs iPhone Development spielt dieser Mechanismus also eine sehr große Rolle. Wenn dein Programmcode Speicherlecks verursacht und du keinen Speicher freigeben kannst, falls bspw. das OS dich dazu auffordert, hast du ein dickes Problem. Aber mit Instruments findet man alle Lecks. Auch jene, die von dem SDK verursacht werden. :-D
Viele Grüße
Selçuk Sinan
Danke für die ausführliche Beschreibung. Die Properties hab ich mir noch nicht genauer angeschaut, genauso wie die iPhone-Entwicklung. Die Sache mit der Atomarität und dem Multi-threading muss ich mir nochmal gesondert anschauen, danke!