Quelques subtilités sur la gestion mémoire

AliGatorAliGator Membre, Modérateur
Je viens de retomber sur cet article d'Apple
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmNibObjects.html

Et j'avoue que je suis assez étonné d'y lire ceci :
Objects in the nib file are created with a retain count of 1 and then autoreleased. As it rebuilds the object hierarchy, UIKit reestablishes connections between the objects using setValue:forKey:, which uses the available setter method or retains the object by default if no setter method is available.
Ca veut dire que si on déclare une variable d'instance avec un IBOutlet, sans @property ni setter associé à  cette ivar... l'objet pointé par l'IBOutlet lors du chargement du NIB sera quand même retenu... mais du coup faut penser à  le releaser, right?

Pourtant moi je le fais jamais : si je déclare un @property(retain), le fais un release de l'ivar associée dans le -dealloc... mais si j'ai pas déclaré de @property (ni de setter), pour moi une ivar IBOutlet était juste une variable affectée par un = donc sans retain implicite... va falloir que je revois pas mal de code du coup !


Et j'ai vu ça aussi :
n addition, because of a detail of the implementation of dealloc in UIViewController, you should also set outlet variables to nil in dealloc:
- (void)dealloc {<br />&nbsp; &nbsp; // Release outlets and set outlet variables to nil.<br />&nbsp; &nbsp; [anOutlet release], anOutlet = nil;<br />&nbsp; &nbsp; [super dealloc];<br />}
Ce qui m'étonne c'est que normalement après le "dealloc", forcément anOutlet, qui est une variable de la classe en question, ne pourra plus être accédée après ce -dealloc puisque cette instance est en train d'être détruite justement...
Alors après peut-être que le dealloc de UIViewController utilise les méthodes du Runtime d'ObjC pour faire de l'instrospection et regarder les IBOutlets déclarés... mais je vois pas ce qu'ils peuvent faire avec, surtout si on est sensé les passer à  "nil" justement...
«1

Réponses

  • SmySmy Membre
    21:46 modifié #2
    Amusant, je viens de tomber aujourd'hui sur la même interrogation que toi, mais en partant d'un problème de leak sur une appli qui fait un usage intense de viewcontroller.

    Jusqu'à  présent, je ne faisais pas de release sur mes ivar IBOutlet car je pensais (à  tort) que tout était géré automatiquement. J'ai ensuite ajouté un release sur les ivar IBOutlet avec property, mais ce n'était pas suffisant car j'avais toujours des fuites. J'ai donc fini par ajouter un release sur toutes les ivar IBOutlet et tout est maintenant correct.

    Je vais aussi devoir vérifier mes sources de mes anciennes applis. Par contre, je ne règle pas à  nil dans le dealloc.

    Ce qui est surprenant, c'est que je ne voyais ces fuites qu'avec Instrument sur un véritable iPhone, et pas sur le simulateur.
  • AliGatorAliGator Membre, Modérateur
    21:46 modifié #3
    Oui dans tous les cas il faut mettre tous les IBOutlets à  nil dans le viewDidUnload et dans le dealloc... mais il semble que ça soit le cas y compris pour les IBOutlets qui ne sont que des ivar, alors qu'on s'attend à  ce que ce soit le cas uniquement pour les IBOutlet avec @property(retain)... déroutant.

    Je me demande du coup si je vais pas faire une petite fonction qui utilise les méthodes du runtime Objective-C pour faire de l'introspection et mettre automatiquement tous les IBOutlets à  nil (avec un release si ce sont des ivar, avec un self.outlet = nil si c'est une @property), comme ça il suffira dans le viewDidUnload comme dans le dealloc d'appeler cette fonction et ça libérerait automatiquement tous les IBOutlets de la classe...
    Mais je sais pas si c'est faisable faut que je regarde ! (Je sais qu'avec les méthodes du runtime on peut lister toutes les @property, mais peut-on lister tous les IBOutlets je suis pas sûr)
  • 21:46 modifié #4
    ça me parait complètement bizarre... à  mon avis il faudrait demander sur les forums d'Apple... Parce que sur un projet Mac OS X, apple met par défaut le window en @property assign, et ne fait aucun release dessus dans le .m
  • SmySmy Membre
    21:46 modifié #5
    dans 1298311339:

    Oui dans tous les cas il faut mettre tous les IBOutlets à  nil dans le viewDidUnload et dans le dealloc... mais il semble que ça soit le cas y compris pour les IBOutlets qui ne sont que des ivar, alors qu'on s'attend à  ce que ce soit le cas uniquement pour les IBOutlet avec @property(retain)... déroutant.


    Pourquoi mettre à  nil dans le viewDidUnload ? Le release dans le dealloc est suffisant non ?
  • 21:46 modifié #6
    J'ai fait un test de leak  avec Instruments sur un projet neuf.. Sans le release et avec un outlet en property assign.. aucun problème de mon côté.
  • AliGatorAliGator Membre, Modérateur
    21:46 modifié #7
    Moi j'ai eu des leaks sur FoodReporter à  cause de ça, et dès que j'ai passé mes IBOutlets qui étaient sur des variables en "@property(retain) IBOutlet" et du coup mis un "release" dans le dealloc et un "self.truc = nil" dans viewDidUnload, le leak a été résolu. J'ai pas compris pourquoi sur le coup (puisque dans ma tête un IBOutlet sur une variable d'instance bah ça faisait qu'un assign donc pas besoin de release) mais quand je suis tombé sur ce morceau de doc finalement ça explique ce que j'avais constaté.

    Louka attention, sur iOS et sur MacOSX ce n'est pas géré du tout pareil. Le désarchivage des XIB n'est pas le même, cf la même page de la doc. OSX retient ses topLevelObjects, pas iOS : ce qui veut dire que si tu as des objets à  la racine de ton XIB (en plus du File's Owner et de la UIView de ton ViewController " si ton XIB est un XIB associé à  un UIViewController), sous iOS ils seront détruits à  la prochaine runloop.
    Et il y a 2-3 autres subtilités et différences entre iOS et OSX qui sont expliquées dans la doc surtout concernant les XIB justement. Donc ne pas se baser sur un exemple sous OSX pour tirer tes conclusions !
  • 21:46 modifié #8
    Bha pourquoi tu postes dans la section "commune"... ?
  • SmySmy Membre
    21:46 modifié #9
    dans 1298314378:

    Moi j'ai eu des leaks sur FoodReporter à  cause de ça, et dès que j'ai passé mes IBOutlets qui étaient sur des variables en "@property(retain) IBOutlet" et du coup mis un "release" dans le dealloc et un "self.truc = nil" dans viewDidUnload, le leak a été résolu. J'ai pas compris pourquoi sur le coup (puisque dans ma tête un IBOutlet sur une variable d'instance bah ça faisait qu'un assign donc pas besoin de release) mais quand je suis tombé sur ce morceau de doc finalement ça explique ce que j'avais constaté.


    Il y a un truc que je ne saisis pas, dans ton nil et ton release. Le viewDidUnload est forcément appelé avant le dealloc, et si tu fais un self.truc = nil et que ton ivar truc est en property retain, le setter fait un release. Donc pourquoi refaire [truc release] dans le dealloc ? Ou plutôt question inverse, le dealloc étant forcément appelé, pourquoi faire un self.truc = nil dans le unload ?

    Ahhh, les subtilités de mémoire. Au moins en C, malloc et free suffisent  :)
  • AliGatorAliGator Membre, Modérateur
    21:46 modifié #10
    dans 1298311987:

    Pourquoi mettre à  nil dans le viewDidUnload ? Le release dans le dealloc est suffisant non ?
    Ohhh que non !!

    Quand le XIB de ton ViewController est désarchivé/instancié, il instancie tous les objets du XIB (à  part bien sûr le File's Owner qui est le ViewController lui-même), et affecte les IBOutlets en utilisant le setter dédié/associé s'il existe, ou setValue:forKey:, mais donc en retenant ces objets. Du coup quand tu as un memory warning, et donc que les UIViewControllers non visibles relâchent leur UIView et appelle viewDidUnload, il faut releaser tous les objets qui étaient retenus par le désarchivage du XIB.
    Sinon comme la prochaine fois que ce ViewController va avoir besoin de recharger sa UIView (typiquement la prochaine fois qu'il va devoir être affiché à  l'écran) il va de nouveau charger son contenu à  l'aide du même XIB qu'au début, il va réinstancier les objets de ce XIB... si tu n'as pas relâché les précédents, bonjour le leak !

    D'ailleurs viewDidUnload est précisément faite pour ça comme méthode. (d'ailleurs dans les modèles de fichiers type quand tu crées une nouvelle classe de type UIViewController, il le met même dans les commentaires disant qu'il faut que tu release les IBOutlets à  cet endroit)
    When a low-memory warning occurs, the UIViewController class purges its views if it knows it can reload or recreate them again later. If this happens, it also calls the viewDidUnload method to give your code a chance to relinquish ownership of any objects that are associated with your view hierarchy, including objects loaded with the nib file, objects created in your viewDidLoad method, and objects created lazily at runtime and added to the view hierarchy. Typically, if your view controller contains outlets (properties or raw variables that contain the IBOutlet keyword), you should use the viewDidUnload method to relinquish ownership of those outlets or any other view-related data that you no longer need.
  • AliGatorAliGator Membre, Modérateur
    21:46 modifié #11
    dans 1298314700:

    Bha pourquoi tu postes dans la section "commune"... ?
    Parce que justement il y a aussi d'autres subtilités pour OSX. Lis toute la page de la doc Apple, y'a des points spécifiques à  iOS, des points spécifiques à  OSX, et des points communs aux deux
  • AliGatorAliGator Membre, Modérateur
    21:46 modifié #12
    dans 1298314999:

    Le viewDidUnload est forcément appelé avant le dealloc
    Nope !

    viewDidUnload est appelé quand il y a un memory warning, et donc quand la vue est alors déchargée... mais que le UIViewController existe encore, et que la vue sera rechargée plus tard quand il y aura assez de mémoire et que la vue du UIViewController devra être réaffichée

    dealloc est appelé à  la destruction du UIViewController. Mais il n'appelle pas forcément viewDidUnload avant !!
    If your view controller stores references to views and other custom objects, it is also responsible for relinquishing ownership of those objects safely in its dealloc method. If you implement this method but are building your application for iOS 2.x, your dealloc method should release each object but should also set the reference to that object to nil before calling super.
  • 21:46 modifié #13
    *sort l'iPad et enregistre la doc dessus*
    Va y avoir de la lecture dans le lit  :P
  • AliGatorAliGator Membre, Modérateur
    21:46 modifié #14
    Je croyais que t'avais mis ton iPad au placard et qu'il te servait plus ? :D

    Et comment ça, tu veux dire que la Doc Apple n'était pas encore ton livre de chevet ? Mais que pouvais-tu donc bien lire de plus intéressant ?! :D
  • SmySmy Membre
    février 2011 modifié #15
    Merci AliGator pour tes explications sur le DidUnload. Je vais devoir réétudier les cas de memory warning, c'est une partie lue en diagonal il y a deux ans :)

  • 21:46 modifié #16
    Salut Ali,

    J'ai relu la doc concernant le passage iOS et la gestion outlets, et j'ai aussi relu ton premier post. J'en ai par ailleurs profité pour faire des test sur device cette fois-ci et non sur OS X.
    Résultat... toujours aucun leak de mon côté. Et après avoir relu le passage que tu cites:

    Objects in the nib file are created with a retain count of 1 and then autoreleased. As it rebuilds the object hierarchy, UIKit reestablishes connections between the objects using setValue:forKey:, which uses the available setter method or retains the object by default if no setter method is available.

    En aucun cas la doc stipule que, dans ce cas, tu es responsable de release l'outlet. Ou bien j'ai manqué quelque chose.
    En tout cas je n'ai vraiment aucun leaks de mon côté que ça soit en @property (assign) ou direct l'IBOutlet en iVar.


    Concernant le fait de release un outlet dans le dealloc, ça me semble évident en @property (retain). En revanche je ne comprends pas pourquoi Apple utilise [myOutlet release], myOutlet=nil au lieu de self.myOutlet=nil; Il y a une subtilité quelconque ?
  • AliGatorAliGator Membre, Modérateur
    21:46 modifié #17
    Bah et "or retains the object by default if no setter method is available" tu le comprends comment alors ?
  • 21:46 modifié #18
    Je le comprends comme tu le comprends. Sauf qu'il n'est pas indiqué qu'on doit se charger de release l'outlet dans ce cas là . à  mon avis il fait ça comme un grand.
  • SmySmy Membre
    février 2011 modifié #19
    Je confirme après de nombreux tests qu'il faut faire un release de toutes les ivar IBOutlets, y compris celles sans property.

    Je ne vois pas les fuites sur émulateur, mais sur un iPhone 3 (4.2.1).

    Du coup, n'est il pas plus logique de faire des property pour toutes ces ivar ?
  • LexxisLexxis Membre
    21:46 modifié #20
    D'après ce que j'ai compris il est très vivement recommandé (donc quasi obligatoire) de déclarer ses Outlets sur des @property pour iOS pour éviter justement ce problème de leak mémoire évoqué par Ali. De cette manière l'implémentation de la méthode viewDidUnload n'est même pas obligatoire !!! et ce quelque soit la déclaration de votre Outlet (assign ou retain) car lors du prochain chargement de la vue (viewDidLoad) les objets précédent (outlet en assign) ont été libérer par le UIViewController et les objets qui sont encore retenues (outlet en retain) seront remplacés en utiliser les setter et les objets précédent sont libérés.
    Evidemment il faut utiliser 'viewDidUnload' pour libérer le plus de mémoire possible.

    Corrigez moi si je me trompe.
  • dotshedotshe Membre
    21:46 modifié #21
    Oui c'est tout à  fait cela.

    Personnellement, tous mes IBOutlets sont toujours déclarés en @property (en retain) et j'effectue le release + remise à  nil dans le dealloc  [myOutlet release], myOutlet = nil;

    Et depuis quelques temps, je passe tous mes IBOutlet à  nil au niveau du viewDidUnload afin de libérer la mémoire en cas de besoin.
  • LexxisLexxis Membre
    21:46 modifié #22
    Par contre je penses que Ali à  raison concernant le Outlet sur les ivar (comme c'était le cas au début de OS X d'ailleurs). Lors du chargement du Nib, UIKit effectue les liens avec les outlets et effectivement réalise un retain la où il n'a pas trouvé de setter. Il faut donc faire un release sur ces Outlet (je n'ai effectués aucun test). Normalement ce genre de cas ne devrait pas se présenter puisqu'il est vivement préconisé (...) d'utiliser les Outlet sur les @property.
  • CéroceCéroce Membre, Modérateur
    21:46 modifié #23
    Déclarer des propriétés sur les outlets me semble contraire à  l'esprit de la POO.

    L'avénement de la POO est dû à  la volonté de simplifier (et rendre moins chère...) l'évolution du code. La bonne pratique consiste à  limiter les informations qu'une classe donne d'elle-même pour permettre des changements de son implémentation sans impacter les classes qui y font appel.

    Or les outlets sont des détails de l'implémentation.

  • 21:46 modifié #24
    Et bien j'ai beau faire et refaire des tests, moi je n'ai aucune fuite.
    Ceci-dit, je suis en 4.3 beta 2 sur mon device. Quelque chose aurait changé de ce côté?
  • AliGatorAliGator Membre, Modérateur
    21:46 modifié #25
    Je confirme ce qu'ont remarqué Lexxis et dotshe. Leaks si mes IBOutlets sont sur des ivar.
    Après je sais pas si c'est le cas partout dans mon code. Je l'ai constaté dans mon PhotoViewer (classe perso affichant une image dans une ScrollView en plein écran permettant de zoomer sur une photo, tourner l'iPhone pour tourner la photo, tout ça) où j'ai passé plusieurs jours à  comprendre pourquoi j'avais une leak et en passant mon IBOutlet de l'ivar à  la @property ça s'est résolu tout seul, alors bon...

    @Ceroce : moi ça me choque pas tant que ça, de toute façon faut bien le mettre quelquepart ce mot clé IBOutlet. Et puis tu mets bien IBAction sur les méthodes que tu veux pouvoir pluguer sur IB comme tu lets les IBOutlet pour la mm chose côté variables/propriétés...

    Et puis surtout, avec le Modern Runtime (contrairement au Legacy Runtime), on n'a plus besoin de déclarer les ivar quand on déclare une @property (s'il n'y a pas de ivar avec le même nom, il va créer tout seul une "backing variable" pour stocker la valeur de la propriété)... Dans ce cas tu fais comment pour le mettre ton IBOutlet puisque tu n'as que @property et pas de ivar associée de déclarée dans ton .h ? :P
  • CéroceCéroce Membre, Modérateur
    21:46 modifié #26
    Eh bien, tu ne déclares pas de propriété, mais tu déclares la variable d'instance en IBOutlet. C'est ce qu'on a fait pendant des années.
    C'est vrai que les IBActions posent le même problème.
  • AliGatorAliGator Membre, Modérateur
    21:46 modifié #27
    Le truc c'est que c'est justement le truc déconseillé par Apple que de ne déclarer que la ivar et pas la @property ni aucun accesseur.
    When a nib file is loaded and outlets established, the nib-loading mechanism always uses accessor methods if they are present (on both Mac OS X and iOS). Therefore, whichever platform you develop for, you should typically declare outlets using the Objective-C declared properties feature.

    The general form of the declaration should be:
    @property (attributes) IBOutlet UserInterfaceElementClass *anOutlet;
    

    The behavior of outlets depends on the platform (see “Mac OS X” and “iOS”), so the actual declaration differs:
    • For Mac OS X, you should use:

    @property (assign) IBOutlet UserInterfaceElementClass *anOutlet;
    

    • For iOS, you should use:

    @property (nonatomic, retain) IBOutlet UIUserInterfaceElementClass *anOutlet;
    

    You should then either synthesize the corresponding accessor methods, or implement them according to the declaration, and (in iOS) release the corresponding variable in dealloc.

    This pattern also works if you use the modern runtime and synthesize the instance variables, so it remains consistent across all situations.
  • SmySmy Membre
    21:46 modifié #28
    Au final, quelle est la meilleure solution ?

    - IBOutlet sur l'ivar + property + release dans le dealloc
    - IBOutlet sur l'ivar + release dans le dealloc
    - IBOutlet sur la property + release dans le dealloc

    Je n'ai pas trop envie de changer mes sources, mes ivar sont en _truc et mes properties en truc, avec un synthesize truc = _truc. Si je passe l'IBOutlet de l'ivar à  la property, il va falloir que je change mes liaisons dans IB, non ?
  • 21:46 modifié #29
    La bonne méthode c'est celle d'Apple, même si elle ne plait pas à  tout le monde (dont moi)
    iOS:
    <br />@property (nonatomic, retain) IBOutlet UIUserInterfaceElementClass *anOutlet;<br />
    


    Au moins on est sûr de ne jamais oublier le release dans le dealloc...

    En tout cas j'aimerai bien savoir, pour ceux qui ont fait les "tests", avec quelles Classes en IBOutlet il y a eu des fuites.. Moi j'ai testé sur un UITextField et comme je l'ai dit, aucun leak de mon côté.
  • AliGatorAliGator Membre, Modérateur
    21:46 modifié #30
    Je suis précisément en train de faire une revue de code pour un projet dont je suis l'architecte logiciel.
    La précaunisation, vu avec l'équipe Qualité et validée par d'autres pratiques, et qui correspond également à  ce que précaunise Apple :
    • IBOutlet sur la @property, et pas sur la ivar
      (d'autant plus que la ivar peut avoir un préfixe à  son nom qui fait moins joli à  présenter dans IB, on préfère avoir un outlet qui s'appelle "okButton" plutôt qu'il s'appelle "m_okButton" dans IB, mais bon)
    • Déclarer ou non une ivar, à  toi de voir si tu es prêt à  t'adapter au Modern Runtime ou si tu es encore à  utiliser le Legacy Runtime qui t'oblige, lui, à  déclarer la backing variable
    • Affecter à  nil la property dans le dealloc ET dans viewDidUnload. Dans le dealloc, il faut affecter à  nil TOUTES les @property(retain) ou @property(copy) même celles qui ne sont pas IBOutlet, dans le viewDidUnload il faut garder les @property contenant tes données modèle et ne mettre à  nil que tes @property(retain) associées à  un IBOutlet


    Ce qui donne :
    @interface Toto : UIViewController {<br />//&nbsp; NSString* text; // Inutile depuis le Modern Runtime<br />//&nbsp; UILabel* textLabel; // Inutile depuis le Modern Runtime<br />&nbsp; NSData* internalValue; // variable interne, pas d&#39;accesseur externe<br />&nbsp; // (on veut pas lui mettre de @property car on ne veut pas qu&#39;il soit accédé de l&#39;extérieur)<br />}<br />@property(nonatomic, retain) NSString* text; // données du modèle, stocke une valeur et pas une vue<br />@property(nonatomic, retain) IBOutlet UILabel* textLabel;<br />@end
    
    @implementation Toto<br />@synthesize text, textLabel;<br /><br />-(void)viewDidLoad {<br />&nbsp; [super viewDidLoad];<br />&nbsp; self.textLabel.text = self.text;<br />}<br />-(void)viewDidUnload {<br />&nbsp; [super viewDidUnload]; // à  ne pas oublier<br /><br />&nbsp; // Mémoriser la valeur du texte pour pouvoir le remettre quand la vue sera rechargée<br />&nbsp; self.text = self.textLabel.text;<br /><br />&nbsp; /* release any IBOulet that will be recreated when the NIB will be unarchived again */<br />&nbsp; self.textLabel = nil;<br /><br />&nbsp; // ne pas mettre self.text à  nil par contre bien sûr ni ne toucher à  internalValue<br />}<br />-(void)dealloc {<br />&nbsp; // Release (set to nil) every @property<br />&nbsp; self.text = nil;<br />&nbsp; self.textLabel = nil;<br /><br />&nbsp; // Faire un release également sur toutes les autres variables qu&#39;on aurait pu &quot;retain&quot; pendant la vie de l&#39;objet<br />&nbsp; [internalValue release];<br /><br />&nbsp; [super dealloc];<br />}<br />@end
    
    Avec ça tu as un exemple d'une @property utilisée pour un objet du modèle (text), une @property utilisée pour un IBOutlet, et une variable d'instance qui n'a pas de @property associée car elle doit rester privée.


    Avant je mettais IBOutlet sur mes ivar et je ne leur associait pas de @property, mais depuis que j'ai remarqué les leaks que ça peut provoquer dans certains cas, la prochaine passe que je fais sur mon code pour supprimer ces leaks c'est de passer tous les IBOutlets de ivar à  @property.
  • AliGatorAliGator Membre, Modérateur
    21:46 modifié #31
    dans 1298391128:

    La bonne méthode c'est celle d'Apple, même si elle ne plait pas à  tout le monde (dont moi)
    iOS:
    <br />@property (nonatomic, retain) IBOutlet UIUserInterfaceElementClass *anOutlet;<br />
    


    Au moins on est sûr de ne jamais oublier le release dans le dealloc...

    En tout cas j'aimerai bien savoir, pour ceux qui ont fait les "tests", avec quelles Classes en IBOutlet il y a eu des fuites.. Moi j'ai testé sur un UITextField et comme je l'ai dit, aucun leak de mon côté.
    Je te filerai ma classe PhotoViewer à  l'occasion si tu veux.

    Je me rappelle m'être arraché les cheveux dessus et à  même remarquer des comportements bizarres, genre quand je push mon PhotoViewerViewController, si on clique sur la vue avant de faire back, iOS envoie un "release" de moins que si on clique sur "back" tout de suite (gné?!), ou encore que, ma vue étant composée d'un UIScrollView contenant un UIImageView, si je mets [monImageView removeFromSuperview] dans le dealloc, alors le dealloc de ma UIImageView est bien appelé et mon UIImageView est bien détruite de la mémoire.... alors que je n'ai aucune raison d'avoir à  mettre de "removeFromSuperview dans mon dealloc, mais que si je ne le mets pas... alors ma UIImageView reste en mémoire et son dealloc n'est pas appelé !

    Si je convertit mon ivar "IBOutlet UIImageView* monImageView" en une "@property(nonatomic,retain) IBOutlet UIImageView* monImageView" (et que du coup je rajoute le "release" associé dans le dealloc comme pour toute @property(retain) qui se respecte), là  tout se passe bien côté mémoire, tant pour quand je fais le back que j'ai touché la vue avant ou pas, que pour le cas où j'appelle removeFromSuperview ou pas sur monImageView. Autrement dit je retrouve un comportement logique niveau mémoire que je n'avais pas si je laisse mon IBOutlet sur mon ivar sans @property.
Connectez-vous ou Inscrivez-vous pour répondre.