Accueil > Programmation Cocoa > Gestion de la mémoire en Objective-C

Gestion de la mémoire en Objective-C

Par Mike Beam, le 27/07/2001

traduit par Thierry, le 24/05/2002

Aujourd’hui, nous allons faire la connaissance d’un sujet apparemment intimidant de la programmation : la gestion mémoire en Objective-C.

La gestion mémoire vous permet d’être sûr que l’espace précieux de la mémoire n’est plus détenu par des objets qui ne servent plus à rien dans votre application et que ceux, qui par contre sont dans le besoin, continuent d’exister. Après qu’un objet ait été créé puis utilisé, il devra être dépossédé de sa mémoire.

Le but de la gestion mémoire est de faire en sorte que votre application tourne comme une machine bien huilée. Quand un objet n’est plus sous le contrôle d’une application sans avoir été dépossédé, la mémoire est perdue. Cette perte de mémoire est appelée « fuite mémoire » et, en cas de fuites mémoires trop nombreuses, votre application finira par devenir un monstre d’une lenteur lamentable. S’il lui est permis de tourner indéfiniment, les fuites de mémoire généreront des besoins de plus en plus importants et, avec la façon dont est gérée la mémoire virtuelle sous Mac OS X, les accès disque deviendront de plus en plus fréquents pour combler les débordements de mémoire. Les accès disque ralentissant votre application, il est donc fortement conseillé de gérer proprement sa mémoire.

Les langages de programmation disposent des objets de manière différente. Java, par exemple, implémente un système de gestion mémoire connu sous le nom de collecte automatique des déchets. Avec ce système, le développeur n’a pas à  se soucier de déposséder les objets inutilisés puisqu’un collecteur caché de déchets teste en permanence chaque objet pour s’assurer qu’il est toujours utilisé par d’autres. Si le collecteur de déchets décide qu’un objet n’est plus en cours d’utilisation par quiconque, il le dépossèdera et libèrera ainsi la mémoire qu’il occupait.

Objective-C utilise une autre forme de gestion mémoire connue sous le nom de « compteur de référence ». Avec ce système, à  chaque objet est associé à  un compteur indiquant le nombre d’objets courants qui y accèdent, à  la différence près que les compteurs de référence de chaque objet doivent être gérés par le développeur. L’idée qui est développée ici consiste à  dire que dès l’instant où un objet a un compteur de référence à  « 0 », cela veut dire qu’aucun autre objet y accède et que l’objet en question peut être dépossédé de son espace mémoire.

Ces deux schémas de gestion mémoire ont des avantages et des inconvénients. La collecte automatique des déchets est facile pour le développeur de part son côté automatique. Cependant, cette facilité entraîne une perte de performance et d’efficacité. Le compteur de référence, à contrario, est très efficace et rapide car il ne suppose aucune tâche de fond consommatrice de ressource. L’inconvénient de cette méthode est qu’elle nécessite une implémentation spécifique de la part du développeur. Heureusement, cela n’est pas très difficile dès l’instant où la règle du jeu est comprise. Le but de cet article est de vous enseigner ces règles - comment contrôler le compteur de référence d’un objet en se servant des méthodes de Cocoa - pour que vous soyez sûr que les objets que vous supposez vivants le soient réellement et que ceux qui ne le sont plus soient proprement mis de côté.

Les méthodes Cocoa

La discussion suivante va être focalisée sur quatre méthodes définies dans la classe NSObject. (NSObject est la classe racine de Cocoa - le grand-père (grand-mère ?) de toutes les classes). Ces méthodes sont alloc, release, autorelease, et retain. Il y a plusieurs autres méthodes dans NSObject que vous pourrez trouver utiles dans votre application, donc prenez le temps de parcourir la documentation de cette classe.

Vous trouverez ci-dessous un tableau décrivant l’effet de chacune de ces quatre méthodes sur les objets :

Method

Description

+alloc Crée un nouvel objet (en allouant l’espace mémoire nécessaire) et retourne l’objet avec un compteur de référence à  « 1 ».
release Décrémente le compteur de référence du récepteur (l’objet qui reçoit le message) de « 1 ».
Autorelease Ajoute le récepteur au pool d’  « autorelease » de l’application, dont le but sera de décrémenter le compteur de référence du récepteur de « 1 » — à  de futurs moments.
retain Incrémente le compteur de référence du récepteur de « 1 ».

Nos outils

Pour nous aider à  comprendre la manière dont est gérée la mémoire en Objective-C, nous emploierons les services de deux outils. Un d’entre eux est une application qui nous permettra de lancer l’exécution de code dont le but est de montrer des exemples de bonne et mauvaise gestion mémoire. Vous pouvez télécharger cette application.

L’application est appelée MemApp. Son interface consiste en un simple bouton étiqueté « Go ! » qui est connecté à  une méthode d’action de la classe du contrôleur de l’application appelée goButton. La méthode d’action contient le code que nous manipulerons pour tester différentes situations de gestion mémoire. Additionnellement, j’ai créé une sous-classe de NSObject appelée AClass, qui est la classe dont nous allons créé une instance dans goButton. AClass n’ajoute aucune méthode supplémentaire et aucune variable d’instance à  celles définies dans NSObject; son but est de présenter un nom unique de classe qui se distinguera facilement parmis un océan de noms de classes. L’utilité de cette classe sera bientôt évidente.

ObjectAlloc

Le deuxième outil que nous allons utiliser est une application fournie par Apple et qui fait partie de la procédure d’installation des outils de développement. Elle est appelée ObjectAlloc, et peut être trouvée dans le répertoire /Developer/Applications/Performance Tools.

Cette application contrôle l’exécution de vos applications et crée une table à  partir de toutes les classes utilisées par votre application (et il y en a beaucoup). A chaque classe sont associés trois nombres : le nombre courant d’instances de la classe qui résident en mémoire, le pic formé par le nombre d’instances d’une classe donnée qui existent simultanément en mémoire, et finalement, le nombre total d’objets d’une classe qui ont été créés depuis le lancement de l’application.

Pour obtenir des informations relatives aux allocations de mémoire des objets d’une application particulière, vous devez d’abord lancer cette application à  partir de ObjectAlloc. Quand vous lancez ObjectAlloc, en premier lieu une boîte de dialogue vous est présentée pour vous demander quelle application doit être lancée. Notre application est localisée dans le dossier du projet MemApp au chemin suivant /MemApp/build/MemApp/Contents/MacOS/MemApp - où le premier MemApp est le nom du dossier du projet et le second celui du paquet applicatif (il portera une icône générique d’application dans la liste des fichiers). Le dernier MemApp est celui que nous voulons lancer (c’est aussi le seul que ObjectAlloc nous accordera de lancer). Sélectionnez le et cliquez sur le bouton « Open » de façon à  déclencher l’apparition de la fenêtre principale d’ ObjectAlloc.

Dans notre cas, nous allons sélectionner la boite à  cocher « auto-sort » située en bas de la fenêtre. Cela aura pour effet de trier alphabétiquement la liste des classes - quand AClass apparaîtra dans la liste des classes, elle sera positionnée directement en première position sous la ligne « TOTAL ». Les boutons situés en haut de la fenêtre contrôlent l’exécution de l’application.

Pour démarrer l’application, cliquez sur le premier bouton du haut : « Start Task ». Pour stopper l’application, cliquez sur le même bouton. Quand ObjectAlloc commencera à  tourner, vous verrez les classes apparaître au fûr et à mesure qu’elles seront instanciées. La classe intéressante pour notre expérience est, bien sûr, AClass, mais elle n’apparaîtra qu’après avoir cliqué sur le bouton dont le but sera d’en créer une instance. L’image ci-dessous illustre le montage de MemApp et de ObjectAlloc en exécution simultanée.

Notre objectif est ici de lancer l’application et d’observer combien d’instances de la classe AClass existent avant, pendant, et après l’exécution de la méthode d’action du bouton. Pour notre exercice, nous allons supposer qu’une fois terminée l’exécution de la méthode d’action nous n’aurons plus besoin d’aucune instance de la classe AClass. Voyons comment cela marche.

Le bien et le mal

Notre premier exemple est une situation dans laquelle nous n’interviendrons pas dans la gestion mémoire - la seule chose que nous allons faire est de créer des instances de AClass avec alloc et les laisser ainsi. Le fichier d’implémentation du contrôleur Controller.m contient la méthode d’action simple constituée du code suivant :

 -(IBAction)goButton:(id)sender { AClass *ourObject; ourObject = [AClass alloc]; }

Comme nous pouvons le voir, tout ce que faisons est de déclarer une variable de type AClass, puis nous l’assignons au nouvel objet créé avec alloc. Chargez l’exécutable dans ObjectAlloc et démarrez la tâche (assurez-vous que « auto-sort » est cochée en bas de la fenêtre). MemApp va alors ouvrir sa fenêtre principale. Cliquez sur le bouton « Go ! », et observez les mises à jour de la liste des classes, présentes en mémoire, faite par ObjectAlloc. AClass devrait surgir en haut de la liste.

Nous voyons ici que les compteurs Courant, Pic et Total enregistrent tous une instance de AClass présente en mémoire. Si vous cliquez de nouveau le bouton « Go ! », vous verrez chacun des compteurs augmenter à chaque appel de la méthode.

Malgré le fait que l’exécution du code soit terminée, les instances de AClass continuent de résider en mémoire. Ceci n’est pas bien pour plusieurs raisons. Tout d’abord, nous gâchons de la mémoire - il y a déperdition. Petite, soit, mais quand même ! Ensuite, nous sommes confrontés à de plus gros problèmes. Aprés avoir laissé le champ d’action de la méthode (le « scope » d’un nom ou d’une variable représente la partie du programme où le nom ou la variable peut être utilisé) où ourObject est déclaré, il n’y plus aucune manière disponible de communiquer avec la variable ou l’objet qui pointe vers. Nos objets sont perdus quelque part dans la mémoire hors de notre application - isolés et seuls au monde.

Si notre objet ourObject avait été déclarée comme variable d’instance avec un champ d’action global, la situation aurait été meilleure puisque nous pourrions toujours accéder l’objet vers lequel ourObject pointe - même s’il est placé en dehors de la méthode où il a été créé. La morale de l’histoire est de ne pas perdre trace des objets. Si vous créez un objet avec alloc, vous possédez cet objet et il est de votre responsabilité d’en disposer correctement quand vous n’en avez plus besoin. Assurez-vous de vous en débarrasser avant de ne plus l’avoir à l’esprit.

La manière de se débarrasser d’un objet que nous avons créé avec alloc tient dans l’envoi d’un message release ou autorelease. Chacune de ces deux méthodes aura pour effet de décrémenter de « 1 » le compteur de référence de l’objet, et compte tenu que ourObject n’a qu’une référence (en provenance d’ alloc), cela rendra nul son compteur et, par conséquent, éligible à la dépossession. Voyons maintenant comment nous pouvons nous servir de ces méthodes.

Le remède

La solution à ce problème est de détruire chaque instance créée avant d’en perdre le contact. Dans le code ci-dessus cela se traduirait par l’envoi d’un message « release » à ourObject avant de sortir du champ d’action de ourObject (la méthode dans ce cas). Plus tard nous verrons l’endroit adéquate pour désactiver les objets assignés aux variables d’instance. Ce que nous devons faire maintenant est d’envoyer à chaque objet actif un message release au moment où leur activité n’est plus utile. Retournons dans le fichier d’implémentation Controller.m de MemApp et changeons la méthode goButton tel que décrit dans le code ci-dessous :

 - (IBAction)goButton:(id)sender { AClass *ourObject; ourObject = [AClass alloc]; // Faire des choses avec ourObject é. [ourObject release]; ourObject = nil; }

Dans l’avant-dernière ligne, nous envoyons un message de désactivation à ourObject, et amenons son compteur de référence à « 0 » . La dernière ligne sert à deux choses importantes. Premièrement, elle nous aide à garder trace des variables qui pointent toujours vers des objets et de celles qui ne pointent vers aucun - une sorte de commodité comptable. Deuxièmement, et d’un point de vue plus pratique, elle nous aide à éviter les problèmes que pourraient provoquer l’envoie, par inadvertance, de messages vers des objets qui n’existent plus.

L’envoi d’un message vers un objet qui n’existe plus provoquera l’arrêt brutal de votre application avec un signal 10 (SIGBUS) ou 11 (SIGSEGV) alors que votre application ne bronchera pas d’un poil si un message est envoyé vers un objet vide nil. Si vous vous retrouvez face à ce type d’arrêt brutal, vérifiez que vous n’essayez pas de joindre un objet qui a disparu. De part ma propre expérience, j’ai constaté que l’envoi d’un message vers un objet inexistant provoquait un signal 11 de violation de segmentation, alors que la tentative de récupération de données provenant de tels objets provoquait un signal 10 d’erreur de bus.

Ouvrez le projet MemApp et ajouter ces lignes de codes dans le Controller.m, enfin compilez l’application. A partir de ObjectAlloc, arrêter l’exécution précédente de MemApp et redémarrez la (la nouvelle version va alors être lancée). Maintenant appuyez sur le bouton « Go ! » et observez les compteurs de AClass. Voici ce que nous obtenons :

Paramêtre Nombre d’objets
Courant 0
Plus haut 1
Total 1

Nous voyons ici qu’aucune instance ne reste de AClass en mémoire à la fin de notre méthode. Ceci est un exemple de gestion mémoire efficace. Maintenant nous allons regarder une autre façon, fournie par Cocoa, d’obtenir le même résultat avec plus de flexibilité.

Auto-déstruction

Une autre manière de diminuer le compteur de référence d’un objet consiste à lui envoyer un message d’ autorelease. Quand vous envoyez un message d’autorelease, le récepteur n’est pas immédiatement libéré de la mémoire contrairement au release. Autorelease diminue le compteur de référence du récepteur d’une unité à certains moments qui interviendront dans le future. Ainsi, il fournit plus de flexibilité dans la désactivation d’un objet et nous permet d’utiliser l’objet un peu plus longtemps avant qu’il ne soit réellement désactivé.

autorelease fonctionne en se servant d’un objet connu sous le nom de « bassin de déstruction automatique » (autorelease pool). Quand vous envoyez un message autorelease à un objet, ce dernier est ajouté au bassin. A la fin de la boucle événementielle, ce bassin est détruit par l’objet applicatif et de ce fait tous les objets contenus dans le bassin sont aussi détruits. Au démarrage de la boucle suivante, un nouveau bassin est créé et ainsi de suite. A des fins pratiques, un objet auto-destructif est considéré comme étant valide à n’importe quel endroit du champ d’action où il a reçu le message autorelease.

Par exemple, notre méthode associée au bouton de MemApp peut étre modifiée afin d’intégrer un autorelease, à la place d’un release.

- (IBAction)goButton(id)sender { AClass *ourObject; ourObject = [AClass alloc]; [ourObject autorelease]; // Nous pouvons toujours utiliser ourObject jusqu'à // la fin réelle de cette méthode ! }

Quand nous exécutons ce code au travers de ObjectAlloc, nous nous apercevons que les compteurs d’instances sont exactement les mêmes que ceux obtenus auparavant en employant un release. Cela ne devrait pas vous surprendre parce que l’objet ne sera détruit qu’aprés la fin de la méthode.

autorelease est spécialement utile quand nous avons une méthode qui crée et retourne un objet. Dans cette situation, si nous suivons les régles, nous devons nous assurer que chaque objet créé est détruit avant qu’il ne soit trop tard (une fois que l’objet créé est sorti du champ d’action de la méthode).

L’utilisation d’un release ne serait pas opportun dans ce cas car l’objet serait détruit avant qu’il ne soit retourné.

- (NSString)releasedString { NSString *string = [[NSString alloc]  initWithString:@"Le récepteur de ce message ne verra jamais cette chaîne de caractères..."]; [string  release]; // nous devons faire cela ou nous aurons une fuite de mémoire return string;   // mais que renvyons nous ?  }

La solution à ce probléme est l’emploi d’un autorelease qui nous permet de traiter correctement les objets que nous créons et possédons, tandis que nous donnons, à l’émetteur du message, une chance de s’emparer de l’objet retourné pour qu’il s’en serve à ses propres fins.

- (NSString)autoreleasedString { NSString *string = [[NSString  alloc]  initWithString:@"Le récepteur de ce message ne verra jamais cette chaîne de caractères..."]; [string  autorelease];  // nous devons faire cela ou nous aurons une fuite de mémoire return  string; }

Nous pouvons maintenant retourner une chaîne à l’émetteur du message et laisser notre esprit avec la confortable impression d’avoir accompli notre devoir de prévention de pertes de mémoire. Ces quelques derniers exemples prodiguent une excellente transition vers notre nouveau sujet - les constructeurs de commodité - qui s’intégre dans le moule d’une méthode dont le but est de créer et de retourner un objet auto-destructif.

Constructeurs de commodité

A ce moment, vous avez sûrement noté dans la documentation des classes que la plupart des classes Cocoa détiennent un ensemble de méthodes dont le nom est formaté sur le mode +className…. Ces méthodes spéciales sont appelées « Constructeurs de commodité » (« Convenience Constructors ») et sont utilisées pour créer des objets temporaires. Ce que j’entends par temporaire veut dire que les objets créés par un constructeur de commodité sont supposés être auto-détructifs, et ne sont, par conséquent, valides qu’à  l’intérieur de la méthode qui fait usage de ce type de constructeur.

Un exemple d’un des nombreux constructeurs de commodité disponibles dans la méthode NSString est +stringWithCString:. Nous pouvons utiliser cette méthode au lieu de se servir du process classique alloc/init pour créer une instance de NSString.

Par exemple, considérons le code qui utilise alloc et init:

NSString *string = [[NSString alloc initWithCString:"Hello"]; [textField  setStringValue:string]; [string release];

Il pourrait être raccourci en utilisant un constructeur de commodité :

NSString *string = [NSString stringWithCString:"Hello"]; [textField setStringValue:string]; // pas besoin de détruire la chaîne puisque les objets  // retournés par les constructeurs de commodité sont   // supposés être auto-destructifs.

Ces constructeurs peuvent aussi le rendre encore moins encombrant en imbriquant les messages :

[textField setStringValue:[NSString stringWithCString:"Hello]];

Donc, si vous avez besoin d’une chaîne (ou d’un autre objet) dont la durée de vie n’ira pas au delà  de la méthode, il est souvent plus facile, plus propre, et plus lisible d’utiliser un constructeur de commodité.

Si vous voulez valoriser une variable d’instance en vous servant d’un constructeur de commodité, vous devez lui envoyez un message retain de manière à  ce qu’elle ne soit pas détruite automatiquement au moment de la purge du pool d’autorelease, comme suit :

// on supposse que stringInstanceVariable existe stringInstanceVariable  = [[NSString stringWithCString:"Hello"] retain];

L’utilisation de retain est une manière de vous attribuer la possession d’objets que vous n’avez pas créés, ce qui par conséquent étend notre obligation de vérifier qu’à chaque alloc correspond un release ou un autorelease, puisque maintenant nous devons aussi le vérifier pour chaque message retain.

En savoir plus sur la gestion mémoire

Dans cet article j’ai essayé de couvrir les aspects basiques de la gestion mémoire sous Cocoa et en Objective-C. Il y a de nombreux autres articles et références disponibles que j’encourage fortement de lire. Ces articles vont plus dans le détail, fournissent des exemples supplémentaires de codes, et couvrent une grande variété de pièges auxquels vous devriez être attentifs. Voici une liste de ces références :

Règles très simples de gestion mémoire sous Cocoa par Malcolm Crawford, fournit un bon résumé de ce que l’on a discuté ici, inclue beaucoup d’exemples qui montrent comment éviter les pertes de mémoire dans votre code.

Prenez moi, utilisez moi, libérez moi par Don Yacktman, est un aperçu détaillé du problème de la gestion mémoire et comment il est traité sous Cocoa et en Objective-C.

Programmation orientée objet et le langage Objective-C, Chapitre 6, « Mode d’acquisition des objets et dispositions automatiques » (« Object Ownership and Automatic Disposal ») de la firme Apple. Quiconque voulant apprendre sérieusement Cocoa doit lire ce livre. Les éditions parues après décembre 2000 contiennent le chapitre relatif à la gestion mémoire. Ce livre aborde beaucoup de sujets déjà  traités ici, mais vient directement des développeurs d’Apple. Il met aussi l’accent sur l’idée de possession des objets. Vous n’aurez jamais trop de références !

Les documents de référence sur La Classe NSObject et La Classe NSAutoreleasePool peuvent être trouvés sur le site d’Apple. Les méthodes +alloc,-release,-retain, et -autorelease sont toutes définies et décrites sur ce site. Le document de référence sur NSAutoreleasePool détaille plus les opérations de ces pools et décrit la manière de créer votre propre pools d’autodestruction et le moment où vous devrez le faire.

Bien, c’est tout pour maintenant. J’espère que je vous ai donné une bonne idée de la manière dont fonctionne le comptage de références et la gestion de la mémoire en Objective-C sous Cocoa. Si vous êtes toujours un peu dubitatif, ne vous inquiétez pas trop. Dans les prochains articles nous allons construire une application dans laquelle nous allons suivre les règles d’une bonne gestion mémoire, ainsi vous pourrez voir comment nous implémentons tout ça dans du code réel. Dans l’article suivant, nous allons monter l’interface de cette application et apprendre quelques trucs sur les tables présentes dans les interfaces et, sur la classe NSTableView.


Textes originaux en anglais sur O’Reilly : http://www.macdevcenter.com/pub/au/159

Thierry Programmation Cocoa , , ,

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