Zurück zur Übersicht

Speicherverwaltung

Einführung

Anders als in anderen Programmiersprachen ist in Objective-C die Erzeugung von Instanzobjekten in das Framework verlegt, da die Instanzobjekt-Erzeugung nicht mittels new und delete, sondern durch Methoden des Frameworks erfolgt.

Klassenobjekte werden hingegen durch das Framework automatisch erzeugt. Sie sind beim Programmstart als globale Singletons vorhanden.

Der nachfolgende Artikel beschreibt die Art und Weise, wie in Objective-C vorteilhaft der Speicher verwaltet wird. Er erläutert zunächst Grundkonzepte. Danach folgen How-Tos für den üblichen Gebrauch. Es ist nicht erforderlich, aber sinnvoll, für die How-Tos, die Grundkonzepte verstanden zu haben.

Konzepte

Die wesentlichen Konzepte der Cocoa-Speicherverwaltung lassen sich mit Reference Counting (RC), Autorelease-Pools (ARP) und Factories bezeichnen.

Teilweise wird vertreten, dass die Speicherverwaltung eine Garbage Collection (GC) darstelle, sich jedenfalls so auswirke. Hierbei wird jedoch übersehen, dass anders als bei der GC der Programmierer jederzeit die Möglichkeit hat, für die Freigabe von Speicher zu sorgen. Auch ist der Zeitpunkt des Aufräumens des Autorelease Pools genau definiert.

Reference Counting

Das Prinzip

Beim RC wird zu jedem Instanzobjekt ein Referenz-Zähler gehalten, der angibt, wieviele Zeiger ein Instanzobjekt referenzieren. Ein Instanzobjekt wird grundsätzlich mit einem Zähler von 1 erzeugt. Da in der Cocoa-Speicherverwaltung das RC nicht wie bei smart Pointers transparent implementiert ist, muss eine Referenz auf ein Instanzobjekt mittels <tt>-retain</tt>-Nachricht an dieses ausdrücklich angegeben werden. Die Aufgabe einer Referenz erfolgt umgekehrt ausdrücklich durch eine <tt>-release</tt>-Nachricht.

Ein Instanzobjekt wird endgültig aus dem Speicher entfernt, wenn der Referenz-Zähler 0 erreicht.

Beispielhaft lässt sich der Lebensweg eines Instanzobjektes wie folgt skizzieren.

  • Ein Benutzer erzeugt das Instanzobjekt und referenziert es selbst. Bei der Erzeugung wird automatisch der Referenz-Zähler auf 1 gesetzt.
  • Ein weiterer Benutzer referenziert das Instanzobjekt. Hierdurch wird der Referenz-Zähler des benutzten Instanzobjektes auf 2 erhöht.
  • Einer der Benutzer benötigt das Instanzobjekt nicht mehr und gibt es frei. Der Referenz-Zähler des benutzten Instanzobjektes vermindert sich wieder auf 1.
  • Auch der andere Benutzer hat kein Interesse mehr an dem Instanzobjekt und gibt es ebenfalls frei. Der Referenz-Zähler vermindert sich auf 0 und das Instanzobjekt wird freigegeben.

Hierbei soll etwas verdeutlicht werden, was anfänglich vielen Programmierern Schwierigkeiten bereitet, vor allem, wennn sie von C/C stammen: Grundsätzlich dient die Einführung eines RC dazu, Objekterzeugung und Objektreferezierung zu trennen. Wer ein Objekt benötigt, erzeugt es sich. Jeder, gleichgültig, ob er das Objekt erzeugt hat, muss es referenzieren, wenn er es dauerhaft benutzen will. Jeder muss die Referenzierung freigeben, wenn er kein Interesse mehr daran hat. Die Objektvernichtung erfolgt hingegen nicht zentral. Ein Objekt wird vom System vernichtet, wenn es keiner mehr referenziert. Man kann also nicht sagen, wer genau ein Objekt wann genau vernichtet. Es wird vernichtet, wenn es nicht mehr benötigt wird. Daher gibt es auch keinen dezidierten Befehl zur Vernichtung. In obiger Auflistung wird in Schritt 3 und 4 jeweils das gleiche vom Programm gemacht: Das benutzte Objekt wird freigegeben. In Schritt 3 führt dies nicht zur Objektvernichtung, in Schritt 4 sehr wohl. Die Objektvernichtung ist also für den Programmierer transparent.

Wir müssen also eine Referenz anlegen, wenn wir ein fremdes Objekt nutzen wollen. Wir müssen sie aufgeben, wenn wir es nicht mehr benutzen wollen oder wenn unser Objekt selbst gelöscht wird. Dies geschieht im dealloc.

Ohne weitere Änderung in der Source würde daher die in der Graphik abgebildete Person-Instanz auch dann weiterleben, wenn sie aus dem NSArray entfernt wird. Denn es besteht weiterhin eine Referenz darauf, nämlich über den owner-Member der Model-Instanz. Dies kann gewollt sein, aber auch zu Problemen führen, etwa wenn die Software anderweitig davon ausgeht, dass das mit owner referenzierte Person-Objekt sich in dem NSArray befindet.

Die Auflösung unserer <tt>Model</tt>-Instanz schauen wir uns einmal an: Wir nehmen an, dass unser Model irgendwann gelöscht werden soll, also die letzte Referenz weggenommen wurde. Dadurch wid automatisch die <tt>-dealloc</tt>-Methode aufgerufen. Dies geschieht stets bevor ein Objekt gelöscht wird. Dort löschen wir wiederum die Referenzen, die das Objekt gehalten hat:

 - (void)dealloc {
     [persons release]; // löscht das Array
     [owner release]; // löscht den owner
     [super dealloc];
 }

'Hinweis': Diese Art, Referenzen mittels <tt>-retain</tt>-Nachricht unmittelbar anzulegen und <tt>-release</tt>-Nachricht unmittelbar wieder zu löschen, ist nicht gut. Wir werden später eine bessere Vorgehensweise kennenlernen, begnügen uns jetzt aber mit der einfachen Referenzierung mittels <tt>-retain</tt> und <tt>relase</tt>.

Da unser NSArray nur einmal referenziert wurde, bedeutet dies, dass sein RC auf 0 geht und auch das Arrays gelöscht wird. Auch dessen <tt>dealloc</tt>-Methode wird aufgerufen. Das Array nimmt daraufhin seinerseits alle Referenzierungen auf enthaltene Objekte zurück. Dies können wir durch folgenden Pseudocode darstellen:

 - (void)dealloc {
     foreach( element ) {
         [element release];
     }
 }

Hiermit erhält jedes Element eine <tt>release</tt>-Nachricht und vermindert seinen RC. Da es nicht weiter referenziert wird, wird es wiederum einen RC von 0 haben und gelöscht.

Halt! Ein Element des Arrays ist jedoch noch durch den Member <tt>owner</tt> der <tt>Model</tt>-Instanz referenziert. Es bleibt zunächst im Speicher, weil sein RC weiterhin 1 ist. Er hat sich nur von 2 auf 1 vermindert. Aber in der nächsten Zeile unseres <tt>dealloc</tt> wird auch der <tt>owner</tt> befreit, womit der RC dieses Elementes ebenfalls auf 0 geht. Auch das letzte, doppelt referenzierte Objekt, verlässt den Speicher.

Referenzen und Verweise

Wird auf ein Objekt lediglich verwiesen, ohne es mittels <tt>-retain</tt> ausdrücklich zu referenzieren, so erhöht dies den Referenz Counter nicht. Ein solch einfacher Verweis verhindert also nicht, dass das Objekt aus dem Speicher entfernt wird und ein dangling Pointer zurückbleibt.

In der obigen Zeichnung sind Referenzen als ausgefüllte Pfeile dargestelllt. So referenziert etwa die Instanz der Klasse <tt>Model</tt> über den Member <tt>persons</tt> eine <tt>NSArray</tt>-Instanz und über den Member <tt>title</tt> eine <tt>NSString</tt>-Instanz. Umgekehrt wird etwa die abgebildete Instanz der Klasse <tt>Person</tt> von dem <tt>persons</tt> der <tt>Model</tt>-Instanz und über die Instanz des <tt>NSArray</tt> referenziert. Der RC entspricht also der Anzahl der Spitzen der ausgefüllten Pfeile und beträgt für die abgebildete <tt>Person</tt>-Instanz 2.

Demgegenüber verweist die <tt>Model</tt>-Instanz lediglich auf die View-Instanzen (hohle Pfeile). Ein solcher Verweis erhöht den Reference Count nicht. Daher hat <tt>Model</tt>-Member <tt>preview</tt> lediglich einen RC von 1, denn der reine Verweis der <tt>Model</tt>-Instanz wird nicht mitgezählt. Es ist programmtechnisch sicherzustellen, dass die Views nicht aus dem Speicher entfernt werden, ohne dass die <tt>Model</tt>-Instanz hiervon erfährt.

Diese Unterscheidung von Referenz und Verweis sollte bedacht werden. Programmatisch sieht sie so aus:

model.h

 @interface aClass : NSObject {
     id anotherObject;
     [...]
 }
model.m
 - (void)anyMethod {
 
     // Dies ist ein Verweis
     // Es ist in der Regel keine gute Idee, den Erzeuger eines Objektes,
     // welches über einen Member gehalten wird, lediglich auf das
     // erzeugte Objekt verweisen zu lassen.
     anotherObject = [AnotherObject anotherObject];
 
     // Dies ist eine Referenz
     // Üblicherweise hält der Erzeuger eines in einem Member gehaltenen
     // Objektes eine Referenz auf das Objekt. 
     anotherObject = [[AnotherObject anotherObject] retain];
     [...]
 
     // Die Referenz wird wieder aufgegeben.
     [anotherObject release];
     [...]
 }
'Hinweis:': Es sei hier angemerkt, dass der obige vorstehende nicht günstig ist, da das <tt>-retain</tt> nicht geschlossen wird. Weiter unten werden wir eine bessere Implementierung mittels sogenannter Setter kennelernen.

Anker und Zyklen

Aus dem Gesagten lässt sich folgern, dass es ein „Anker-Objekt“ geben muss, welches andere Objekte referenziert, damit diese weiter leben können und weitere Objekte referenzieren, die damit weiterleben können usw. Im obigen Beispiel ist das App. Dies geschieht dadurch, dass ein Objekt mit dem RC 1 erzeugt wird und bis zum Programmende bestehen bleibt. Wir müssen uns hierum nicht kümmern. Vielmehr stellen uns Cocoa und das RunTime-System bestimmte Objekte zur Verfügung, auf die wir uns verlassen können. An diese Objekte sollte niemals ein -retain oder ein -release gesendet werden.

  • Klassenobjekte sind als Singletons vom Programmstart bis Programmende vorhanden.
  • Objekte, die wir in NIBs instantiiert haben, sind für die Lebensdauer des NIBs sicher vorhanden.
  • Bei Document-based-Applications sind Dokumente vorhanden bis der User sie schließt.

Insbesondere die beiden letzten Punkte führen dazu, dass NIB-Objekte und document hervorragende Ankerpunkte für unsere Software sind.

Hinweis: Das obige Beispiel soll nicht die wahre Struktur einer laufenden Applikation wiedergeben. Sie dient beispielhaft. Der orange-farbene Bereich ist derjenige, in der sich unsere Software in der Regel bewegt.

Bei dem Verhältnis von der Instanzen von Super- und Subview zeigt sich ein weiterer Effekt. Die Objekte referenzieren sich gegenseitig. Sie sind daher unbegrenzt lebensfähig, selbst dann, wenn niemand mehr auf sie referenziert. Dies könnte zu einem Memory Leak werden.

Nehmen wir an, dass der Superview der Content-View des Fensters ist. Dieser wird wiederum duch das Fenster über den Member contentView referenziert. Dies ist eine stabile Sache. Der Superview hat also einen RC von 2: einmal referenziert der <tt>contentView</tt>-Member des Fensters, das andere mal der superview-Member des Subviews.

Der Subview hat einen RC von 1, da er lediglich durch den Superview referenziert wird. Wie sieht das Auflösen aus? Geschieht dies ebenso automatisch wie oben in dem NSArray-Beispiel?

Wenn das Fenster gelöscht wird, so nimmt es die Referenz von dem Superview weg. Dies geschieht im dealloc der NSWindow-Klasse:

 - (void)dealloc {
    [...]
    [contentView release];
    [...]
    [super dealloc];
 }
Damit hat der Superview aber immer noch eine Referenz, nämlich von dem Subview. Sein RC berägt also 1. Die <tt>dealloc</tt>-Methode des Superviews wird nicht ausgeführt. Daher gibt der Superview nicht seine subviews frei! Die beiden halten sich gegenseitig und werden Speicherleichen.

Daher müsste bei einer solchen Struktur durch irgendjemanden sichergestellt werden, dass der Superview seine Subviews löscht. Dies könnte etwa das Fenster machen, indem es die Subviews des Superviews manuell löscht. Das obige <tt>dealloc</tt> sähe also wie folgt aus:

 - (void)dealloc {
    [...]
    [contentView removeSubviews];
    [contentView release];
    [...]
    [super dealloc];
 }
Was geschieht jetzt? Durch den <tt>-removeSubviews</tt> wird die Referenz auf das Subview gelöscht.
 - (void)removeSubviews {
     NSEnumerator* enumerator;
     NSView*       subview;
 
     enumerator = [[self subviews] objectEnumerator];
     while( subview = [enumerator nextObject] )
         [subview release];
     }
 }
Jeder Subview verliert seine einzige Referenz. Da er nummehr einen RC von 0 hat, wird wiederum seine (die des Subviews) <tt>dealloc</tt>-Methode aufgerufen. Dort heißt es entsprechend:
 - (void)dealloc {
    [...]
    [superview release];
    [...]
    [super dealloc];
 }
Hierdurch wird der RC des Superviews vermindert. Er ist jetzt also nur noch 1, weil kein Subview mehr auf ihn verweist, jedoch noch immer das Fenster. Wenn also dann das weitere <tt>release</tt> in unserer Fenster-Klasse ausgeführt wird, vermindert sich der RC des Fensters auf 0 und die <tt>dealloc</tt>-Methode des jetzt nackten Superviews wird ausgeführt. Obiges Beispiel:
 - (void)dealloc {
    [...]
    // Löscht die Subviews, welche das Superview referenzieren.
    [contentView removeSubviews];
 
    // Jetzt referenziert kein Subview maehr das Superview. Es hat nur
    // noch einen RC von 1. Das nächste release löscht den Superview.
    [contentView release]; 
 
    [...]
    [super dealloc];
 }

Dies alles ist sehr kompliziert. Man kann das vermeiden, indem man statt Referenzen teilweise einfache Verweise verwendet. Dies bietet sich vor allem, wie hier dargestellt bei zyklischen Hierachien, also solchen mit Rückverweisen auf die Eltern, an. In diesem Falle erhält das Eltern-Objekt keinen zusätzlichen RC. Wird es anderweitig freigegeben, so geht sein RC auf 0. Die <tt>-dealloc</tt>-Methode wird aufgerufen und kann ihrerseits ihre Kinder freigeben.

'Hinweis': Die <tt>NSView</tt>-Klasse macht es in Wahrheit auch so.

Haben wir es mit komplizierteren Graphen, die Zyklen enthalten zu tun, so geht dies häufig nicht so einfach. In diesem Falle sollten wir ein Objekt als Eltern-Objekt ansehen und uns von dort aus einen Auflösungsplan überlegen.

Autorelease-Pool

Der ARP dient zur Vereinfachung der Handhabung nur kurzfristig benötigter Instanzobjekte wie etwa Nameless Objects, sowie zur deren vereinfachten Instantiierung.

Man kann ihn sich vereinfacht als eine große Collection vorstellen, der Instanzobjekte referenziert. Dadurch kann der Referenz-Zähler des Objektes nicht 0 erreichen, und das Instanzobjekt nicht freigegeben werden. Allerdings löscht der ARP am Ende einer Event-Loop sämtliche Referenzen auf die gehaltenen Instanzobjekte, so dass diese Objekte endgültig aus dem Speicher entfernt werden, wenn sie nicht zwischenzeitlich anders referenziert werden.

Hinweis: Natürlich ist es theoretisch denkbar, den RC eines im ARP befindlichen Objektes durch wiederholtes Versenden einer -release-Nachricht auf 0 zu zwingen. Auch andernortes liegt es bei der Entdeckung eines Memory Leaks nahe, ein -release mehr oder weniger wild irgendwo einzubauen. Dies alles führt sehr gerne zum Absturz durch einen dangling Pointer. Es empfiehlt sich daher, die hier enthaltenen Regeln einzuhalten, da sich dann Memory Leaks recht leicht finden lassen.

Da allerdings die Event-Loop gleichermaßen die „oberste“ Methode ist, von der alle anderen Methoden aufgerufen werden, kann man sich sicher sein, dass ein im ARP befindliches Objekt in der gesamten Aufrufkette vorhanden ist.

Durch den ARP werden Objekte referenziert, die entweder eine -autorelease-Nachricht erhalten haben oder aber durch einen Convinience Allocator oder eine sonstige Factory erzeugt wurden. Alle diese Objekte verlieren also einen RC, wenn die Event Loop beendet wird.

Im obigen Beispiel ist ein ARP blau dargestellt. Lediglich dieser referenziert derzeit die Preview-Instanz. Außerdem ist darauf nur ein reiner Verweis von model gesetzte worden, der den RC nicht verändert. Dies ist in etwa die Situation nach folgendem Code:

modell.m

 [...]
 // preview ist nur ein Verweis, denn es erfolgt kein retain
 // Das Objekt wird jedoch mittels Convinience Allocator erzeugt, wes-
 // halb es durch den ARP referenziert wird. Der RC beträgt also 1.
 preview = [NSView viewWithFrame:frame]; 
 [...]
Ohne weitere Maßnahmen würde der durch den Member <tt>preview</tt> referenzierte View demnächst, nämlich bei der Rückkehr aus der Event Loop aus dem Speicher entfernt werden. Denn dann werden alle Referenzen im ARP gelöscht und die <tt>Preview</tt>-Instanz verliert ihre einzige Referenz: Der RC wird 0, das Objekt gelöscht.

Wir müssen also, wenn wir dies verhindern wollen, die <tt>Preview</tt>-Instanz entweder selbst referenzieren oder jemand anderen referenzieren lassen. Jedenfalls muss eine Referenz her, die den RC auf zwei treibt, so dass der Wegfall der Referenz aus dem ARP nicht zur Löschung führt. (Es sei denn, dies ist beabsichtig.) <tt>NSView</tt>-Objekte werden üblicherweise entweder in Superviews gehangen oder als Content-View eines Fensters angemeldet. Die hierfür vorgesehenen Methoden <tt>-contentView:(NSWindow)</tt> und <tt>-addSubview:(NSView)</tt> setzen eine Referenz auf das View, indem sie es retainen. Dies bedeutet, dass nunmehr eine Referenz außerhalb des ARP existiert und das View im Speicher verbleibt. Im obigen Beispiel ist etwa die <tt>NSButton</tt>-Instanz zu einem Superview als Subview hinzugefügt worden, weshalb es eine entsprechende Referenz erhielt. Wir müssen in unserem Beispiel bespielsweise folgenden Code ausführen:

model.m

 [...]
 // Erzeugung im ARP. Der View droht zu verschwinden.
 preview = [NSView viewWithFrame:frame]; 
 
 // Es wird eine Referenz durch den Superview erzeugt. Das View bleibt 
 // erhalten.
 [aSuperview addSubview:preview];        
 [...]
Natürlich müssen wir uns fragen, welche Methoden eine solche Referenz setzen. Die wichtigsten Fälle:

  • Ein <tt>NSWindow</tt> referenziert sein Content-View.
  • Ein <tt>NSView</tt> referenziert seine Subviews.
  • Eine Collection (NSArray, NSSet, NSDictionary) referenziert ihre Elemente.

Gerade der letzte Fall kommt sehr häufig vor, weshalb wir uns den entsprechenden Code noch einmal vor Augen halten. Die Erzeugung einer neuen Person-Instanz sähe etwa wie folgt aus:

model.m

 - (NSMutableArray*)persons {
     return _persons;
 }
 
 -(void)newPerson {
     Person* person;
 
    // Erzeugung im ARP. Das Objekt droht wieder zu verschwinden.
    person = [Person personWithName:@"N.N."]; 
 
    // Das Array referenziert als Container das enthaltene Objekt. Es
    // verbleibt im Speicher.
    [[self persons] addObject:person];
 }

Factories

Mit dem Konzept der Factories bezeichnet man Objekte, die andere Objekte erzeugen. Während etwa in C++ dies mittels new erfolgt, sind es also in Cocoa Methoden von Objekten, meist Klassenobjekten. Dabei gibt es grundsätzlich zwei Arten zu unterscheiden:

  • +alloc erzeugt ein Instanzobjekt auf dem Heap und setzt den Referenz-Zähler auf 1. Als Ergebnis wird ein Zeiger als Verweis zurückgeliefert.
  • Der Convenience Allocator erzeugt ebenfalls ein Instanzobjekt auf dem Heap, initialisiert dieses und erzeugt eine Referenz im ARP. Der RC beläuft sich ebenfalls auf 1. Es handelt sich um die Referenz aus dem ARP. Der Returnwert ist derselbe.

Hinweis: Es ist nicht garantiert, dass bei Aufruf von +alloc oder +allocator tatsächlich ein Instanzobjekt der verwendeten Klasse zurückgegeben wird. Vielmehr ist es erlaubt, Instanzobjekte anderer Klassen, insbesondere spezialisierter Subklassen zu liefern. Cocoa macht dies tatsächlich an zahlreichen Stellen. Man spricht dann von (Class Clustern).

Auch ist es erlaubt, dass Instanzobjekte selbst Factories sind. Ein Beispiel hierfür ist etwa -stringByAppendingString:(NSString).

In dem eingangs skizzierten Beispiel ist die Preview-Instanz in dem Zustand nach der Erzeugung mittels eines Convenience Allocators:

 [...]
 Preview* preview = [Preview preview];
 [...]

Zentrale Methoden

Die Speicherverwaltung von Cocoa wird über vier zentrale Methoden benutzt. Diese sollten stets gepaart angewendet werden.

  • +alloc Klassenmethode, die ein Instanzobjekt im Speicher anlegt, aber nicht initialisiert. Das Objekt erhält einen Referenz-Zähler von 1.
  • -autorelease Veranlasst den Autorelease-Pool (ARP), das Objekt bis zum Ende der Event-Loop zu halten.
  • -retain Erhöht den Referenz-Zähler um 1.
  • -release Vermindert den Referenz-Zähler um 1. Erreicht er 0, so wird das Objekt vernichtet.

Das erste Paar sollte tunlichst nur innerhalb eines Constructors der Klasse benutzt werden, während das zweite Paar tunlichst nur durch die Benutzer eines Instanzobjektes Verwendung finden sollte. Leider verhält es sich jedoch so, dass nicht jede Cocoa-Klasse einen eigenen Constructor mitbringt, weshalb dann auf +alloc und -autorelease zurückgegriffen werden muss.

How-To: Lokale Benutzung eines Instanzobjektes

Zuweilen möchte man ein Instanzobjekt nur lokal benutzen. Dies hat in der Regel einen von den drei folgenden Gründen:

Lokale Objekte

Das erzeugte Instanzobjekt ist (methoden-)lokal und wird lediglich zur Zwischenspeicherung von Werten benutzt. Es kann am Ende der Methode verworfen werden.

 - (void)method {
    NSString*  aString;
 
    // Setzen von aString
    aString = ...;
 
    // Weitere Benutzung
    [aString ...];
    [anObject useString:aString];
 
    // Am Ende der Methode wird das Objekt nicht mehr benötigt
 }
Dies entspricht in anderen Programmiersprachen der Erzeugung auf dem Stack mittels lokaler Variable.

Return-Objekte

Das Objekt ist eine lokale Variable, die zurückgegeben wird:

 - (NSString*)method {
    NSString*  aString;
 
    // Setzen von aString
    aString = ...;
 
    // Weitere Benutzung
    [aString ...];
    [anObject useString:aString];
 
    // Am Ende der Methode wird das Objekt an den Aufrufer zurückgegeben
    return aString;
 }

Namenlose Objekte

Mit namenlosen Objekt bezeichnet man solche Objektinstanzen, die nie einer (Zeiger-)Variablen zugewiesen werden. Sie tauchen lediglich in einer Parameterliste auf:

 [object useString:[NSString string]]; // der String ist namenlos
Das Problem dieser Objekte liegt darin, dass sie mangels Zeiger nicht „von Hand“ freigegeben werden können.

Lösung

In all den vorgenannten Fällen sollte seltener <tt>-release</tt> oder <tt>-retain</tt> verwendet werden. Vielmehr sollte, wenn vorhanden, ein Convenience Allocator aufgerufen werden, der das Instanzobjekt im 'ARP' zurückgibt. In dem dritten Beispiel ist dies bereits erfolgt (“<tt>[NSString string]</tt>“). In den beiden ersten Beispielen sollte die Objekterzeugung ebenso erfolgen.

 aString = [NSString string];
Steht kein Convenience Allocator zur Verfügung, weil die verwendete Klasse dies nicht anbietet, so ist es vorteilhaft ein alloc-init-autorelease-Tripel zu verwenden:
 aString = [[[NSString alloc] init] autorelease];
Da die meisten Objective-C-Programmierer von Programmeriersprachen kommen, die kein RC und ARP kennen, sieht man -übrigens auch in der Apple-Dokumentation zuweilen- eine Erzeugung, die dem Konzept von etwa Cpp entspricht, nicht jedoch dem von Cocoa:
 - (NSString*)method {
    NSString*  aString;
 
    // Setzen von aString
    aString = [[NSString alloc] init]; // Nicht machen! autorelease fehlt!
 
    // Weitere Benutzung
    [aString ...];
    [anObject useString:aString];
 
    // Am Ende der Methode wird das Objekt nicht mehr benötigt
    [aString release]; // Nicht machen! Retain fehlt!
 }
Dies hat zahlreiche Nachteile: * Die Objekterzeugung verursacht einen Seiteneffekt zur Objektvernichtung. Wird etwa später ein Convenience Allocator statt des <tt>+alloc</tt> benutzt zieht dies eine Änderung am Ende der Methode nach sich. Dies ist fehleranfällig und sehr häufige Ursache für Memory Leaks. * Die Objekterzeugng ist nicht kompatibel zur Objekterzeugung mittels Convenience Allocator. * Je nachdem, ob das Objekt zurückgegeben werden soll, muss es -retaint werden oder nicht. Auch dies ist ein Seiteneffekt, der zu Fehlern führt. Außerdem ist es bei zurückgegebenen Objekten nicht einsichtig, dass der Erzeuger das retain durchführt. Dies macht ansonsten ein Convienience Allocator auch nicht. Vielmeher gilt der Grundsatz, dass jeder für sich entscheidet, ob ein Objekt gehalten wird. * Es lässt sich, gerade bei verschachtelten Methoden, nicht schnell nachprüfen, ob die Anzahl der <tt>-retains</tt> stimmt. Die Suche eines Memory Leaks ist also schwierig. Die Verwendung des alloc-init-autorelease-Tripels beschränkt die Suche hingegen auf eine Zeile. Bei einer Suche nach „alloc“ lassen sich bereits im Search-Fenster Memory Leaks entdecken, ohne dass man die Source anschauen muss. * Werden mehrere etwa Eingangs einer Methode nacheinander angelegt, so müssen entweder sehr tief verschachtelte if-Zweige erfolgen oder es wird mit gotos gearbeitet. Beides tut der Leserlichkeit nicht gut. Bei der Erzeugung im ARP kann nach jeder fehlgeschlagener Erzeugung ein return erfolgen.
 // So ist es kompliziert wie C/Cpp
 -(void)methodCStyle {
     // Erzeuge 1. Hilfsobjekt
     NSString* myFirst = [[NSString alloc] init];
 
     if( myFirst != nil ) {
         // Erzeuge 2. HilfsObjekt
         NSString* mySecond  = [[NSString alloc] init];
 
         if( mySecond != nil ) {
             // Erzeuge 3. Hilsobjekt
             NSString* myThird = [[NSString alloc] init];
 
             if( myThird != nil ) {
                  // Jetzt mach endlich das, was du eigentlich tun willst.
                  ...
             } // third
         } second
      } first
 }
 
 // Und jetzt noch mal für Erwachsene 
 - (void)methodOCStyle {
      NSString* myFirst = [[[NSString alloc] init] autorelease];
      if( myFirst nil ) 
          return;
          ///////////////////////////////////////
 
      NSString* mySecond = [[[NSString alloc] init] autorelease];
      if( mySecond nil ) 
          return;
          ///////////////////////////////////////
 
          // Das geht, weil myFirst im ARP erzeugt wurde und demnächst 
          // gelöscht wird.
 
      NSString* myThird = [[[NSString alloc] init] autorelease];
      if( myThird nil ) 
          return;
          ///////////////////////////////////////
          // Und es geht immer noch. Und auch für die 4., 5., 6. ... lokale Instanz
 
      // Jetzt mache das, was du eigentlich wolltest. Und das am linken
      // Rand des Editor-Fensters!
 }
*Bei der INstantiierung mehrerer anhängiger Objekte kann es zu dangling Pointern kommen, wenn man nicht aufpasst. Schließlich hängt dies von der Implementierung der Methode ab. Beispiel:
 - (void)methode {
     Object* object;
     NSString* string;
 
     object = [[Object alloc] init];
     string = [object string]; // Ich brauche nur diesen String;
 
     [string retain];
     [object release];
 
     // string verwenden
     [string release];
 }
Wer diese Vorgehensweise nicht versteht oder bereits hier einmal Fehler gemacht hat, hat damit das beste Argument geliefert, warum man es so nicht machen sollte. ;-)

Diese Überlegungen und die Arbeit, bei denen man auch leicht etwas vergisst, kann man sich einfach sparen, wenn man gleich ein alloc-init-autorelease-Tripel verwendet:

 - (void)methode {
     Object* object;
     NSString* string;
 
     object = [[[Object alloc] init] autorelease];
     string = [object string]; // Ich brauche nur diesen String;
 
     // string verwenden
 }
So einfach kann es sein! Wir wollen gar nicht von Fällen sprechen, bei denen es um zahlreiche Objekte in Abhängigkeiten geht. Viel Spaß ohne ARP! * Und vor allem: Es gibt überhaupt keinen Grund dafür! (Ausnahme)

Die verwendete Methode funktioniert übrigens auch, wenn das Instanzobjekt zurückgegeben wird. Denn der ARP gibt nicht zwischen zwei Methodenaufrufen frei, sondern erst am Ende der Event-Loop:

 - (NSString*)stringMaker {
    NSString*  aString;
 
    // Setzen von aString
    aString = [NSString string];
 
    // Das Objekt wird an den Aufrufer zurückgegeben
    return aString;
 }
 
 - (void)stringUser {
    NSString* aNewString;
 
    aNewString = [self stringMaker];
    // aNewString befindet sich noch im ARP
 }

How-To: Der eigene convenience Allocator

Das vorangestellte Beispiel führt uns zum convenience Allocator. Grundsätzlich sollten eigene Klassen stets convenience Allocators anbieten. Wie oben dargestellt, führt dies zur einfacheren Lesbarkeit des Codes und vermindert außerdem, dass der Benutzer eines Objektes mit nicht notwendigem Aufwand (alloc-init-autorelease-Tripel) belastet wird. Ein Constructor sollte daher wie folgt aussehen

 + (Klasse*)klasse { // Ein Convenience Allocator in einer Klasse
    Klasse* object; 
 
    // Erzeugung des Instanzobjektes im ARP
    object = [[[Klasse alloc] init] autorelease];
 
    // Vielleicht noch etwas damit machen
    ...
 
    // Am Ende der Methode wird das Objekt an den Aufrufer zurückgegeben
    return object;
 }
Wenn -wie fast immer- dem Convenience Allocator eine entsprechende init-Methode zur Seite steht, reduziert er sich auf eine Zeile:
 + (Klasse*)klasseWithString:(NSString*)string { // zum Beispiel
    return [[[Klasse alloc] initWithString:string] autorelease];
 }

How-To: Referenzierung

Bis hierher haben wir lediglich Instanzobjekte lokal benutzt, die in einem Convenience Allocator oder zu Fuß mittels alloc-init-autorelease-Tripel erzeugt wurden. Dies führte dazu, dass Objekte im 'ARP' landeten und am Ende der Event-Loop vernichtet wurden.

Stärke des 'RC' von Cocoa ist aber, die Lebensdauer von Objekten besser kontrollieren zu können. Dazu muss einem Objekt mittels <tt>-retain</tt> gesagt werden, dass es erhalten bleiben soll, mittels <tt>-release</tt>, dass es jetzt freigegeben werden kann. Leider ist es auch hier so, dass alte Gewohnheiten aus anderen Programmiersprachen -auch hier wieder zuweilen die Apple-Dokumentation- zu einem Vorgehen führt, dass dem Konzept von Cocoa widerspricht. Dies soll an einem Beispiel verdeutlicht werden:

Stellen wir uns ein Objekt vor, welches auf einen String verweist:

 @interface Klasse : NSObject {
    NSString *_title; // Verweis auf ein anderes Objekt
 }
 
 - (Klasse*)initWithTitle:(NSString*)title;
 + (Klasse*)klasseWithTitle:(NSString*)title;
 
 @end
Im Convenience Allocator legen wir ein Instanzobjekt an und lassen es initialisieren:
 - (Klasse*)klasseMitTitle:(NSString*)title {
    return [[[Klasse alloc] initWithTitle:title] autorelease];
 }
Bis hierher waren wir schon gekommen. Im initWithTitle muss nun das übergebene String-Objekt referenziert werden, da es ansonsten möglicherweise beim nächsten Leeren des ARP verloren geht. Nicht günstig ist es hierbei, ein einfaches <tt>-retain</tt> zu senden. Denn zur Vermeidung eines Memory Leaks müsste irgendwo ein passendes <tt>-release</tt> stehen. Dies zu finden und zu kontrollieren ist schwierig. Nicht immer, sogar so gut wie nie, reicht es im -dealloc aus. Wir handeln uns also wieder jede Menge Seiteneffekte ein und müssen über mehrere (hundert) Codezeilen „schielen“.

Daher empfiehlt es sich, auch <tt>-retain</tt> und <tt>-release</tt> eng zu paaren. Um dies zu ermöglichen ist absichtlich in Objective-C die Möglichkeit vorhanden, diese Nachrichten auch an ein <tt>nil</tt>-Objekt zu senden.

Accessoren

Die Vorgehensweise ist recht einfach. Zunächst programmiert man sich Accessor-Methoden, welche die entsprechende Referenz setzen. Diese braucht man in der Regel ohnehin, so dass dies keinen Overhead bedeutet:

 -(NSString*)title {
    return _title;
 }
 
 - (void)setTitle:(NSString*)title {
    [title retain];
    [_title release];
    _title = title;
 }
Diese Befehlsfolge hat den Vorteil, dass die Anzahl der <tt>-retain</tt>- und <tt>-release</tt>-Nachrichten auf jeden Fall gleich ist. Niemals kann ein einzelnes <tt>-retain</tt> oder <tt>-release</tt> vergessen werden!

Aber wir müssen noch auf einen Effekt aufpassen: Es kann passieren, dass erneut das Objekt, welches wir bereits referenzieren gesetzt wird. Im „klarsten“ Falle sieht das so aus:

 [self setTitle:[self title]];
Würden wir die Reihenfolge ändern und zunächst den release im Setter machen, so würde für einen Moment der RC auf 0 gehen und das Objekt freigegeben werden. Das nachfolgende <tt>retain</tt> würde ins leere laufen, wir erhielten einen dangling Pointer:

 // title hat einen RC von 1
 // wir benutzen einen "umgekehrten" Setter:
 - (void)setTitle:(NSString*)title {
    [_title release];
    [title retain];
    _title = title;
 }
 
 // Jetzt rollen wir mal den obigen Code aus:
 [self setTitle:[self title]];
 
 // Weil _title title führt dies de facto zu  
 [_title release]; // Der RC geht auf 0. _title wird gelöscht
 [_title retain]; // Das neue Objekt ist das alte! Das klappt nicht!
 _title = _title; // Jetzt zementiern wir auch noch den Unsinn fest.
 
Eine andere beliebte Art, dies auszuschließen ist der folgende Setter:
 - (void)setTitle:(NSString*)title {
     if( title != _title ) {
        [_title release];
        _title = [title retain];
     }
 }
Dies dürfte etwas weniger performant sein, wenn in der Regel nicht immer das gleiche Objekt gesetzt wird, in anderen Fällen performanter. Inhaltlich läuft es auf das gleiche hinaus.

Ein weiteres Problem, welches beachtet werden muss, kann zu einem Copy-Setter führen. Da wir mit Referenzen arbeiten, kann jeder unser Objekt außerhalb des Setters manipulieren. Nicht immer lässt sich die auch durch KVO observieren. Wir müssen uns klar werden: Wir speichern lokal eine Referenz, kein Objekt!

Wollen wir lokal wirklich das Objekt speichern, so müssen wir uns eine Kopie anfertigen. Dies macht etwa Sinn, wenn wir lokale Veränderungen vornehmen, die andere nicht bemerken sollen. Hier liegt auch gleich das Gegenargument: Andere erhalten diese Veränderungen nicht, auch nicht mittels KVO, da wir ja ein eigenes Objekt haben. Ein Copy-Setter sieht wie folgt aus:

 - (void)setTitle:(NSString*)title {
     [_title autorelease];
     _title = [title copy];
 }
Allerdings verbraucht der Copy-Setter Performance und Speicher, weshalb er nur angewendet werden sollte, wenn die private Kopie notwendig ist.

Auch beim Getter kann es bei multi-threaded Applikationen zu Problemen kommen. Wenn wir uns in einem Thread die Referenz mit dem Getter abholen, kann es sein, dass in einem anderen Thread eine neue Refrenz gesetzt wird und unser abgeholtes Objekt nicht mehr existiert. Die Folge ist ein dangling Pointer. Dem kann entgegengewirkt werden, indem man das Objekt „zur Sicherheit“ erneut im ARP des abholenden Threads speichert. (Jeder Thread hat einen eigenen ARP.) Ein solcher Getter sähe wie folgt aus:

 - (NSString*)title {
       return [[_title] retain] autorelease];
 }
Was geschieht hier? Durch das retain und autorelease wird der RC zunächst um 1 erhöht bis der 'ARP des abholenden Threads gelöscht wird. Wir haben also eine Löschverzögerung. Allerdings hat diese Problemlösung zwei Nachteile:

* Es werden bei jedem Getter-Aufruf (sehr häufig) zwei Nachrichten verschwendet, obwohl die meisten Programme nicht mittels Gettern über Threads kommunizieren. * Es ist ein doktorn an Symptomen. Wenn das referenzierte Objekt in einem anderen Thread ausgestauscht wurde, so behalten wir auf diese Weise zwar unser Objekt, es ist jedoch ungültig geworden, womit unser Model inkonsistent ist. Das ist zwar ein kleineres Übel, aber immer noch ein gewaltiges.

Um in multi-threaded Applikationen Accessoren sicher benutzen zu können, bleibt uns daher im Endeffekt nichts anderes übrig als entsprechende Mechanismen des Multi-Threading zu implementieren. Wem dennoch die verlängerte Überlebensdauer eines Objektes reicht, kann die Nachrichten auch im Thread schicken, so dass sie nur Performance verbrauchen, wenn dies erforderlich ist. Er nimmt den gewöhnlichen Getter und fügt selbst das <tt>retain</tt> und <tt>autorelease</tt> hinzu:

 ... = [[[object title] retain] autorelease];
Durch diese „Verschiebung“ der Lösung von dem stets benutzen Getter in dem speziellen Anwendungsfalls des Threadings lässt sich die Performance genauer „dosieren“.

init und dealloc

In unserem <tt>-initWithTitle</tt> müssen wir jetzt nur noch den übergebenen Parameter setzen. Weitere Members sollten ebenfalls initialisiert werden:

 - (Klasse *)initWithTitle:(NSString *)title {
    self = [super init];
 
    if (self) {
       [self setTitle:title];
       [self setMember:[NSString string]]; // Ein Member der Klasse
    }
 
    return self
 }
Wie immer muss in einem <tt>-dealloc</tt> die referenzierte Ressource noch freigegeben werden:
 - (void)dealloc {
   [self setTitle:nil];
   [self setMember:nil];
 
   [super dealloc];
 }

Sonstiges

Problem: Groß wachsender ARP

Ein Problem bei Iterationen und Rekursionen kann darin liegen, dass der ARP in ungeahnte Größe wächst. Ist es nicht so, dass der Algorithmus selbst Instanzobjekte sammeln muss, so kann der ARP tatsächlich eine riesige Müllhalde erzeugen. Dies soll an einem Beispiel gezeigt werden:

Wir haben ein Objekt, welches ein Array von Strings hält, welches wir <tt>strings</tt> nennen.

 @interface Model : NSObject {
     NSMutableArray* _strings;
 }
 
 -(NSMutableArray*)strings;
 -(void)setStrings:(NSMutableArray*)strings;
 @end
 
 @implemenation Model
 -(NSMutableArray*)strings { return _strings; }
 -(void)setStrings:(NSMutableArray*)strings {
     [strings retain];
     [_strings release];
     _strings = strings;
 }
 
 - (Model*)init {
     self = [super init];
     if( self ) {
        [self setStrings:[NSMutableArray array]];
     }
     return self;
 }
 
 -(void)dealloc {
     [self setStrings:nil];
     [super dealloc];
 }
 @end
In einer Methode dieses Modelles sollen aufeinanderfolgende Strings erzeugt werden:
 -(void)iterateToARPDeath {
    int      i;
    NSString*       first;
    NSString*       number;
    NSMutableArray* myStrings;
 
    myStrings = [NSMutableArray array];
    first = [NSString stringByString:@"Tom "];
    for( i = 0; i  <=9811 ; i++ ) {
 
       // Erzeuge den Nummernstring
       number = [NSString stringByFormat:@"%d", i];
 
       // Erzeuge den Zusammengesetzten String
       [myStrings addObject:[first stringByAppendingString:number]];
 
    } // for i
    [self setStrings:myStrings];
 }
Scheinbar besteht nur ein String <tt>first</tt>, ein String <tt>number</tt> und die ganz vielen erzeugten Strings, weil in jedem Durchlauf der gleiche Zeiger neu gesetzt wird. Der Convenience Allocator hat jedoch den String in den 'ARP' gesetzt, weshalb er nicht sofort freigegeben wird. Vielmehr sammeln sich in der Iteration unzählige String-Objekte im ARP. Jedes <tt>number</tt> wird in jedem Durchgang erzeugt, womit der Speicherverbrauch verdoppelt ist. Bei 1.000 Durchläufen entstehen etwa:

* 1 <tt>first</tt>-String * 9812 endgültige Strings (Das ist in Ordnung, denn die wollen wir ja gerade erzeugen.) * 9812 <tt>number</tt>-Strings, die wir eigentlich gar nicht benötigen.

Hiergegen helfen nur „Notmaßnahmen“, die man nicht verallgemeinern sollte:

Alloc - Release - Paar

Statt des Convenience Allocator oder des alloc-init-autorelease-Tripel nutzt man 'ausnahmsweise' nur ein alloc-init-Dupel und gibt die lokale Variable am Ende der Iteration tatsächlich wieder frei.

 -(void)iterateToARPDeath {
    int      i;
    NSString*        first;
    NSString*         number;
    NSMutableArray* myStrings;
 
    myStrings = [NSMutableArray array];
    first = [NSString stringByString:@"Tom "];
    for( i = 0; i  <=9811 ; i++ ) {
       // Erezuge den Nummernstring
       number = [[NSString alloc] initByFormat:@"%d", i];
 
       // Erzeuge den Zusammengesetzten String
       [myStrings addObject:[first stringByAppendingString:number]];
 
       // und wieder weg
       [number release];
    } // for i
    [self setStrings:myStrings];
 }
Dies hat neben den allgemeinen Nachteilen, denjenigen, dass ein Break ausscheidet. Außerdem kann nicht sicher gesagt werden, dass der <tt>-initByFormat</tt>-Methode ihrerseits Objekte im ARP erzeugt, die vermüllen.

Wiederverwendung

Ganz ähnlich ist folgender Ansatz: Man alloziert eine Instanz und initialisiert diese immer wieder mit neuen Werten.

 -(void)iterateToARPDeath {
    int      i;
    NString* aString;
 
    aString = [NSString string];
    for( i = 0; i < 10000000; i++ ) {
       // Erzeuge einen String zur Zwischenspeicherung
       aString = [NSString init];
       [...]
 
    } // for i
 }
Aber auch hier besteht das Problem, dass man nicht weiß, ob <tt>init</tt> Müll zurücklässt.

Häppchen bilden

Besser ist es -da man ohnehin auch Performance-Schwierigkeiten bekommt-, die Routine zu zerteilen, falls dies möglich ist. Dann wird zwischendurch die Event-Loop verlassen. Der ARP wird geleert.

Das Ganze ruft man dann in einem Timer auf.

 -(void)iterateToARPDeathFrom:(int)from to:(int)to  {
    int      i;
    NSString*        first;
    NSString*         number;
 
    first = [NSString stringByString:@"Tom "];
    for( i = 0; i  <=9811 ; i++ ) {
       // Erzeuge den Nummernstring
       number = [[NSString alloc] initByFormat:@"%d", i];
 
       // Erzeuge den Zusammengesetzten String
       [[self strings] addObject:[first stringByAppendingString:number]];
 
       // und wieder weg
       [number release];
    } // for i
 }
Das geht freilich nicht immer.

Eigener ARP

Schließlich bietet es sich an, eigene ARP zu verwenden, die man etwa wieder leert. Dies dürfte die mit Abstand überlegene Methode sein.

 -(void)iterateToARPDeath {
    int      i;
    NSString*        first;
    NSString*         number;
    NSMutableArray* myStrings;
    NSAutoreleasePool* pool;
 
    myStrings = [NSMutableArray array];
    first = [NSString stringByString:@"Tom "];
 
    // Erzeuge ARP, den man selbst kontrollieren kann
    pool = [[NSAutoreleasePool alloc] init];
    for( i = 0; i  <=9811 ; i++ ) {
       // Erzeuge den Nummernstring
       number = [NSString stringByFormat:@"%d", i]; // wird im privaten ARP abgelegt
 
       // Erzeuge den zusammengesetzten String
       [myStrings addObject:[first stringByAppendingString:number]];
                    // auch der erzeugte String wird im lokalem ARP abgelegt.
                    // Da aber addObject ein retain durchführt, erhht sich der RC um 1. Wird der lokale ARP gelöscht,
                    // bleibt unser String erhalten, da sein RC von 2 auf 1, nicht aber auf 0 fällt
 
       // Wenn wir hin und wieder den ARP leeren, fliegen die nicht retainten Objeke heraus. Die retainten bleiben erhalten.
       if( (i % 256) 0 ) {
           [pool release];
           pool = [[NSAutoreleasePool alloc] init];
       }
 
    } // for i
     [pool release];
     [self setStrings:myStrings];
 }
Natürlich kann man auch zwei verschachtelte Schleifen machen. Eine äußere, die in etwa 256er Schritten zählt und den ARP am Anfang anlegt und am Ende freigibt und eine innere, die dann durchzählt. Dies ist in der Regel besser lesbar.

Links

 
wiki/speicherverwaltung.txt · Zuletzt geändert: 2007/12/28 12:10 (Externe Bearbeitung)
 
Falls nicht anders bezeichnet ist der Inhalt dieses Wikis unter der folgenden Lizenz veröffentlicht:CC Attribution-Noncommercial-Share Alike 3.0 Unported
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki