Introduction à la Gestion de la Mémoire
Durant l'exécution d'un programme, de nombreux objets sont instanciés. Comme dans la plupart des langages de haut niveau, les objets en Objective-C sont alloués dans le tas (heap). Contrairement aux types primitifs gérés automatiquement sur la pile (stack), le système n'efface pas automatiquement la mémoire du tas. Une absence de libération explicite entraîne une consommation croissante de la mémoire vive. Alors que des environnements comme Java ou C# utilisent un garbage collector (GC), Objective-C repose historiquement sur une gestion manuelle ou semi-automatique via le comptage de références.
Pour comprendre les mécanismes sous-jacents, il est essentiel de maîtriser la gestion manuelle (MRC - Manual Reference Counting), même si les versions modernes d'Xcode utilisent l'ARC (Automatic Reference Counting). L'ARC insère automatiquement les instructions de libération, mais comprendre le fonctionnement manuel reste crucial pour optimiser les performances et déboguer les fuites de mémoire.
Le Compteur de Références
Chaque objet Objective-C possède un compteur interne (retainCount). Lorsqu'un objet est créé via alloc, new, copy ou retain, ce compteur est incrémenté. L'appel à la méthode release le décrémente. Lorsque le compteur atteint zéro, le système invoque automatiquement la méthode dealloc pour détruire l'objet.
Voici un exemple illustrant le cycle de vie d'un objet :
// Utilisateur.h
#import <Foundation/Foundation.h>
@interface Utilisateur : NSObject
@property (nonatomic, copy) NSString *prenom;
@property (nonatomic, assign) NSInteger age;
@end
// Utilisateur.m
#import "Utilisateur.h"
@implementation Utilisateur
- (void)dealloc {
NSLog(@"Destruction de l'objet Utilisateur");
[super dealloc];
}
@end
// main.m
#import <Foundation/Foundation.h>
#import "Utilisateur.h"
void exempleCompteur() {
Utilisateur *utilisateur = [[Utilisateur alloc] init];
utilisateur.prenom = @"Alice";
utilisateur.age = 30;
NSLog(@"Compteur: %lu", [utilisateur retainCount]); // Affiche 1
[utilisateur retain];
NSLog(@"Compteur après retain: %lu", [utilisateur retainCount]); // Affiche 2
[utilisateur release];
NSLog(@"Compteur après release: %lu", [utilisateur retainCount]); // Affiche 1
[utilisateur release]; // Déclenche dealloc
utilisateur = nil; // Bonne pratique pour éviter les pointeurs sauvages
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
exempleCompteur();
}
return 0;
}
Il est impératif de mettre le pointeur à nil après la libération. Tenter d'envoyer un message à un objet déjà détruit provoque une erreur d'accès mémoire (EXC_BAD_ACCESS). En Objective-C, envoyer un message à nil est sûr et ne génère pas d'exception.
Règles de Propriété et Possession
La règle fondamentale est : "Celui qui alloue doit libérer". Cependant, dans des structures complexes où les objets se référencent mutuellement, la gestion devient délicate. Considérons un scénario où un Conducteur possède un Vehicule.
// Vehicule.h
#import <Foundation/Foundation.h>
@interface Vehicule : NSObject
@property (nonatomic, copy) NSString *plaque;
- (void)demarrer;
@end
// Vehicule.m
#import "Vehicule.h"
@implementation Vehicule
- (void)demarrer {
NSLog(@"Le vehicule %@ demarre", self.plaque);
}
- (void)dealloc {
NSLog(@"Destruction Vehicule %@", self.plaque);
[super dealloc];
}
@end
// Conducteur.h
#import <Foundation/Foundation.h>
@class Vehicule;
@interface Conducteur : NSObject {
Vehicule *_vehicule;
}
@property (nonatomic, copy) NSString *nom;
- (void)setVehicule:(Vehicule *)vehicule;
- (Vehicule *)vehicule;
@end
// Conducteur.m
#import "Conducteur.h"
#import "Vehicule.h"
@implementation Conducteur
- (void)setVehicule:(Vehicule *)vehicule {
if (_vehicule != vehicule) {
[_vehicule release]; // Libérer l'ancienne référence
_vehicule = [vehicule retain]; // Conserver la nouvelle
}
}
- (Vehicule *)vehicule {
return _vehicule;
}
- (void)dealloc {
[_vehicule release];
[super dealloc];
}
@end
Dans le setter setVehicule:, il est crucial de libérer l'ancien objet avant d'en retenir un nouveau. Si l'on assignait directement sans retain, l'objet pourrait être détruit prématurément par le contexte appelant. Inversement, sans libérer l'ancien, une fuite de mémoire se produirait.
Attributs des Propriétés
L'utilisation de @property permet d'automatiser la génération des accesseurs. Les attributs définissent le comportement de la mémoire :
- atomic / nonatomic :
atomicgarantit la sécurité des threads (verrouillage), tandis quenonatomicest plus performant mais non sûr en contexte multithread. - assign : Simple assignment, utilisé pour les types primitifs (int, float).
- retain : Incremente le compteur de références lors de l'assignment. Utilisé pour les objets NSObject (sauf NSString).
- copy : Crée une copie de l'objet. Indispensable pour
NSString,NSArray,NSDictionaryet les blocks afin d'éviter les modifications externse.
Par défaut, les propriétés sont (atomic, readwrite, assign). Pour un objet personnalisé, (nonatomic, retain) est souvent préféré en MRC.
Pool de Libération Automatique (Autorelease)
L'objectif-c propose un mécanisme semi-automatique via @autoreleasepool. Un objet marqué comme autorelease n'est pas libéré immédiatement, mais ajouté à une pool. Lorsque la pool se vide (à la fin du bloc), un message release est envoyé à tous ses membres.
Cela permet de retourner des objets depuis des méthodes utilitaires sans imposer la charge de la libération à l'appelant, tant que celui-ci ne conserve pas l'objet indéfiniment.
// FabriqueUtilisateur.m
#import "Utilisateur.h"
@implementation Utilisateur
+ (Utilisateur *)utilisateurAvecNom:(NSString *)nom {
Utilisateur *u = [[Utilisateur alloc] init];
u.prenom = nom;
[u autorelease]; // Délègue la libération à la pool
return u;
}
@end
// main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
Utilisateur *u1 = [[Utilisateur alloc] init];
[u1 autorelease]; // Sera libéré à la fin du pool
Utilisateur *u2 = [Utilisateur utilisateurAvecNom:@"Bob"]; // Déjà autorelease
Utilisateur *u3 = [Utilisateur utilisateurAvecNom:@"Charlie"];
[u3 retain]; // Annule l'effet de l'autorelease, responsabilité de libération transférée
}
// u1 et u2 sont libérés ici. u3 persiste car retainé (fuite si non libéré plus tard).
return 0;
}
Il est important de noter que autorelease ne modifie pas le compteur de références immédiatement, il planifie simplement une future décrémentation. Si un objet est créé danss une boucle intensive, il est préférable de créer des pools locales pour éviter une saturatoin mémoire avant la fin du bloc englobant.