Foundation's Edge -- NSAutoreleasePool

cs.rit.edu

Foundation's Edge -- NSAutoreleasePool

Foundation’s Edge

‘‘N SAutoreleasePool’’

Axel T. Schreiner, Universität Osnabrück

Das Foundation-Ki t is t die Grundlage der OPENSTE P-Technologie von NeXT. Dieser

Ar tikel zeig t am Beispiel eines Programms zur Zählung von Wor thäu figkei ten einige

Stärken dieser Klassenbibliothek, die auch von GNU ver fügbar is t.

Foundation

Der Titel stammt zwar von einer Science-Fiction-Geschichte von Isaac Asimov, aber es geht

hier nicht um zukünftige Herrscher, sondern um das Foundation-Kit, das schon seit einigen

Releases der Portable Distributed Objects oder im Enterprise Object Framework oder eben

mit OPENSTEP verfügbar ist und dessen GNU-Implementierung auch in großen Teilen

fertiggestellt ist. Nach Webster bedeutet edge auch einen Wettbewerbsvorteil, und einiges,

was meiner Meinung nach dazu beiträgt und was man durchaus auch anderswo verwenden

könnte, soll dieser Artikel illustrieren.

Foundation enthält Klassen für Werte wie NSDate, NSNumber und NSString, Container

wie NSArray, NSDictionary sowie NSSet und Systemzugriff wie NSFileHandle,

NSFileManager, NSHost, NSProcessInfo und NSThread. Objekte können über ein

NSNotificationCenter kommunizieren, mit Unterklassen von NSCoder als Bytes in NSData


gespeichert oder über eine NSConnection verteilt und mit NSAutoreleasePool zuverlässig

dynamisch verwaltet werden.

Insgesamt gibt es etwa 10 Protokolle und über 50 öffentliche Klassen. Sogenannte Class

Cluster wie NSNumber, NSSet oder NSString verbergen eine Reihe spezialisierter und daher

effizienter Unterklassen hinter einer gemeinsamen, meist abstrakten Basisklasse als einziger

Schnittstelle. Das vereinfacht zwar die Benutzung (und die Lernkurve), erschwert aber die

Implementierung von Unterklassen, denn man muß häufig auf Aggregatbildung ausweichen.

Speicherverwaltung

Foundation verwendet Referenzzählung. Ein Objekt wird durch Aufruf von alloc oder, falls

eine Klasse das NSCopying-Protokoll unterstützt, mit copy erzeugt, und sein Referenzzähler

ist dann 1. retain erhöht den Zähler, und release verringert ihn. release dient zur Freigabe

des Objekts, denn wenn der Referenzzähler den Wert 0 erreicht, wird implizit dealloc

aufgerufen und damit das Objekt freigegeben. dealloc sollte man nie explizit aufrufen. Wenn

ein Objekt eigene Ressourcen besitzt, muß man dealloc jedoch implementieren und dort die

Ressourcen wieder mit release freigeben, bevor man dealloc in der Oberklasse aufruft.

Bisher ist das eine klassische Lösung, die allerdings dadurch etwas unorthodox anmutet,

daß die Referenzzähler nicht etwa in den Objekten selbst gespeichert werden, sondern durch

globale Tabellen repräsentiert sind, in die Objekte nur bei erhöhtem Zähler eingetragen

werden.

Spannend ist jedoch, wie die Frage der Verantwortung für die Freigabe von Objekten

geregelt ist: Wer einen Referenzzähler erhöht, muß ihn auch wieder verringern. Wer also

alloc, copy oder retain aufruft, ist auch für das zugehörige release verantwortlich. Wer diese

Methoden nicht aufruft, darf dann auch release nicht aufrufen.


Nach diesen Regeln könnte man kein Objekt als Resultat liefern, wenn da nicht noch der

NSAutoreleasePool wäre. Objekte dieser Klasse werden durch Erzeugung implizit

geschachtelt. Wenn man bei einem beliebigen Objekt für release verantwortlich ist, kann man

diese Verantwortung durch Aufruf von autorelease an den innersten aktiven Pool abschieben.

Schickt man später release an einen NSAutoreleasePool, schickt er seinerseits release an alle

in ihn verschachtelten Pools und an alle Objekte, für die er verantwortlich gemacht wurde.

Man liefert und erhält also als Resultat immer Objekte, die sich quasi selbst zerstören, wenn

der aktive Pool aufgegeben wird. Speichert ein Container wie NSArray ein Objekt, so muß

er es mit retain schützen und dies mit release rückgängig machen, wenn er das Objekt wieder

abgibt.

In main() wird immer ein Pool angelegt, sonst setzt es bissige Bemerkungen aus dem

Laufzeitsystem. Andere Pools erzeugt man zum Beispiel nur für die Dauer eines

Funktionsaufrufs oder eines Durchgangs durch eine Schleife, bei denen sehr viele temporäre

Objekte entstehen; OPENSTEP hat insbesondere einen eigenen Pool für die Verarbeitung eines

Events. Da ein Pool verschachtelte Pools berücksichtigt, funktioniert das System auch bei

Fehlerbehandlung mit NSException korrekt.

‘‘freq’’

Zur Illustration der Speicherverwaltung folgt hier ein Programm, das einige Foundation-

Klassen verwendet, um die Häufigkeit von Wörtern in einer Datei zu zählen. Wie üblich

werden Dateinamen als Argumente angegeben, falls das Programm nicht seine Standard-

Eingabe verarbeitet; per Konvention steht − als Argument für die Standard-Eingabe.

Damit das Beispiel nicht ganz trivial ausfällt (und Java-Freunde anspornt), wird Unicode

verarbeitet, das heißt, die Ein- und Ausgabe erfolgt in UTF, einem Format, bei dem sich

ASCII-Zeichen selbst darstellen und alle anderen Unicode-Zeichen durch zwei oder drei Bytes


epräsentiert werden.

Hauptprogramm

Im Hauptprogramm wird primär die Kommandozeile verarbeitet, siehe Abbildung 1.

1 #import "NSCountedSet.h"

2 int main (int argc, char * argv []) {

3 NSAutoreleasePool * pool = [NSAutoreleasePool new];

4 NSCountedSet * words = [NSCountedSet set];

5 NSCharacterSet * sep;

6 { NSMutableCharacterSet * tmp;

7 tmp = [[NSCharacterSet punctuationCharacterSet] mutableCopy];

8 [tmp formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];

9 [tmp formUnionWithCharacterSet:[NSCharacterSet controlCharacterSet]];

10 sep = [[tmp copy] autorelease], [tmp release];

11 }

12 NS_DURING

13 if (argc


25 }

Abbildung 1: ‘‘freq.m’’ — Hauptprogramm

Zur Zählung dient ein Bag, das heißt, ein NSCountedSet, das Objekte ein- oder mehrmals

enthalten kann (4). set ist eine Klassenmethode von NSSet, die eine leere Menge erzeugt und

sie, wie alle Methoden außer alloc, new und copy, sofort in den aktuellen

NSAutoreleasePool einstellt. Dieser wurde nach Konvention ganz zu Anfang erzeugt, und er

wird am Schluß samt Inhalt beseitigt (3,23).

Ist kein Argument angegeben, wird die Standard-Eingabe gezählt (13f), andernfalls wird ein

Argument nach dem andern bearbeitet, bis man auf den Nullzeiger am Schluß der

Argumentliste stößt (16f). Foundation arbeitet nur mit NSString-Objekten, die Unicode

enthalten, deshalb muß jedes Argument umgewandelt werden (17). Auch hier entsteht ein

Objekt, das später von pool wieder freigegeben wird.

Als Worttrenner sollen Zwischenraum, Steuerzeichen wie Zeilentrenner, aber auch

Interpunktionszeichen dienen. Zur Repräsentierung verwendet man am besten ein

entsprechend initialisiertes NSCharacterSet (5f). Gewisse konstante Zeichenmengen sind

vordefiniert, aber um sie zusammenzufügen, benötigt man ein NSMutableCharacterSet (6).

Die Unterscheidung ist typisch für Foundation: Konstante Objekte sind möglicherweise

effizienter zu implementieren, deshalb gibt es oft Klassen mit und ohne dem Wort Mutable

im Namen. Der Übergang erfolgt durch mutableCopy (7) und zurück mit copy (10). Für die

Kopien ist man verantwortlich, deshalb werden sie hier mit release und autorelease behandelt

(10).

Ähnlich wie in Java gibt es auch bei Foundation die Möglichkeit, Fehler als NSException-

Objekte zu verpacken und durch Aufruf von raise:format: verschachtelte Funktionsaufrufe

abzubrechen.


Der Makro NS_DURING leitet eine Kontrollstruktur ein, in der eine NSException

abgefangen wird (12,19,21). Im Bereich des Handlers ist localException zugänglich, und

man kann zum Beispiel eine Meldung ausgeben oder mit raise den Fehler auch weiterleiten.

NSLog() ist eine Funktion im Stil von printf(), die eine Diagnose-Ausgabe produziert

(18,20). Das Format muß allerdings ein NSString-Objekt sein, und mit dem Formatelement

%@ kann man die Beschreibung eines Objekts ausgeben. Jede Klasse erbt die Methode

description, die eine Beschreibung eines Objekts als NSString liefert. Ein NSString

beschreibt sich selbst, deshalb ist der Aufruf von description in diesem Zusammenhang

überflüssig.

Programme enthalten häufig konstante Strings, deshalb kann man neuerdings

Zeichenketten-Literale durch @ in NSString-Objekte verwandeln.

NSFileHandle ist eine der Klassen, die Systemzugriffe verbergen (22). writeData:

akzeptiert ein NSData-Objekt, das einen dynamisch dimensionierten Vektor von Bytes

enthält, und gibt ihn aus. Ein Fehler würde als NSException berichtet werden; wird dies

nicht abgefangen, bricht das Programm mit einer entsprechenden Meldung ab.

Spezi fische Methoden

NSCountedSet registriert beliebige Objekte beliebig oft. Man kann alle Objekte als

NSArray abholen und abfragen, wie oft ein Objekt registriert wurde. Abbildung 2 zeigt neue

Methoden wie collected, die im Hauptprogramm verwendet wurden, sowie einige zusätzliche

Methoden, die praktisch umsonst bei diesem Projekt abfallen.

1 #import

2 @interface NSCountedSet (collect)

3 — (void)collectStandardInputWithin:(NSCharacterSet *)sep;

4 — (void)collectPath:(NSString *)fnm within:(NSCharacterSet *)sep;


5 — (void)collectFileHandle:(NSFileHandle *)fh within:(NSCharacterSet *)sep;

6 — (void)collectString:(NSString *)s within:(NSCharacterSet *)sep;

7 — (NSData *)collected;

8 @end

Abbildung 2: ‘‘NSCountedSet.h’’ — problemspezifische Methoden

Da für das NSCountedSet keine zusätzlichen Daten erforderlich sind, muß keine Unterklasse

konstruiert werden, es genügt, die Methoden in einer Kategorie collect zu NSCountedSet

hinzuzufügen (2).

Implementierung

Abbildung 3 illustriert schließlich, wie die eigentliche Arbeit des Programms geleistet wird.

collectStandardInputWithin: leitet einfach einen passend erzeugten NSFileHandle und die

Trennzeichen weiter (3f). Da es zwei Möglichkeiten gibt, die Bearbeitung der Standard-

Eingabe auf der Kommandozeile zu verlangen, lohnt sich eine gemeinsame Methode.

1 #import "NSCountedSet.h"

2 @implementation NSCountedSet (collect)

3 — (void)collectStandardInputWithin:(NSCharacterSet *)sep {

4 [self collectFileHandle:[NSFileHandle fileHandleWithStandardInput]

5 within:sep];

6 }

7 — (void)collectPath:(NSString *)fnm within:(NSCharacterSet *)sep {

8 NSFileHandle * fh;

9 if (! [fnm length])

10 [NSException raise:@"InvalidPath" format:@"empty"];

11 if ([fnm isEqualToString:@"—"])


12 [self collectStandardInputWithin:sep];

13 else if (! (fh = [NSFileHandle fileHandleForReadingAtPath:fnm]))

14 [NSException raise:@"InvalidPath" format:@"%@: cannot open", fnm];

15 else

16 [self collectFileHandle:fh within:sep], [fh closeFile];

17 }

18 — (void)collectFileHandle:(NSFileHandle *)fh within:(NSCharacterSet *)sep {

19 NSAutoreleasePool * pool = [NSAutoreleasePool new];

20 NSData * data = [fh readDataToEndOfFile];

21 NSString * text =

22 [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

23 [self collectString:text within:sep];

24 [text release], [pool release];

25 }

26 — (void)collectString:(NSString *)s within:(NSCharacterSet *)sep {

27 NSScanner * scan = [NSScanner scannerWithString:s];

28 NSString * word;

29 [scan setCharactersToBeSkipped:sep];

30 while (! [scan isAtEnd])

31 if ([scan scanUpToCharactersFromSet:sep intoString:&word])

32 [self addObject:word];

33 else

34 [NSException raise:@"Botch" format:@"no word"];

35 }

36 — (NSData *)collected {

37 NSAutoreleasePool * pool = [NSAutoreleasePool new];

38 NSMutableString * text = [NSMutableString string];

39 NSEnumerator * seq = [[[self allObjects]

40 sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]

41 objectEnumerator];

42 NSString * s;


43 NSData * result;

44 while (s = [seq nextObject])

45 [text appendFormat:@"%@\t%u\n", s, [self countForObject:s]];

46 result = [[text dataUsingEncoding:NSUTF8StringEncoding] retain];

47 [pool release];

48 return [result autorelease];

49 }

50 @end

Abbildung 3: ‘‘NSCountedSet.m’’ — Implementierung der neuen Methoden

collectPath:within: könnte prinzipiell eine Methode von NSString verwenden, die eine Datei

in einen String einliest. In diesem Fall hat man keinen Einfluß auf die Codierung, nach der

die Bytes aus der Datei in Unicode-Zeichen im String verwandelt werden. Da freq aber

unbedingt UTF verarbeiten soll, muß man für einen Dateinamen einen NSFileHandle

erzeugen, bei dem man dann Lesen und Codieren genau kontrollieren kann.

Je nach Betriebssystem kann ein leerer Dateiname Probleme verursachen, deshalb wird er

explizit verboten (9). raise:format: erzeugt eine NSException, hinterlegt einen String und

einen formatierten String, die als name und reason abgefragt werden können, und springt

dann zur Fehlerbehandlung in der nächstgelegenen NS_DURING-Struktur (10).

Wenn − als Dateiname angegeben ist, wird die Standard-Eingabe verarbeitet (11f).

Andernfalls sollte man aus dem Argument einen NSFileHandle für Lesezugriff erzeugen

können (13), der dann an die dafür vorgesehene Methode geschickt wird (16).

Der NSFileHandle wird nicht explizit durch alloc oder copy erzeugt, deshalb darf er auch

nicht mit release freigegeben werden. Er besitzt jedoch eine kostbare System-Ressource,

nämlich eine Dateiverbindung, die man möglichst bald zur Wiederverwendung bringen muß.

Dafür gibt es die Methode closeFile (16).


NSFileHandle liefert beim Aufruf readDataToEndOfFile einen Byte-Vektor mit dem

Dateiinhalt, beginnend an der aktuellen Position (20). Ein solches NSData-Objekt kann man

in einen String umwandeln und dabei genau kontrollieren, wie die Bytes interpretiert werden

sollen (22). Da man möglicherweise später auch Wörter aus einem String einsammeln

möchte, erfolgt die Analyse des Strings in einer eigenen Methode (23).

Es ist keineswegs abwegig, eine ganze Datei auf einmal einzulesen. Ein modernes

Betriebssystem erlaubt, daß die Datei einfach in den Adreßraum des Programms abgebildet

wird; erst wenn tatsächlich auf den Inhalt zugegriffen wird, erfolgen Transferoperationen im

Rahmen von Paging.

Da das Einlesen der Datei möglicherweise viel Speicherplatz beansprucht, wird ein eigener

Pool eingesetzt (19). Das NSData-Objekt stammt nicht explizit von alloc oder copy, deshalb

wird es implizit freigegeben, wenn der Pool freigegeben wird (24). Das NSString-Objekt

text kann hier nur mit einer init-Methode initialisiert werden; es muß also explizit mit alloc

erzeugt und folglich auch mit release freigegeben werden (22,24).

In collectString:within: geht es schließlich zur Sache. Zur Zerlegung eines Unicode-

Strings dient ein NSScanner (27), mit dem man dann in einer Schleife einzelne Wörter

zwischen den Zeichen in sep lokalisieren kann (29f). Der Fehler in Zeile (34) sollte nicht

auftreten können.

Jedes Wort wird implizit als String erzeugt, ginge also bei Freigabe des nächstgelegenen

Pools wieder verloren. Als Container-Objekt übernimmt das NSCountedSet in addObject

jedoch die Verantwortung für das Wort und führt intern retain aus, um die Freigabe zunächst

zu verhindern (32). Die Freigabe erfolgt in diesem Fall, wenn das NSCountedSet selbst

freigegeben wird, wenn also im Hauptprogramm release an den äußeren Pool geschickt wird.

collected soll sich um einen Bericht über die Häufigkeiten kümmern, der allerdings in UTF

ausgegeben werden muß (36f). Man konstruiert dazu den Bericht als NSMutableString, an

den mit appendFormat: Zeilen angefügt werden (38,45). Als Resultat wird der String dann


kontrolliert in ein NSData-Objekt umgewandelt (46).

Da hier möglicherweise viele temporäre Objekte entstehen können, wird nochmals ein

lokaler Pool verwendet (37,47). Das Resultat muß allerdings diesen Pool überleben, wobei

collected immer noch für die Freigabe verantwortlich bleibt. Im Pool sorgt retain dafür, daß

result die Freigabe des Pools überlebt (46). Am Schluß der Methode übergibt dann

autorelease die Verantwortung für die Freigabe an einen äußeren Pool (48).

Im Gegensatz zu NeXTSTEP werden in OPENSTEP sehr viele Protokolle und an Stelle von

id explizite Klassenangaben verwendet. Wo möglich, liefern die Methoden immer void,

damit eine Verteilung von Objekten billiger wird. retain und autorelease gehören zu den

wenigen Methoden, die noch ihren Empfänger als Resultat liefern, damit man wie hier

Methodenaufrufe kaskadieren kann (46,48).

collected demonstriert einen typischen, aber relativ rücksichtslosen Umgang mit

Speicherplatz: Das Resultat soll unabhängig von Groß- und Kleinschreibung sortiert sein.

allObjects liefert die Elemente einer Menge in einem NSArray-Objekt, aus dem man mit

verschiedenen Methoden sortierte NSArray-Objekte erzeugen kann; hier geschieht das relativ

zu einer NSString-Methode, die auf die Element-Objekte angewendet wird, um ihre

Reihenfolge zu bestimmen (40).

NSEnumerator ist ein allgemeiner Mechanismus, mit dem eine Sammlung von Elementen

durchlaufen wird. Von einem NSArray erhält man ihn durch objectEnumerator, und mit

nextObject kann man dann die Objekte der Reihe nach, also hier sortiert, betrachten (41,44).

Mich stört ein bißchen, daß ich für jedes Wort seine Häufigkeit nochmals explizit beim

Container NSCountedSet hinterfragen muß (45), denn dahinter steckt natürlich eine Suche in

der Menge aller Wörter, aber alles andere läßt sich so elegant aus vorgefertigten Klassen

aufbauen, daß ich diese Ineffizienz in Kauf genommen habe.


Fazit

NSAutoreleasePool ist ein interessanter Kompromiß zwischen Speicherverwaltung mit

garbage collection, wie sie in Java und SmallTalk zur Verfügung steht, und expliziter

Freigabe im Stil von C, NeXTSTEP und C++. Hat man sich an die Spielregeln gewöhnt, kann

man lokal sehr zuverlässig kontrollieren, daß weder Speicherlecks entstehen, noch Objekte

vorzeitig freigegeben werden. Mit den Pools hat man trotzdem die Lebensdauer temporärer

Werte ganz gut im Griff.

Installiert man eine Ablaufverfolgung, stellt man allerdings fest, daß OPENSTEP in einem

aktiven Fenster ohne Benutzeraktivität im Sekundentakt Objekte erzeugt und freigibt — die

Disziplin der Pools hat offensichtlich auch ihren Preis.

freq illustriert die üblichen Verfahren: den äußeren Pool in main() (1-3,23), explizites

release nach copy (1-7,10) und nach alloc (3-22,24), autorelease nach copy (1-22), Schutz

durch ein Container-Objekt wie NSCountedSet (3-32), Zugriff auf ein Element eines

Container-Objekts, wobei man selbst nicht verantwortlich wird (3-44) und schließlich den

relativ trickreichen Export eines Objekts aus einem Pool mit retain und dann aus einer

Funktion mit autorelease (3-46,48). Alle diese Vorgänge spielen sich lokal innerhalb einer

Funktion ab.

Man beachte, daß new natürlich nach wie vor ein Synonym für einen Aufruf von alloc und

init darstellt, das heißt, wenn man ein Objekt mit new erzeugt, muß man es wie bei alloc

selbst mit release freigeben, wie das in freq für alle Pools geschehen ist.

Insgesamt ist für Foundation natürlich ein gewisser Lernaufwand erforderlich, aber bei über

50 Klassen für Datenstrukturen, Systemzugriff, Persistenz und Verteilung ist das kaum zu

vermeiden. Unicode und die dazu unabdingbaren Klassen NSString zur Verarbeitung und

NSData zum Transport sind sicher ein Fortschritt, aber mächtige Konzepte wie zum Beispiel

reguläre Ausdrücke stehen dafür (derzeit?), wie auch in Java, nicht zur Verfügung.


Ich hoffe, daß dieses Beispiel trotzdem demonstriert hat, daß es sich lohnt, eine derartige

Klassenbibliothek im Werkzeugkasten zu haben. Die Quellen sind, wie immer, über die

HTTP-Adresse im Impressum zu beziehen.

Weitere Magazine dieses Users
Ähnliche Magazine