Accueil > Développement > La gestion de la mémoire en Objective-C

La gestion de la mémoire en Objective-C

Par Renaud Préat, le 09/05/2003

La gestion de la mémoire consiste à spécifier lors de l’écriture du programme, quand allouer de la mémoire pour une variable, quand supprimer l’espace mémoire occupé par cette variable, quand le conserver,…. C’est une contrainte dont il faut tenir compte lors de l’écriture du programme et dont l’importance diffère suivant le langage utilisé. Dans certains langages, conçus pour être simples à apprendre et à utiliser comme le Basic et ses variantes, le programmeur peut oublier la gestion de la mémoire, mais en contrepartie, le programme sera relativement lourd, et consommera une partie importante de la mémoire, même si il est extrêmement simple. D’autres langages, souvent qualifiés de difficiles, comme par exemple le C et ses variantes, demandent au programmeur de s’en occuper lui-même, ce qui fait que si le programme est bien écrit, il ne consommera que la mémoire dont il a besoin.

La gestion de la mémoire peut se faire de deux façons en Objective-C : on peut avoir une gestion explicite de la mémoire, comparable à celle qu’on retrouve en C++ où le programmeur doit lui-même spécifier quand garder ou supprimer un objet, ou bien sous une forme comparable à Java où les objets sont libérés automatiquement lorsqu’ils ne sont plus utilisés. Les deux coexistent, mais dans la mesure où les messages envoyés pour la gestion de la mémoire sont différents, il n’y a pas de confusion possible.

Gestion explicite

Cette première partie de l’article expliquera la gestion explicite de la mémoire, où le programmeur doit lui même s’occuper de la gestion de la mémoire, ou spécifier lui même quelles sont les variables qu’il faudra éliminer, copier ou garder. Commençons par un peu de théorie.

Chaque objet dans votre application Cocoa a une valeur interne connue sous le nom retain count. Ce nombre est simplement un entier qui donne le nombre d’autres objets « intéressés par » l’objet. Par exemple, vous pouvez avoir un NSArray utilisé par quatre autres objets, le retain count du NSArray sera quatre. Quand un objet ne s’intéressera plus au NSArray, son retain count chutera à trois. Si un autre objet se présente, a de l’intérêt, il fera grimper le retain count à quatre. Il peut être important de spécifier une chose importante dès le départ : en Objective-C une variable ne désigne jamais un objet, mais un pointeur vers cet objet. De façon simplifiée, un pointeur désigne la zone de la mémoire où trouver un objet, un peu comme l’adresse d’une maison indique l’endroit où elle se trouve, mais n’indique pas la maison elle-même, on peut imaginer qu’avec le temps un commerce se trouvera à cette place, ou un entrepôt. Il faut donc veiller à ce que l’objet désigné par un pointeur soit du même type que ce qui est attendu, si votre gestion de la mémoire est correcte, ce sera toujours le cas.

Les objets manifestent leur intérêt, ou leur désintérêt, grâce aux méthodes retain et release. retain incrémente le retain count de l’objet désigné par le pointeur et renvoie un pointeur vers cet objet, release le décrémente. Lorsque le retain count de l’objet atteint zéro, il est libéré (désalloué, effacé de la mémoire, comme vous voulez). Attention cependant, on ne peut pas envoyer de message release à un objet dont le retain count vaut 0, faute de quoi le programme plantera.

Par exemple, prenons une phrase (aString), dont le retain count vaut au départ deux :

anotherString = [aString retain]; //le retain count devient trois

//un peu de code

[anotherString release]; //le retain count devient deux

Dans la première ligne, nous créons un pointeur vers aString, puis attribuons à anotherString ce pointeur. Comme anotherString « manifeste son intérêt » envers aString, le retain count augmente de un. Un peu plus tard dans le code, anotherString n’est plus nécessaire, on la libère, et le retain count diminue de un. Bien entendu, les objets qui viennent d’être créés ont un retain count qui vaut un:

MyObj *someObj = [[MyObj alloc] init];

La méthode alloc renvoie un objet avec un retain count valant un et init initialise ensuite les variables d’instance de l’objet.

Autorelease pool

Dans certains cas, la gestion explicite de la mémoire peut s’avérer assez peu pratique pour le développeur. Il existe un autre mécanisme de gestion de la mémoire en Objective-C, qui permet au programmeur de ne plus s’en soucier, du moins dans une certaine mesure. Cette méthode consiste à placer les objets dans l’autorelease pool. Un objet placé dans l’autorelease pool sera automatiquement libéré « un peu après » la fin de l’exécution de la méthode dans laquelle il a été créé, ce qui rend ce type d’objet très pratique pour tous les objets qui ne sont utilisés que durant le temps d’exécution de la méthode, comme une NSString qui sera affichée dans un message d’alerte, un énumérateur pour traiter un tableau…

Pour placer un objet dans l’autorelease pool, deux possibilités s’offrent à vous : la première, la plus simple, consiste à utiliser une méthode de commodité qui le fera d’elle-même, ou bien le faire explicitement.

Utiliser une méthode de commodité est la solution la plus simple. Prenons un exemple : la méthode dictionary de NSDictionary renvoie un dictionnaire, vide, initialisé et placé dans l’autorelease pool. Si on s’intéresse à la classe NSDictionary, on remarquera qu’il existe d’autres méthodes qui peuvent être utilisées en ce sens :

+ dictionaryWithContentsOfFile:
+ dictionaryWithDictionary:
+ dictionaryWithObject:forKey:
+ dictionaryWithObjects:forKeys:
+ dictionaryWithObjects:forKeys:count:
+ dictionaryWithObjectsAndKeys:

Ces méthodes renvoient toute un dictionnaire rempli (pour peu que les arguments soient valables et ne soient pas nil) initialisé et placé dans l’autorelease pool. En plus de placer le dictionnaire dans l’autorelease pool, ces méthodes de commodité sont également très pratiques, car elles permettent d’éviter d’écrire du code rébarbatif. Illustrons cela par exemple :

// Exemple 1 - Alloc et Init
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict addObject:@"obj1" forKey:@"key1"];
[dict addObject:@"obj2" forKey:@"key2"];
[dict addObject:@"obj3" forKey:@"key3"];

//fin de la méthode
[dict release];

// Exemple 2 – Méthode de commodité
NSDictionary *dict = [[NSDictionary dictionaryWithObjectsAndKeys: @"obj1",
  @"key1", @"obj2", @"key2", @"obj3", @"key3", nil];

Cet exemple est assez explicite pour montrer l’intérêt de telles méthodes. La question est maintenant de savoir comment les reconnaître dans l’aide fournie ? Tout d’abord, il doit s’agir de méthodes de classes, identifiables par le symbole “+” qui précède le nom de la méthode. Par convention, toutes les méthodes de classe dont le nom commence par le nom de la classe (moins le NS) sont des méthodes qui renvoient un objet initialisé et placé dans l’autorelease pool, comme +stringWithContentsOfFile: pour NSString, ou encore +arrayWithObjects: pour NSArray, ou encore…

Il est également possible de placer soi-même des objets dans l’autorelease pool, en leur simplement envoyant un message autorelease. Bon, et que devient le retain count dans tout cela ? En fait, il ne change pas, mais, comme l’objet est placé dans l’autorelease pool, il recevra le message release « un peu après » la fin de l’exécution de la méthode, sans que le programmeur n’ai plus à s’en soucier.

Comme je n’ai cessé de le répéter, les objets placés dans l’autorelease pool sont libérés un peu après l’exécution de la méthode. Si on désire les conserver bien après l’exécution de la méthode, c’est possible, il suffit, comme pour n’importe quel objet qu’on désire conserver, de leur envoyer un message retain, et, comme pour n’importe quel objet “conservé”, il ne faudra pas oublier par la suite de leur envoyer un message release ou autorelease pour que l’objet soit libéré, tout en veillant à ce que le retain count ne soit pas inférieur à 0.

Collections

Comme on l’a vu, les variables sont en fait des pointeurs vers des espaces mémoires donnés. Les éléments d’une collection (terme générique pour désigner NSArray, NSDictionary ou NSSet) ne font pas exception à cette règle. Quand on ajoute un objet dans une collection, celle-ci lui envoie un message retain, afin de s’assurer que l’objet sera disponible si il a été libéré ailleurs. Lorsque la collection est libérée, elle envoie un message release à tous ces éléments, pour qu’ils puissent être libérés si nécessaire.

En conséquence, un élément dans une collection peut être modifié séparément. Illustrons par un exemple :

NSMutableString *aString = [NSMutableString stringWithString:@"phrase1"];
NSArray *anArray = [NSArray arrayWithObject:aString];
NSLog([anArray objectAtIndex:0]); //renverra phrase 1
[aString setString:@"phrase2"];
NSLog([anArray objectAtIndex:0]); //renverra phrase 2

Dans un premier temps, nous créons un pointeur vers une phrase. On crée ensuite un tableau, dont l’élément est un pointeur qui désigne un autre pointeur, qui désigne lui la phrase. Si on modifie la phrase, la valeur désignée par les pointeurs change et donc, en quelque sorte, la valeur contenue dans le tableau change également.

Il y a des fois où vous désirerez éviter ce phénomène. La valeur « contenue dans le tableau » ne pouvant être modifiée que en passant par le tableau, et non par un pointeur externe. Dans ces cas, vous pouvez utiliser la méthode copy. copy fait une copie de l’objet et la renvoie (ou plus exactement un pointeur vers elle) avec un retain count valant un. Le code devient donc :

NSMutableString *aString = [NSMutableString stringWithString:@"phrase1"];
NSArray *anArray = [NSArray arrayWithObject:[aString copy]];
NSLog([anArray objectAtIndex:0]); //renverra phrase 1
[aString setString:@"phrase2"];
NSLog([anArray objectAtIndex:0]); //renverra phrase 1

Il y a juste encore un léger problème : la copie de la phrase restera en mémoire après que le tableau ait été libéré, car les objets créés par  copyne sont pas placés dans l’autorelease pool. Pour que notre exemple soit parfait, il faut donc envoyer un message autorelease à la copie. L’exemple final donnera :

NSMutableString *aString = [NSMutableString stringWithString:@"phrase1"];
NSArray *anArray = [NSArray arrayWithObject:[[aString copy] autorelease]];
NSLog([anArray objectAtIndex:0]); //renverra phrase 1
[aString setString:@"phrase2"];
NSLog([anArray objectAtIndex:0]); //renverra phrase 1

Avant de passer à la suite, je voudrais faire une petite remarque concernant les expressions assez fréquemment rencontrés de type NSString *aString = @”une pharse quelconque”; qui semblent incomplètes par rapport à ce qui a été expliqué précédemment, puisque aucun mécanisme de gestion de la mémoire n’est impliqué. Dans ce genre de cas, on crée en fait une NSString statique, qui n’obéit pas aux règles énoncées. C’est très simple en fait, pour ce type d’objet, il est inutile de se soucier de la mémoire. Cependant, il vous est toujours possible d’envoyer des messages retain si vous désirerez conserver l’objet, mais n’oubliez pas les release ou autorelease correspondants…

Méthodes d’accès

Les méthodes d’accès servent à accéder aux données d’un objet. Examinons la méthode marque de la classe Voiture.

- (NSString *)marque
{
	return marque;
}

C’est une simple méthode d’accès. On renvoie simplement un pointeur vers une variable d’instance, ce qui est justement problématique, puisque par convention de la programmation objet, les variables d’instance ne peuvent être accessibles que par l’objet lui-même. Le mieux est donc d’envoyer une copie de la variable d’instance, puis de la placer dans l’autorelease pool, ce qui donne:

- (NSString *)marque
{
	return [[marque copy] autorelease];
}

Dans certains cas, il vous arrivera de renvoyer une valeur calculée par la méthode plutôt qu’une variable d’instance. Par exemple, nous désirons construire une méthode qui renvoie un NSMenu. Ce qui pourrait donner :

-(NSMenu*)menu
{
	NSMenu *aMenu = [[NSMenu alloc] init];
	…//Le code pour construire le menu
	return aMenu;
}

Il y a un problème. aMenu a été déclaré au sein de la méthode, mais n’a pas été libéré. La solution consiste à envoyer un message autorelease à aMenu au moment où il est renvoyé, de cette façon, on est sûr que l’objet sera libéré « un peu après » la fin de la méthode. Vous avez remarqué que j’insiste constamment sur le fait que l’objet est libéré « un peu après » et non après la fin de la méthode, cette précision était en fait destinée aux méthodes d’accès. Envoyer un message release effacerait l’objet pointé par aMenu avant que le destinataire de la méthode menu puisse le lire, alors que autorelease accorde un petit délai, permettant ainsi de copier l’objet avant qu’il ne soit effacé. Le code devient donc :

-(NSMenu*)menu
{
	NSMenu *aMenu = [[NSMenu alloc] init];
	…//Le code pour construire le menu
	return [aMenu autorelease];
}

Bien entendu, si l’objet que vous renvoyez a été créé avec une méthode de commodité qui place l’objet dans l’autorelease pool, il est inutile d’envoyer le message autorelease.

Modifier une variable d’instance

La modification de variable d’instance peut aussi causer de gros problèmes de mémoire. Par exemple, vous avez un objet tâche dont vous désirez modifier la description.

- (void)setDescription:(NSString*)newDesc
   {
   description = newDesc;
   }

C’est trop simple que pour être bon… description est un pointeur, qui désigne un espace mémoire contenant la description, ou plus exactement l’ancienne description. En procédant de cette sorte, il y aura des fuites de mémoire, puisqu’à chaque changement de description, on ne libéra pas l’ancienne. Essayons encore :

- (void)setDescription:(NSString*)newDesc
   {
   [description release];
   description = newDesc;
   }

Il y a encore un gros problème : si newDesc est libéré, le pointeur description pointera vers un espace vide. La correction donne :

- (void)setDescription:(NSString*)newDesc
   {
   [description release];
   description = [newDesc retain];
   }

On pourrait croire que c’est bon, mais il y a encore quelques problèmes. Le premier, description pointe vers le même espace mémoire que newDesc, une modification de l’un entraînera une modification de l’autre, ce qui n’est pas vraiment le but désiré. La solution consiste à utiliser copy plutôt que retain. Il y a encore un problème (le dernier, juré craché). Ce code fonctionnera dans tous les cas sauf un : si on demande de remplacer la description par elle même ([uneTache setDescription:[uneTache description]]). Dans la première ligne, nous libérons decription, et dans la seconde nous faisons appel à ce même objet mais comme il est libéré, il n’est plus disponible, ce qui fait que le plantage est assuré. On peut passer par un pointeur pour résoudre le problème, ce qui donne :

- (void)setDescription:(NSString*)newDesc
   {
   NSString *ancienneDesc = description;
   description = [newDesc copy];
   [ancienneDesc release];
   }

Tous les problèmes sont enfin résolus. On crée dans un premier temps un pointeur vers l’espace mémoire de description, mais sans augmenter le retain count de l’objet anciennement pointé par description (puisque aucun retain n’a été envoyé). On fait ensuite pointer description vers le nouvel objet, et on libère l’ancienne description.

Libération de variables d’instance

Avant de terminer, il nous reste à parler d’un dernier point sur les variables d’instance. Ces variables sont déclarées dans l’en-tête d’une classe, et doivent normalement être disponibles pour l’ensemble des méthodes de la classe. Mais comme pour tout autre objet, elles se doivent d’être libérées de la mémoire une fois qu’on n’en a plus besoin. La meilleure façon de libérer une variable d’instance consiste à redéfinir la méthode dealloc existant pour toute sous-classe de NSObject. Un message dealloc est automatiquement envoyé lorsqu’un objet doit être « éliminé ». Dans l’implémentation de la classe, il faut donc rajouter :

- (void)dealloc {
   [uneVariable release];
   [uneAutre release];
   …
   [super dealloc];
   }

Le message [super dealloc] est très important, car il libérera les variables d’instance héritées de la classe-mère. On est donc sûr qu’il ne restera plus rien de l’objet en mémoire.

On n’envoie jamais un message dealloc directement à un objet (sauf pour super dans la méthode dealloc), cette méthode étant « réservée au système ».

Récapitulatif

Voici un petit récapitulatif des différentes méthodes vues dans cet article :

- alloc : alloue la mémoire pour un objet, attribue un au retain count et renvoie un pointeur vers cet objet.
- retain : augmente le retain count de un et renvoie un pointeur vers l’objet.
- release : diminue le retain count de un, mais ne renvoie rien.
- autorelease : décrémentera le retain count de un « un peu après » la fin de la méthode.
- copy : crée une copie de l’objet, attribue un au retain count de la copie, et renvoie un pointeur vers cette copie.

Maintenant que les principaux concepts ont été expliqué, on peut donner quelques règles relatives à la gestion de la mémoire. Elles ont pour la plupart déjà été introduites au cours de l’exposé :

- Si vous créez, copiez ou conservez un objet, vous êtes responsable de le libérer. Si vous recevez un objet, vous n’êtes pas responsable de sa libération.
- Si vous devez redéfinir une variable d’instance, vous devez conserver ou copier l’objet utilisé pour définir la nouvelle valeur.
- Ne jamais envoyer autorelease ou release à un objet déjà libéré.

Conclusion

Avec les concepts introduits dans cet article, vous devriez pouvoir gérer la mémoire dans la majorité des cas auxquels vous serez confronté. Cet article a cependant des limites, certaines notions devront être redéfinies dans les cas des processus multi-tâches ou des objets distribués.

Bon code !

© Juin 2003 Renaud pour Project:Omega

renaud Développement , ,

  1. Pas encore de commentaire
  1. Pas encore de trackbacks
Vous devez être identifié pour poster un commentaire