Quelques subtilités sur la gestion mémoire
AliGator
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 :
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 :
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...
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 :
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?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.
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 :
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...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 /> // Release outlets and set outlet variables to nil.<br /> [anOutlet release], anOutlet = nil;<br /> [super dealloc];<br />}
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...
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
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.
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)
Pourquoi mettre à nil dans le viewDidUnload ? Le release dans le dealloc est suffisant non ?
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 !
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
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)
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 !!
Va y avoir de la lecture dans le lit :P
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 ?!
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:
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 ?
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 ?
Evidemment il faut utiliser 'viewDidUnload' pour libérer le plus de mémoire possible.
Corrigez moi si je me trompe.
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.
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.
Ceci-dit, je suis en 4.3 beta 2 sur mon device. Quelque chose aurait changé de ce côté?
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'est vrai que les IBActions posent le même problème.
- 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 ?
iOS:
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é.
La précaunisation, vu avec l'équipe Qualité et validée par d'autres pratiques, et qui correspond également à ce que précaunise Apple :
(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)
Ce qui donne : 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.
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.