Tutoriel, Gestion Mémoire en ARC (Automatic Reference Counting) : exemple dans le cas de la résolution d’un bug particulièrement coriace.

Niveau technique : très avancé

Les concepts détaillés dans l’article sont donnés pour macOS, et s’appliquent tels quels à iOS, éventuellement à Java (ramasse miette).

Le projet dont il s’agit (Dichotomizer), consiste, réduit aux fonctions mentionnées ci-dessous, à lire une image sur disque, l’afficher, puis en calculer des résolution successives : une image de 512×512 pixels -> 4 images de 256×256 -> chacune divisée en 4 images de 128×128 soit 16 en tout -> etc…

imageSuccessiveSubdivisions

Animation : états successifs (approximations successives d’image)

(NOTE : j’ai présenté une image plus petite, 64×64, pour cette animation ; l’utilisation de « directives de compilation » dans le .pch me permet d’obtenir cette image, et pouvoir revenir instantanément au code original, et ce quel que soit l’avancement du projet, c’est quand même plus simple que d’utiliser des « branches » Git !)

1) l’image est lue sur disque, en tant que NSImage. Une fonction est appelée pour en extraire itFullImageRef de type CGImageRef, plus adaptée aux traitements numériques (explications plus bas), qui est gardée pour les utilisations ultérieures (mise en « cache »).

-(instancetype)initWithImage:(NSImage *)iImage{
    …
    self->itFullImageRef = [iImage CGImageForProposedRect:&proposedDestRect context:referenceContext hints:hints];
    …
}

2) ensuite la fonction getImageFromRawDataWithSize: est appelée plusieurs fois obtenir les NSImage de résolutions successives à partir de cette dernière.

Tout se passe bien, la fonction est appelée de nombreuses fois, jusqu’à cette erreur :

Arrêt du programme (exception)

 

Une ligne de commande nous apprend que le pointeur est bien valide :
(lldb) pri croppedImage
(CGImageRef) $3 = 0x00007fff85588e60

Par contre cette commande ci nous donne une information plus inquiétante, lorsque on veut lire la largeur de l’image :
(lldb) po CGImageGetWidth(croppedImage)
10180964699509114614

et même, celle là ne nous donne pas l’information sur la nature de itFullImageRef, comme avec croppedImage :
(lldb) po itFullImageRef
0x00006000001a1340

De nombreuses sessions de DEBUG ne nous donne aucune information sur ce qui a bien pu arriver à itFullImageRef. La recherche de l’origine du bug, passe par la création d’un « watch point » en ligne de commande, qui permettra de mettre le programme en pause dès qu’une modification se sera produite, en exécutant cette commande juste après la création de itFullImageRef (voir le code plus haut) :
(lldb) watch set variable itFullImageRef
Watchpoint created: Watchpoint 1: addr = 0x618000120818 size = 8 state = enabled type = w watchpoint spec = ‘itFullImageRef’
     new value: 0x00006000001a0620

Le crash se produit toujours, effectivement le pointeur n’a pas été modifié, on va plutôt s’intéresser à la zone mémoire indiquée par le pointeur :
(lldb) watch set expression — itFullImageRefWatchpoint created: Watchpoint 1: addr = 0x6180001a0a80 size = 8 state = enabled type = w
     new value: 0x001dffff7588cfa1

Le programme finit par s’arrêter sur le watch point, avec affichage dans la console :
Watchpoint 1 hit:
old value: 0x001dffff7588cfa1
new value: 0xbaddc0dedeadbead

L’information donnée par la pile d’appels est plutôt verbeuse :

Stack

Pile d’appels (erreur)

mais la ligne 10 en particulier nous en donne une particulièrement intéressante :

Stack_Detail

Pile d’appels, fonction ayant causé le crash mise en évidence

Explication :
Tant qu’il s’agissait d’obtenir une NSImage quelconque à partir de l’image d’origine (de type NSImage), tout se passait bien.
Il me faut préciser que l’image était affectée à l’interface, pour affichage, de cette manière :
    NSImage *image = [[NSImage alloc] initWithData:imageData];
    …
    [self->_twImageView_Left setImage:iImage];

… à la suite de quoi l’image était détruite, mais la zone mémoire qu’elle indiquée était « retenue » au sens ARC par _twImageView_Left, et donc conservée.

Lors de l’affichage d’une nouvelle image (voir l’animation au dessus), une nouvelle NSImage est affectée à _twImageView_Left, et donc la précédente n’est plus retenue. Et par conséquent détruite !

Ceci m’a beaucoup intrigué car la documentation de CGImageForProposedRect (création de itFullImageRef) n’indique rien de tel. Tout au plus on peut apprendre que la vraie description d’une image et de ses données est CGImageRef, et que NSImage est en réalité une CGImageRef associée à une description de sa représentation.
L’analyse de la pile des appels prouve que la création de itFullImageRef n’a fait que récupérer la CGImageRef de l’image d’origine, en copiant son adresse, mais sans retenir la zone.

Solution pour le crash :

self->itFullImageRef = [iImage CGImageForProposedRect:&proposedDestRect context:referenceContext hints:hints];
CFIndex test;
if(itFullImageRef)
{
    CGImageRetain(itFullImageRef);
    test = CFGetRetainCount(itFullImageRef); // => 4
}

On vérifie que le crash n’a plus lieu. La valeur 4 n’est pas très importante. La fonction CFGetRetainCount lorsque elle est appelée dans dealloc (à la fin de l’exécution du programme), donne aussi 4 seulement si le programme est quitté avant que l’image affichée soit changée, et 1 sinon.

Social tagging: > > > >

Laisser un commentaire