Question(s) sur la gestion de la mémoire

NeofelisNeofelis Membre
avril 2011 modifié dans API AppKit #1
Bonjour,

Je me présente rapidement : j'ai 28 ans, je suis en tant que professionnel développeur web (PHP5, HTML, JS...) et je commence la programmation Cocoa en tant que passion personnelle (en tout cas pour le moment ;-)). Je suis actuellement en train de lire le livre "Programmation Cocoa sous Mac OSX" d'Aaron Hilegass. J'aurai quelques questions à  propos du chapitre 4 sur la gestion de la mémoire. L'auteur passe un peu vite sur quelques notions pour un débutant comme moi (en web la gestion de la mémoire est inexistante).

En gros j'ai compris comment passer du mode garbage collector au mode manuel. Maintenant l'auteur dit (page 69) : "Le code de cet ouvrage est en mode dual. Avec un tel code, les fuites de mémoire n'existent pas, que le ramasse-miettes soit activé ou non". Malheureusement l'auteur n'explique pas ce qu'est ce fameux mode dual.

Quelqu'un pourrait-t-il éclairer ma lanterne ?

Merci :)

Réponses

  • zoczoc Membre
    12:03 modifié #2
    Le mode "dual" est surtout utile quand on développe des frameworks, qui pourraient à  la fois être utilisés par des applications utilisant le garbage collector et par d'autres ne l'utilisant pas.

    Globalement (il y a quand même quelques exceptions décrites dans les docs concernant le garbage collector), pour avoir du code "dual", il suffit d'écrire du code respectant les conventions de gestion mémoire en vigueur avant l'apparition du GC, et d'ajouter une méthode finalize.

    Quand le code de la librairie sera exécuté par une application fonctionnant avec le garbage collector:
    • Les appels à  retain, release et autorelease n'auront aucun effet
    • dealloc ne sera jamais appelé, mais finalize sera appelé lors de l'exécution du ramasse miettes sur les objets n'étant plus référencés.
    Quand le code de la librairie sera exécuté par une application gérant la mémoire elle-même:
    • Les appels à  retain, release et autorelease on un comportement "classique"
    • dealloc sera appelé lors de la destruction des objets, et finalize ne sera jamais appelé.
  • NeofelisNeofelis Membre
    12:03 modifié #3
    Ok donc une application en mode "dual" c'est tout simplement une appli qui a été codé pour fonctionner avec et sans le garbage collector. C'est ce que je voulais savoir. Merci :)
  • NeofelisNeofelis Membre
    avril 2011 modifié #4
    J'ai à  nouveau un petit soucis (toujours dans le même livre).

    Soit le prototype de classe suivant :

    <br />#import &lt;Foundation/Foundation.h&gt;<br /><br />@interface LotteryEntry : NSObject {<br />&nbsp; &nbsp; NSCalendarDate *entryDate;<br />&nbsp; &nbsp; int firstNumber;<br />&nbsp; &nbsp; int secondNumber;<br />}<br /><br />- (void)prepareRandomNumbers;<br />- (void)setEntryDate:(NSCalendarDate *)date;<br />- (NSCalendarDate *)entryDate;<br />- (int)firstNumber;<br />- (int)secondNumber;<br />- (id)initWithEntryDate:(NSCalendarDate *)date;<br />@end<br /><br />
    


    et l'implémentation de la méthode description de cette même classe :

    <br />- (NSString *)description<br />{<br />&nbsp; &nbsp; NSString *result;<br />&nbsp; &nbsp; <br />&nbsp; &nbsp; result = [[NSString alloc] initWithFormat:@&quot;%@ = %d et %d&quot;,<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  [entryDate descriptionWithCalendarFormat:@&quot;%d %b %Y&quot;],<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  firstNumber, secondNumber];<br />&nbsp; &nbsp; return result;<br />}<br /><br />
    


    Je ne détaille pas le contexte car je ne pense pas que ce soit utile. L'auteur dit que l'appel à  la méthode description provoque une fuite de mémoire, et qu'il ne faut pas utiliser [result release] car le compteur de référence passe à  zéro et que l'objet qui reçoit la chaà®ne reçoit un pointeur sur un objet qui a été libéré.

    Je suis ok avec le principe, ce que je ne comprends pas c'est que chez moi les appels à  la méthode description continuent de fonctionner lorsque je fais un [result release] avant le return de la méthode (j'ai bien désactivé le Garbage collector). Logiquement si j'ai bien compris en faisant un [result release] vu que mon compteur de référence est à  1 je libère l'objet, donc mon appel à  description ne devrait plus fonctionner...

    Quelqu'un aurait une explication ?
  • AliGatorAliGator Membre, Modérateur
    12:03 modifié #5
    Oui, l'explication c'est la chance.

    En fait quand tu fais "release" sur ton objet result, la mémoire est libérée, c'est à  dire que la zone mémoire utilisée est marquée comme de nouveau disponible pour être utilisée par un autre objet, cette zone mémoire peut être écrasée par d'autres données.
    Mais le Runtime ne s'embête pas à  remettre à  zéro toute la zone mémoire : il marque juste l'info que la zone est libre, mais il laisse son ancien contenu pour l'instant " contenu qui pourra du coup à  tout moment être remplacé par d'autres données.
    Du coup quand tu récupères ces données, elles ne sont pas "sûres" mais si t'as de la chance et que la zone mémoire n'a pas été écrasée par de nouvelles données, tu arrives quand même à  t'en sortir pour récupérer ce qu'il reste de données.

    C'est un peu comme si tu mettais des feuilles à  la poubelle, mais sans les passer à  la déchiqueteuse. Tu n'es pas à  l'abri que les éboueurs passent, que tu mettes d'autres trucs à  la poubelle genre une bouteille qui va couler sur le papier ou quoi... mais si tu viens tout juste de mettre la feuille à  la poubelle et que tu essayes de la relire avec un peu de chance elle sera encore là .
    Mais tu n'as pas de garantie, et si finalement tes données ont été écrasées entre temps, tu vas utiliser des données corrompues, ou plutôt utiliser du n'importe quoi au lieu d'utiliser ce que tu crois être une NSString, et le plantage est assuré.

    En bref, en mettant [result release], l'objet "result" est détruit. Après tu as genre une chance sur deux si tu récupères le pointeur dans la méthode appelante et si tu l'utilises tout de suite, de réussir à  interpréter les données restantes comme une NSString, mais surtout une chance sur deux que ça crash. Et si tu mets rien qu'une ou deux instructions avant de manipuler cette zone mémoire, tu as encore plus de chances que ça crash car elle aura été réutilisée entre temps.
  • NeofelisNeofelis Membre
    12:03 modifié #6
    Tout s'explique. Merci pour la rapidité et la qualité de la réponse !  :)
  • NeofelisNeofelis Membre
    12:03 modifié #7
    Dans le livre que je suis en train de livre, dans les méthodes dealloc d'une classe, les variables d'instances sont parfois libérées de deux manières différentes.

    Soit :

    <br />[personName release];<br />
    


    soit à  l'aide du setter :

    <br />[self setPersonName:nil];<br />
    


    Ces deux manières de libérer un objet sont-elles strictement équivalentes ?
  • DrakenDraken Membre
    12:03 modifié #8
    Oh non.. je termine de déjeuner et je t'explique.
  • DrakenDraken Membre
    avril 2011 modifié #9
    re !

    La bonne manière de libérer une variable d'instance est d'employer release.

    [maVariable release];
    


    En écrivant la valeur nil dans la variable, tu ne libères pas la zone mémoire allouée. Et hop, une fuite mémoire !

    Les exemples de ton livre sont certainement corrects, mais doivent concerner des variables définies comme propriétés avec l'attribut RETAIN. C'est différent parce que la propriété gère elle-même l'allocation mémoire.

    @interface MaClasse : NSObject {<br />&nbsp; &nbsp; MonObjet *maVariable;<br />}<br /><br />@property (nonatomic, retain) MonObjet *monObjet;<br /><br />@end<br />
    


    En stockant un objet dans une propriété RETAIN, elle s'occupe automatiquement de libérer le contenu précédent. Magique !

    // LIBERATION PROPRIETE RETAIN<br />self.maVariable = nil;<br />
    


    Un détail important : il ne faut pas oublier le self avant d'accéder à  la variable dans une méthode de sa classe. Sinon le programme accède directement au contenu de la variable sans passer par le code assurant la gestion mémoire.

    // LIBERATION PROPRIETE RETAIN AVEC FUITE MEMOIRE<br />maVariable = nil;<br /><br />// LA MEME SANS FUITE MEMOIRE<br />self.maVariable = nil;<br />
    


  • zoczoc Membre
    12:03 modifié #10
    dans 1303658747:

    En écrivant la valeur nil dans la variable, tu ne libères pas la zone mémoire allouée. Et hop, une fuite mémoire !

    Oui et Non, parce que dans son exemple, il appelle le setter, il n'affecte pas nil directement à  la variable. Et si le setter est implémenté correctement (ou s'il est synthétisé à  partir d'une property avec l'attribut retain), il est sensé faire un release sur la valeur précédente de la variable...


    Je rappelle que 'self.toto = nil' est équivalent à  '[self setToto: nil]' (par contre, effectivement c'est très différent de 'toto = nil' ou 'self->toto = nil', qui, eux, sont également équivalents, même si en pratique la deuxième écriture n'est pas utilisée ).

  • NeofelisNeofelis Membre
    12:03 modifié #11
    Merci à  vous deux pour ces rappels et précisions.

    Effectivement le setter en question fait un release de l'ancienne valeur et un retain de la nouvelle.

    Donc dans ce cas visiblement le resultat est le même. Par contre je ne vois pas bien l'intérêt de cette syntaxe (mettre nil à  l'aide du setter) puisqu'à  priori le résultat est le même qu'avec release, et je trouve la syntaxe "release" plus explicite, plus simple et plus lisible.
  • laudemalaudema Membre
    12:03 modifié #12
    dans 1303676482:

    Je ne vois pas bien l'intérêt de cette syntaxe (mettre nil à  l'aide du setter) puisqu'à  priori le résultat est le même qu'avec release, et je trouve la syntaxe "release" plus explicite, plus simple et plus lisible.

    L'intérêt est justement dans ce que tu décrivais au début !
    Si tu release une variable ton pointeur est toujours là  et il ne pointe plus sur rien. Si tu l'appelles tu aura un Crash pour avoir lancé une méthode sur un objet ou une variable qui n'existe plus..
    Si tu l'as mis = nil alors tu auras une valeur interprétable (0 ou nil selon ce que tu auras appelé).

  • AliGatorAliGator Membre, Modérateur
    avril 2011 modifié #13
    En effet il s'agit parfois de préférences personnelles selon les développeurs
    En général c'est en effet plus simple de faire juste release quand tu es dans le dealloc vu que de toute façon il n'y a pas de risque que ta variable soit réappelee après ce dealloc ; moi je préfère souvent et trouve cela plus explicite.
    D'autant plus qu'appeler le setter va émettre les messages de KVO dont on peut se passer dans le cadre d'un dealloc

    Mais d'un autre côté, quand on libère également la mémoire mais dans le cadre d'un viewDidUnload (cas de memory warning sur l'appli) là  c'est important de remettre la variable à  nil après le release (et du coup autant appeler le setter plutôt que de faire release + un "=Nil") ! Car sinon l'instance de ta classe existe encore et peut accedder à  ta variable d'instance... alors que tu l'as releasee et si tu l'as pas mis à  nil au passage, crash assuré

    Du coup par habitude on met parfois la même chose dans le dealloc (ce qui permet de faire aussi un copier/coller... hum)
    De même, avec le modern runtime, maintenant on peut déclarer des @property sans déclarer de variable d'instance associée (backing variable), du coup dans ce cas tu n'as pas le choix, la variable d'instance n'existant pas en tant que tel tue peux pas y accéder par code (en réalité si mais bon faut savoir comment le compilo l'a nommé et tu ne peux le faire que après le @synthesize et de toute façon ça serait pas trop cohérent de l'utiliser plutôt que la @property alors que dans le .h elle n'existe même pas officiellement... bref) du coup dans le dealloc là  tu es bien obligé de faire self.mapropriete=nil vu que tu n'as pas de variable d'instance sur laquelle faire de release !
  • FKDEVFKDEV Membre
    12:03 modifié #14
    Je préfère également la méthode explicite.
    Pour éviter le copier coller, je fais toujours une méthode [tt]clean[/tt] dans mes controllers qui est appelé depuis le dealloc et le didunload. Même si en théorie, on n'est pas obligé de tout libérer dans le viewdidunload.
  • AliGatorAliGator Membre, Modérateur
    avril 2011 modifié #15
    Ah non il ne faut PAS tout libérer dans le viewDidUnload, ce sont deux cas différents lui et le dealloc.
    L'idéal est de faire une méthode cleanOutlets qui remet à  nil tous les IBOutlets, après avoir fait un release sur ceux qui sont en @property(retain), et release et remet à  nil également tous les objets crées et retenus dans le viewDidLoad puisqu'ils seront recrées quand la vue sera de nouveau chargée plus tard après la réception du memorywarning.
    Mais viewDidUnload ne doit pas releaser les objets du modèle par contre.

    @interface Toto : NSObject {<br />&nbsp; NSArray* data;<br />&nbsp; NSArray* filtr;<br />}<br />@property(retain) IBOutlet UIView* v1;<br />@property(assign) IBOutlet UIView* v2;<br />@end
    

    @interface Toto () /* Private Methods */<br />-(void)clean;<br />@end<br /><br />@implémentation Toto<br />-(id)initWithData:(NSArray*)someData {<br />&nbsp; self = [super initWithNibName:@&quot;Toto&quot; bundle:nil];<br />&nbsp; if (self != nil) {<br />&nbsp; &nbsp; data = [someData retain]; // objet du modèle ici<br />&nbsp; }<br />&nbsp; return self;<br />}<br /><br />-(void)viewDidLoad {<br />&nbsp; filtr = [[data filteredArrayUsingPredicate:...] retain];<br />}<br /><br />-(void)clean {<br />&nbsp; // data est une donnée du modèle et ne doit pas être releasee dans le cadre de viewDidUnload<br />&nbsp; self.v1 = nil; // release + mise à  nil, vu que v1 sera recrée lors du rechargement du XIB<br />&nbsp; v2 = nil; // ou self.v2 = nil ici c&#039;est équivalent puisque v2 est (assign) ; de même v2 sera recrée quand la vue sera rechargée du XIB<br />&nbsp; // mais faut quand même le mettre à  nil pour pas qu&#039;il pointe sur un objet qui n&#039;existe plus vu que la View a été libérée<br /><br />&nbsp; [filtr release]; // car sera recrée par viewDidLoad lorsque la vue sera rechargée<br />&nbsp; filtr = nil; // pour éviter d&#039;y accéder ailleurs dans le code alors que ce ne serait plus valable et crasherait<br />}<br /><br />-(void)viewDidUnload {<br />&nbsp; [self clean];<br />}<br /><br />-(void)dealloc {<br />&nbsp; [self clean]; // il y a déjà  tout le nettoyage qu&#039;on fait aussi quand viewDidUnload<br />&nbsp; // mais on fait aussi le nettoyage du reste, de tout ce qui est partie modèle qu&#039;on a crée dans le init ou autre<br />&nbsp; [data release];<br />&nbsp; // ici si data avait été déclaré comme une @property on aurait aussi pu écrire self.data = nil, au choix<br />&nbsp; // ceci dit dans le cadre de dealloc à  priori ce n&#039;est pas strictement nécessaire de remettre à  nil la variable après<br />&nbsp; // l&#039;avoir releasée vu qu&#039;après le dealloc l&#039;objet n&#039;existera plus donc on ne risque pas de réutiliser la variable d&#039;instance.<br /><br />&nbsp; [super dealloc];<br />}<br />@end
    
    Apres il y a plein née façons de faire. Par exemple s'il faut bien faire un release sur v1 que ce soit dans le cas de viewDidUnload ou le cas de dealloc, par contre la remise à  nil de ces IBOutlets n'est utile que dans viewDidUnload. Par exemple on pourrait déplacer le code de v2=nil dans viewDidUnload au lieu de clean. Mais bon, pour ce que ça coûte ded remettre la variable à  nil, ça serait bête de se priver de cette sécurité dans tous les cas.
  • NeofelisNeofelis Membre
    12:03 modifié #16
    Merci à  vous. Il faut encore que j'approfondisse un peu car je ne suis pas sûr d'avoir compris toutes les subtilités mentionnées mais je mets ce topic bien au chaud !
Connectez-vous ou Inscrivez-vous pour répondre.