Bonjour à tous !
Vous n’avez jamais rencontré de problèmes avec la gestion mémoire dans votre programme ? Ca ne va pas tarder
C’est pourquoi il vaut mieux s’en préoccuper en amont, plutôt que de fouiller dans votre code pour chercher la cause d’un bug.
Par ailleurs, vous remarquerez très vite qu’une erreur de gestion mémoire entraîne un nombre conséquents de bugs, tous liés !
Je précise que si vous êtes débutants, ce “tuto” va vous paraître hard… N’hésitez pas à le relire plusieurs fois et bien faire par vous-mêmes les manips proposées !
Nous allons donc regarder ensemble les pièges à éviter, les outils à utiliser ainsi que les réflexes de codage à utiliser.
Le développement sous iPhone se fait sans garbage collector (ramasse miettes). C’est à dire qu’il n’y a pas une “entité” qui se charge de désallouer automatiquement les objets inutiles. Il faut tout faire à la main, et bien savoir ce que l’on fait et où on va. Notez que c’est à force de pratique que vous comprendrez tous les mécanismes.
a) Qu’est-ce que la mémoire ?
Comment-ça, mon appli elle a de la mémoire ? Ca se cache où ?
En fait, lorsque vous créez un objet, ou que vous déclarez une variable, l’OS se charge de réserver des cases mémoires pour stocker les données sous formes de bits.
Je vous encourage vivement à regarder un article du site du zéro sur la mémoire.
Et comme ce site est vraiment bien fait, replongez-vous dans l’univers des pointeurs : A l’assaut des pointeurs
Voilà, adresse mémoire, valeur pointée, pointeurs de tableaux n’ont plus aucun secrets pour vous normalement !
b) Et sur mon iPhone ?
En langage C, souvenez-vous, vous deviez gérer les allocations mémoire à la main, avec malloc, free. Cela marche toujours en Objective-C, mais rassurez-vous, cela a été simplifié, allégé !
En fait en Cocoa Touch, vous allez faire face à des termes comme “retain”, “release”, “copy”, “alloc”, “autorelease”, “leaks”, …
Kesako ? Et bien nous allons regarder de plus près tout cela ensemble ! Et dans la bonne humeur !
Commençons tout d’abord par expliquer les termes :
Chaque objet a ce que l’on appelle un “compteur de référence” (je parlerai de “retain count” de temps en temps) : c’est un nombre, positif ou nul, qui informe du nombre d’allocations d’un objet.
– retain augmente de 1 le compteur de référence d’un objet
– release diminue de 1 le compteur de référence d’un objet
– autorelease diminue de 1 le compteur de référence d’un objet à un certain moment dans le futur
– alloc alloue de la mémoire pour un objet, et le retourne avec un compteur à 1
– copy fais une copie d’un objet et le retourne avec un compteur à 1
a) Parlons de tout (sauf autorelease)
Oui, autorelease est un mécanisme est un peu particulier, donc on regardera plus tard.
La trame de vie d’un objet est la suivante :
Création
Gestion mémoire
Destruction
Création
Pour la création, il faut allouer de la mémoire en vue de stocker cet objet, puis on initialise l’état de l’objet.
MyObject *object = [[MyObject alloc] init];
// allocation mémoire puis initialisation
Gestion mémoire
Ensuite, on gére la mémoire :
On peut par exemple écrire (aucun intérêt)
MyObject *object = [[MyObject alloc] init];
// retain count à 1
[object retain];
// retain count à 2
[object release]
// retain count à 1
[object release]
// retain count à 0 -> l'objet est détruit
Maintenant, créez un projet, et créez une classe NSObject “AnObject” telle que :
AnObject.h
#import <Foundation/Foundation.h>
@interface
AnObject :
NSObject
{
NSString
*aString;
}
- (
void
) doSomething;
@end
AnObject.m
@implementation
AnObject
- (
id
)init {
if
(
self
= [
super
init])
{
aString = [[
NSString
alloc] initWithString:@
"Toto"
];
}
return
self
;
}
- (
void
)doSomething {
NSLog
(aString);
}
- (
void
) dealloc {
NSLog
(@
"%@ dealloc"
, [
self
class
]);
[aString release];
[
super
dealloc];
}
Ensuite, dans un viewDidLoad par exemple, mettez ce code :
AnObject *object = [[AnObject alloc] init];
[object release];
[object doSomething];
[
super
viewDidLoad];
Lancez, vous avez ceci dans la console :
objc[71815]: FREED(
id
): message doSomething sent to freed object=0xd07850
Eh bien oui, on fait un release sur l’objet, que l’on détruit, puis on appelle une méthode de cet objet. Ca ne peut pas marcher ! On parle de zombie.
Par contre, si vous faites :
AnObject *object = [[AnObject alloc] init];
[object release];
object =
nil
;
// rajout de cette ligne
[object doSomething];
Vous voyez que ça ne “plante” pas. Par contre, cela ne fait rien. Pensez-y !
Le comportement normal devrait-être :
AnObject *object = [[AnObject alloc] init];
[object doSomething];
Avec pour résultat dans la console
–2009-08-31 14:49:15.177 LeaksBis[71910:20b] Toto
Destruction
Lorsque le retain count d’un objet atteint 0, la méthode dealloc de l’objet en question est appelée.
Appliquons
Si vous souhaitez changer aString de object, vous avez plusieurs manières de voir les choses :
Solution 1 : utiliser retain
- (
void
) setAString:(
NSString
*)newString {
if
(newString != aString)
{
[aString release];
aString = [newString retain];
// on incrémente de 1 le retainCount de aString
}
}
Solution 2 : utiliser copy
- (
void
) setAString:(
NSString
*)newString {
if
(newString != aString)
{
[aString release];
aString = [newString
copy
];
// a String a un retainCount de 1, self le possède
}
}
Mais si vous souhaitez retourner un objet fraîchement créé ?
Par exemple :
- (
NSString
*) returnAStringWithUpCase {
NSString
*stringARetourner;
stringARetourner = [[
NSString
alloc] initWithFormat:@
"%@ %@"
, aString, [aString uppercaseString]];
// release n'est jamais appelé ! -> fuite
return
stringARetourner;
}
AnObject *object = [[AnObject alloc] init];
[object doSomething];
[object setAString:@
"Coucou"
];
[object setAString:@
"Coucou2"
];
NSLog
([object returnAStringWithUpCase]);
ça marche, on a
2009-08-31 15:39:08.720 LeaksBis[72558:20b] Coucou2 COUCOU2
Cependant, on a une belle fuite : stringARetourner est créé mais jamais détruit ! Comment faire ? C’est là que l’on a besoin de l’autorelease…
b) Autorelease
On parle de autorelease pool (bassin d’autorelease) créé dans votre main :
int
main(
int
argc,
char
*argv[]) {
NSAutoreleasePool
* pool = [[
NSAutoreleasePool
alloc] init];
int
retVal = UIApplicationMain(argc, argv,
nil
,
nil
);
[pool release];
return
retVal;
}
En fait, cela fonctionne ainsi : vos objets crées sous autorelease sont mis dans une boucle. Petit à petit, le système supprime les objets de la mémoire et fait tourner la boucle.
Voici un récap d’après le cours de Stanford :
Notez que vous êtes libre de créer un bassin en plein milieu de votre code. Par ailleurs, cela devra obligatoirement être fait si vous détachez un thread. De toute façon, la console vous le rappelera en écrivant leaks partout !
Donc, pour résoudre le problème de dessus :
- (
NSString
*) returnAStringWithUpCase {
NSString
*stringARetourner;
stringARetourner = [[
NSString
alloc] initWithFormat:@
"%@ %@"
, aString, [aString uppercaseString]];
[stringARetourner autorelease];
// Tout va bien, l'objet sera détruit un jour
return
stringARetourner;
}
Attention avec l’autorelease :
Beaucoup de méthodes le cachent, par exemple :
NSString
*string = [
NSString
stringWithFormat:@
"I want %d $"
, 100000];
// string est sous le coup de autorelease !
Or, votre objet créé confié à autorelease va disparaître à un moment donné… Si vous souhaitez l’utiliser par la suite (par exemple :
stringAGarder = [object returnAStringWithUpCase];
// plus tard dans le code
NSLog
(stringAGarder);
) il faut utiliser retain ou copy !
stringAGarder = [object returnAStringWithUpCase];
[stringAGarder retain];
Apple fournit des outils avec son IDE XCode. Parmi ceux-ci, nous allons nous intéresser à Leaks (qui utilise Object allocations).
Pourquoi s’intéresser aux fuites ? Si vous avez une fuite d’eau sous votre évier, vous n’allez pas laisser faire et perdre de l’argent avec l’eau gaspillée + les dégâts ! Eh bien Apple c’est pareil, cela peut être un motif de refus de votre application..
Tout d’abord, créer la méthode suivante
- (
void
)createLeaks {
NSLog
(@
"createLeaks"
);
for
(
int
i = 0 ; i < 100 ; i++)
{
NSMutableArray
*array = [[
NSMutableArray
alloc] initWithCapacity:30];
}
}
Ensuite, l’appeler (ici toujours dans le viewDidLoad)
[object doSomething];
[object setAString:@
"Coucou"
];
[object setAString:@
"Coucou2"
];
NSLog
([object returnAStringWithUpCase]);
stringAGarder = [object returnAStringWithUpCase];
[stringAGarder retain];
// il faut un release quelque part -> dealloc
[object release];
[
self
createLeaks];
// ajout de cette ligne
Lancer le programme. A priori tout va bien, pas de plantage rien…
Mais
Allez dans run / start with performance tools / leaks
Une fenêtre s’ouvre
Un peu plus tard :
Un petit pic orange sur la ligne leaks ! Gloups :S
Cliquez sur la ligne leaks, une longue liste apparaît. Ce sont toutes vos fuites !
Okay, mais on fait quoi ?
Cliquez sur
cela fera apparaître un arbre généalogique qui retranscrit tous l’historique de la fuite (du début à la fin) et hop, voici le responsable :
Un double clic sur cette case vous amène même directement sur la méthode dans votre code ! Wahou !
a) Utiliser les accesseurs
Il est bien plus commode d’utiliser les accesseurs pour gérer la mémoire de vos objets. Cela permet d’éviter un grand nombre d’erreurs.
Par exemple :
// dans .h
@property
(retain)
NSString
*myString;
// dans .m
@synthesize
myString
// dans une méthode
self
.myString = aParameterStringForExample;
// ici, le retain sera fait automatiquement !
N’oubliez pas que self.name
équivaut à appeler une méthode
-(
void
)setName:(
NSString
*)newName
Donc
-(
void
)setName:(
NSString
*)newName {
self
.name = newName;
}
équivaut à
-(
void
)setName:(
NSString
*)newName {
[
self
setName:newName];
}
Ca va donc boucler !
b) Attention aux array ou dictionary
En effet, ils gérent eux-même la mémoire des objets qu’il contiennent. Par exemple, pour remplir un tableau de 10 nombres, vous avez 2 solutions :
NSMutableArray
*array;
int
i;
// ...
for
(i = 0; i < 10; i++)
{
NSNumber
*n = [
NSNumber
numberWithInt: i];
[array addObject: n];
}
ou
NSMutableArray
*array;
int
i;
// ...
for
(i = 0; i < 10; i++)
{
NSNumber
*n = [[
NSNumber
alloc] initWithInt: i];
[array addObject: n];
[n release];
}
Dans le dernier cas, il n’y a aucune raison de ne pas faire le release sur n, car addObject fais automatiquement un retain.
c) Utiliser sciemment l’allocation
Prenons le cas de figure : vous avez une classe Personne comportant un prénom, un nom de famille, un âge, le sexe etc …
Vous créez une première personne de sexe féminin. Par la suite, cette personne se marie et change de nom de famille. Plutôt que de faire un release sur l’objet et ré-allouer, préférer changer le nom de famille de l’objet.
d) Vous avez un doute sur votre gestion mémoire ?
Utilisez retainCount (attention certaines classes comme NSString renvoient de drôles de valeurs…)
NSLog
(@
"retainCount %d"
, [array retainCount]);
e) Simulateur
On ne le répétera jamais assez : le simulateur est un piège ! Le comportement entre autres (ce qui nous intéresse ici) avec leaks et object allocations est très souvent complètement différent de la réalité sur device !
sources :
cours de Stanford (leçon 3)
article sur stepwise
Memory Management in iPhone – Apple