La gestion de la mémoire (2)

27112009 Dans: Cocoa, Objective-C

Dans le premier épisode, nous avons vu qu'un objet qui avait alloué la mémoire pour un autre objet était également responsable de la libérer lorsque l'objet ne lui était plus nécessaire. Ce système est similaire à l'allocation dynamique de la mémoire telle qu'on la connaît en langage C sous la formes des fonctions malloc() et free().

Cependant, ce système comporte un inconvénient majeur dès qu'un objet est passé d'un objet à un autre. Étudions la séquence suivante:

  • Un objet A alloue un objet B.
  • L'objet A passe l'objet B à un objet C. L'objet C garde un pointeur sur l'objet B dont il a encore besoin par la suite.
  • L'objet A n'a plus besoin de l'objet B. Il lui envoie donc un message -[release].
  • L'objet C tente d'appeler une méthode de l'objet B, ce qui mène à un plantage.

Il a donc fallu incorporer un système qui permette de conserver un objet en mémoire tant qu'il est nécessaire, et le libérer lorsqu'il ne l'est plus. Ce système est l'autorelease.

Autorelease pool

Examinons l'exemple suivant:

NSArray* monArray = [[NSArray alloc] init];
[monArray autorelease];

La première ligne alloue, comme nous l'avons vu la dernière fois, un objet de type NSArray et l'initialise. La deuxième ligne ajoute monArray à l'autorelease pool courant.

Un objet de type NSAutoreleasePool conserve une liste d'objets. Ces objets seront désalloués à la fin de la boucle d'événements.

La boucle d'événements

Le moteur d'exécution (runtime) est un programme qui offre une structure à l'exécution des programmes écrits en Objective-C. Il comporte une boucle d'événements:

  • Au début de la boucle, les événements (frappe clavier, mouvements de la souris, timers écoulés, messages des autres applications, etc.) sont récupérés.
  • L'application (votre code !) traite les événements.
  • Les autorelease pools sont vidés
  • Puis on revient au début de la boucle.

Ce principe permet de garantir l'existence de l'objet pendant l'exécution d'un itération de votre code.

Dans les applications Cocoa utilisant une interface graphique, un NSAutoreleasePool est instancié automatiquement pour le thread principal. En général, vous n'avez donc pas besoin d'en créer un. Vous serez toutefois amené à le faire si vous concevez une application multi-thread, ou si vous instanciez un grand nombre d'objets dont la durée de vie est limitée.

Retain/Release

La gestion de la mémoire en Objective-C est souvent expliquée par l'analogie suivante: un petit chien se met à courir dès qu'il n'a plus aucune laisse autour du cou. Tant qu'il a au moins une laisse au cou, il ne peut s'échapper. Évidemment, s'il a plusieurs laisses qui le retiennent, il ne peut pas non plus s'enfuir.

Envoyer un message -[retain] à un objet lui passe une laisse autour du cou.

Lui envoyer un message -[release] lui retire une laisse.

Concrètement, chaque objet héritant de NSObject possède une variable d'instance retainCount. Il s'agit du nombre de "laisses":

  • +[alloc] initialise retainCount à 1.
  • -[autorelease] décrémente retainCount et place l'objet dans l'autorelease pool courant.
  • -[release] décrémente retainCount
  • -[retain] incrémente retainCount

Lorsque survient la fin de la boucle d'événements, l'autorelease pool libère tous les objets dont le retainCount est nul.

Constructeurs de commodité

Il arrive fréquemment de créer un objet, de le passer à un autre objet, puis de ne plus en avoir besoin. Les deux exemples suivants sont équivalents. Nous voulons passer une liste d'invités à un objet fiesta:

Exemple 1:

NSArray* invites = [[NSArray alloc] initWithObjects:@"Pascal", @"Florence", @"Martin", @"Patrick"];
[invites autorelease];
[fiesta setInvites:invites];

Exemple 2:

NSArray* invites = [NSArray arrayWithObjects:@"Pascal", @"Florence", @"Martin", @"Patrick"];
[fiesta setInvites:invites];

La méthode +[arrayWithObjects:] est un constructeur de commodité, qui permet de faire l'allocation, l'initialisation et l'autorelease en une seule opération. Ce type de méthodes est très courant dans Cocoa. En fait par convention, si le premier mot du nom d'une méthode est celui de la classe, vous est certain que l'objet renvoyé est autoreleasé.

Ajout à une collection

Pour finir, sachez que les collections retiennent les objets qu'elle contiennent. C'est à dire que les objets de types: NSArray, NSSet, NSDictionary et compagnie, envoient un message -[retain] aux objets qui leurs sont ajoutés. Quand la collection est libérée, elle envoie un message -[release] à tous les objets qu'elle contient.

Exemple:

@interface Livre : NSObject
{
    NSMutableArray* pages;  
}

@end

@implementation Livre
- (id) init
{
    if(self = [super init])
    {
        // Créer la liste des pages
        pages = [[NSMutableArray* alloc] init];

        // Ajouter une première page vierge
        Page* pageVierge = [[Page alloc] init];     // retainCount = 1
        [pages addObject:pageVierge];               // retainCount = 2
        [pageVierge release];                       // retainCount = 1
    }
    return self;    
}

- (void) dealloc
{
    [pages release];

    [super dealloc];    
}

L'objet pageVierge reçoit un message -[retain] quand elle est ajoutée au NSMutableArray pages. Nous devons donc lui envoyer un message -[release] pour qu'elle soit effectivement désallouée lorsque le livre sera désalloué.

Bientôt la suite

Nous n'en n'avons pas encore fini. Je vous encourage à poser des questions si un aspect n'est pas clair et que vous souhaitez le voir développé.

Renaud Pradenc
Céroce

La gestion de la mémoire (1)

03112009 Dans: Cocoa, Objective-C

La gestion de la mémoire est un sujet qui — bien qu'aussi vieux que Cocoa elle-même — semble toujours aussi mal compris. Cette série d'articles va tenter d'expliquer les choses calmement pour que les nouveaux venus à Cocoa cessent de se torturer avec une question finalement assez simple quand on a bien assimilé deux ou trois principes de base.

Manuel ou automatique

Apple a profité de la version 10.5 de son système d'exploitation pour ajouter au langage Objective-C un ramasse-miettes (garbage collector), système bien connu des programmeurs en Java. Certains en sont partisans, d'autres les exècrent; personnellement, je pense qu'historiquement les langages de programmation évoluent pour éloigner le programmeur des arcanes de la machine, et que c'est une bonne chose: ce n'est pas quand on met au point un algorithme compliqué qu'on veut que la gestion de la mémoire nous pose problème.

Ceci dit, dans mes programmes ObjC, je gère toujours la mémoire manuellement parce que le ramasse-miettes est un rajout, et que Cocoa n'a pas été pensée pour fonctionner avec un ramasse-miettes; certaines choses en deviennent très complexes. Par ailleurs, avec le temps, de nombreux outils ont été développés pour aider à diagnostiquer les problèmes de gestion manuelle de la mémoire, rendant le débogage finalement assez facile.

Si vous visez des systèmes inférieurs à 10.5, ou plus probablement, que vous programmez l'iPhone, alors vous n'avez pas le choix, vous devrez gérer manuellement la mémoire. Continuez la lecture de cet article…

Principe n°1: L'objet qui alloue la mémoire est responsable de sa libération

Un objet alloue habituellement la mémoire pour un autre objet en envoyant le message -[alloc] à sa classe.

NSString* chaine = [NSString alloc];

Ce même objet devra libérer l'objet quand il n'en aura plus besoin, en lui envoyant un message -[release]:

[chaine release];

Allocation et libération d'une variable d'instance

Utilisons ici un exemple d'un programme possédant des classes Livre et Sommaire. Un Livre possède une seule instance de Sommaire:

@interface Livre: NSObject
{
     Sommaire*  sommaire;
}

@end

La méthode -init de Livre va ressembler à ceci:

- (id) init
{
    if (self = [super init])
    {
        sommaire = [[Sommaire alloc] init];
    }

    return self;
}

L'instance de Livre a alloué à sa création une instance de Sommaire. Selon le principe n°1, elle est donc responsable de sa libération. En général, elle le fera quand elle-même est sur le point de disparaître de la mémoire, dans sa méthode -[dealloc]:

- (void) dealloc
{
    [sommaire release];

    [super dealloc];    // Permettre à la classe parente de désallouer
                    // ses propres variables d'instance
}

Quelques compléments

copy

Un objet qui envoie un message -[copy] à un autre objet, alloue nécessairement la mémoire pour stocker la copie. Il devra donc libérer la copie, mais pas l'original.

new

Cette méthode équivaut à -[[alloc init]. Il faudra donc libérer la mémoire.

[self release]

Ne doit jamais être fait. C'est l'objet qui nous a alloué qui doit nous libérer.

Pour finir

Voilà déjà pour cette première partie qui couvrait un point essentiel. Nous verrons la prochaine fois comment passer un objet dont nous n'avons plus besoin à un autre objet, tout en étant sûr qu'il sera bien libéré.

Renaud Pradenc
Céroce

LLVM : Quoi de nouveau ?

26102009 Dans: C, Mac OS X, Objective-C

LLVM, le compilateur qu'Apple utilise de plus en plus (pour proposer les blocks, pour avoir un temps de compilation plus réduit) vient de sortir en version 2.6. Je vous invite si le sujet vous intéresse à lire les ressources suivantes :

Et si vous voulez suivre quelques conférences sur LLVM, il sera organisé un LLVM Camp à Paris le vendredi 20 novembre.

Une des grandes nouveautés de Snow Leopard, est l'arrivé de Grand Central Dispatch qui permet la mise en place des taches concurrentes sur plusieurs coeurs ou plusieurs processeurs. Mais la programmation concurrente implique un paradigme différents de ce que l'on a l'habitude de voir avec les langages objets ou impératifs.

Si vous voulez apprendre à utiliser Grand Central Dispatch (GCD), je vous invite à découvrir les ressources suivante pour une prise en main rapide :

Et si vous voulez aller plus loin, l'Apple Developer Connection propose les ressources suivantes :

Sachez en plus que Apple mis le code de GDC sous licence Open source sous le nom de libdispatch, et l'on peut noter qu'un port est d'ores et déjà disponible pour FreeBSD.

De retour de vacances, je vais commencer par m'intéresser aux dernières publications (livres, magazines, etc...) sur le développement à la fois Mac et iPhone :

Apple vient d'annoncer son projet d'abandonner le langage Objective-C en faveur de Java pour un certain nombre de raisons :

  • Il est nettement plus facile de trouver des développeurs Java que des développeurs Objective-C.
  • Java est le seul vrai langage pour écrire des applications Enterprise
  • La compatibilité des application iPhone avec les applications Android (qui utilise déjà Java).

Le SDK iPhone devrait donc rapidement apparaître dans une nouvelle version permettant l'utilisation de Java, et d'après les rumeurs, une version 4.0 de XCode basé sur Eclipse devrait être remise aux développeurs lors de la WWDC 2009.

Les bogues ne sont pas une fatalité: ce sont des défauts dus à des erreurs d'êtres humains:

  1. Trop grande complexité
  2. Méconnaissance des API ou du langage
  3. Étourderies
  4. Laxisme !

Comme vous êtes comme moi et détestez passer vos après-midi sur un débogueur, voici quelques stratégies éprouvées pour les réduire.

10. Documentez

Je vais ici seulement insister sur les aspects de la doc qui permettent de limiter les bogues :

  • Rédigez une partie de la description d'une méthode avant de la coder
    Vous devez réfléchir aux cas particuliers du déroulement, aux valeurs possibles des arguments d'entrée (notamment comment gérer les gammes de valeurs inattendues), et quelles erreurs peuvent subvenir pendant l'exécution. Inspirez-vous de la programmation par contrat.

  • Faites des commentaires utiles
    Ils ne doivent pas paraphraser le code, ni expliquer chaque appel de méthode. Expliquez le cheminement du code, et soulignez ce qui est le moins évident. C'est très utile au débogage, quand on revient au code quelques semaines plus tard.

  • Nommez vos variables avec grand soin
    Je suis toujours étonné qu'à notre époque, les gens nomment encore leurs variables i,j, tab[] ou ptr, dans la plus pure tradition du Kernighan & Ritchie. Il m'est arrivé de rendre des bogues évidents rien qu'en renommant.

9. Relisez-vous

D'expérience, la moitié des bogues pourraient être évités lors de l'écriture du code. Je ne parle pas des erreurs de syntaxes (signalées par le compilateur), mais utiliser une variable à la place d'une autre, effectuer les opérations dans le mauvais ordre, etc.

Si vous programmez seul, pensez à relire — avec un œil critique s'entend — votre nouveau code systématiquement avant de compiler.

8. Corrigez les bogues dès qu'ils apparaissent

Il arrive fréquemment de découvrir un bogue, et de le laisser à plus tard. Comme vous êtes un programmeur consciencieux, vous y reviendrez. Mon expérience dicte de corriger le bogue immédiatement, parce qu'il arrive souvent de commettre encore et encore la même erreur. Mieux vaut donc la corriger au plus tôt, pour ne faire l'erreur qu'une fois.

7. Testez les codes d'erreur et gérez les exceptions

À moins que cela n'alourdisse exagérément le code, il est sage de vérifier les résultats des fonctions. Gérez aussi les exceptions. Ces deux tâches triviales ne doivent pas être laissées à plus tard — c'est à dire jamais.

6. Créez des projets d'essai

Bien que je travaille sur un gros projet, je crées de nombreux petits projets annexes. Le dernier en date était destiné à vérifier sur mon site web si une nouvelle version de l'appli est disponible. Je n'avais aucune expérience du téléchargement d'un fichier texte. Sur ce petit programme de vingt lignes, j'avais tout de même commis deux bogues. Avoir un programme léger permet de les cerner rapidement.

Si j'avais intégré la fonction directement dans mon appli, je me serais demandé si les problèmes venaient du nouveau code ou de l'ancien. J'aurais peut être modifié du code valide. Enfin, maîtriser une technologie permet de savoir comment l'intégrer au mieux au reste de l'application.

5. Ayez des scénarios de test sous la main

D'expérience, il arrive que les bogues apparaissent dans des circonstances déjà rencontrées. Lorsque vous avez trouvé une procédure pour reproduire un bogue à coup sûr, notez-là et déroulez cette procédure à chaque version. Si possible, automatisez les tests. Par exemple, les tests de l'interface utilisateur peuvent être automatisés avec Instruments.

4. Écrivez des tests unitaires

Les tests unitaires s'appliquent essentiellement aux classes de la couche modèle. Ma première expérience fut une classe que je n'arrivais pas à mettre au point pour un programme de musique, qui devait renvoyer les notes faisant partie d'une gamme. J'étais capable de trouver ces notes sur le papier, mais je m'y suis repris à trois fois avant d'avoir un algorithme… qui ne marchait pas dans certains cas particuliers.

Après avoir écrit les tests unitaires, les erreurs apparaissaient de façon évidente dans la couche modèle, plutôt que de manière indirecte dans l'interface utilisateur. J'ai gagné du temps, et j'étais enfin sûr de mon code.
Nous reviendrons sur les tests unitaires, à la mode, et ceci pour d'autres très bonnes raisons.

3. Réduisez les contraintes

Comme dit plus haut, la cause principale des bugs est la complexité. En effet, des scientifiques ont mesuré que les individus les plus doués étaient capables de prendre en compte simultanément sept contraintes. Pour la plupart des gens, c'est plutôt quatre (<- c'est là que je me situe).

L'idée est donc de limiter la complexité du logiciel:

  • en ajoutant de l'abstraction
    pour pouvoir se concentrer sur la tâche présente au lieu des détails d'implémentation.

  • en limitant le couplage
    c'est à dire les interdépendances entre objets.

Et cela est difficile. Très. Beaucoup de gens se disent programmeur ("oui, j'ai étudié un peu le langage C pendant mes études de biologie"), mais peu ont déjà travaillé sur un projet conséquent, un où les bogues deviennent difficiles à résoudre et semblent réapparaître sans cesse.

Je n'ai pas de secret, il faut se forger une expérience: étudier les patrons de conception, savoir comment fonctionne la machine et le système d'exploitation, étudier d'autres langages de programmation ou d'autres bibliothèques…

2. Ne construisez que les infrastructures nécessaires maintenant

Ce point est un peu en contradiction avec le précédent. En effet, quand on commence à ajouter de l'abstraction, on a ensuite tendance à vouloir rendre tout réutilisable et générique. Imaginons que vous vouliez écrire un texte en rouge. Il est tentant d'écrire une méthode générique qui prend la couleur en paramètre, pour le jour où vous voudriez écrire en vert.

Pourtant, la méthode générique sera forcément plus difficile à écrire et à tester. Pourquoi passer du temps sur quelque chose d'inutile aujourd'hui ? Attendez un jour prochain que le besoin soit réel. De plus, une méthode simple sera une bonne base de départ pour écrire une méthode plus complexe.

1. Codez moins

Aucun code n'est plus flexible que pas de code.

Il existe souvent une méthode qui rend 80% du service avec seulement 20% du travail que demanderait une méthode parfaite. Ne visez pas la perfection. La vie est trop courte ! Demandez-vous si vos utilisateurs préfèrent une fonction parfaite dans un an ou une fonction correcte aujourd'hui.

Moins de code = moins de bogues
Travaillez le cœur de métier de votre logiciel. Les détails seront réglés plus tard (voire jamais).

Pour finir

Voilà les méthodes que j'utilise — avec plus ou moins de rigueur — pour conserver mes applis stables. Auriez-vous d'autres techniques à proposer ?

En vrac de Noël

29122008 Dans: En vrac, iPhone / iPod Touch, Liens, Objective-C

Après quelques jours loin de mon ordinateur pour cause de repas de Noël, et avant quelques jours loi de tous accès Internet pour le nouvel an, voici quelques articles intéressants pour finir l'année 2008 :

Le prochain billet arrivera certainement en 2009, je vous souhaite donc un très bon réveillon.

Objective-C et les tests unitaires

06122008 Dans: Objective-C

On me demandait dans les commentaires du billet précédent ce qu'était exactement les tests unitaires, je vais donc essayer d'expliquer à quoi ils servent et vous proposer quelques ressources pour aller plus loin.

Comme le nom l'indique, il s'agit de tester le comportement d'un unité d'un programme, c'est à dire par exemple une fonction ou une classe. Pour une fonction qui convertit une température de Celsius en Fahrenheit, on va tester que le résultat de la conversion pour différentes valeurs. On testera des valeurs négatives, positives, des lettres, etc. pour s'assurer que le résultat est correct et que les erreurs sont bien traités. Les tests permettent aussi d'éviter les régressions de votre programme, car lors d'une modification de votre code, vous pouvez exécuter les tests et vous assurez que le comportement n'a pas été altéré par vos modifications récents.

Vient ensuite le concept de couverture de code. Il s'agit de connaître le pourcentage des lignes de code du programme qui sont testés par votre jeu de tests unitaires. Par exemple, si votre fonction contient du code conditionnel avec l'instruction if/else, il faut que les différents tests de la fonction fassent intervenir à la fois le code dans le bloc if que dans le bloc else. Plus le pourcentage est élevé plus vous pouvez avoir confiance dans le comportement de votre code.

Pour allez plus loin, n'hésitez pas à lire les ressources suivantes :

J'espère avoir été assez clair, mais n'hésitez pas en cas de besoin à poser vos questions dans les commentaires.

Une des pratiques importantes dans le développement logiciel, est l'utilisation de tests unitaires. Cela permet de s'assurer du comportement de son code, d'éviter les régressions et de manière général d'avoir plus confiance en son code.

Google vous propose dans le cadre de son Google Mac Developer Playground un certain nombre d'outils pour développeur dont CoverStory, qui permet de visualiser facilement le taux de couverture de votre code à partir des fichiers générés par Gcov.

CoverStory

Pour plus d'informations sur CoverStory, les outils Google pour développeurs Mac et Gcov, utilisez les liens ci-dessous :


Sponsors