Utilisation des Blocks en Objective-C

Utilisation des Blocks en Objective-C


Introduits en 2010 avec la sortie d’iOS4, les Blocks ont profondément changé la manière d’écrire des applications en Objectif-C.

L’implémentation de callbacks via la création de protocole est un procédé verbeux mais qui a l’avantage de rester simple à comprendre. Les blocks quant à eux ont une syntaxe déclarative moins évidente à appréhender de prime abord, mais sont bien plus expressifs et permettent d’éviter les allers et retours dans le code entre protocoles, interfaces et implémentations.

A travers cet article, je vous propose de découvrir les différences de styles qu’il existe entre l’usage des protocoles, plus classiques, et des blocks qui se veulent plus modernes.

La gestion d’événements dans iOS

Un programme informatique dans la plus simple de ses expressions un simple traitement exécutant de façon séquentielle une suite d’instructions. Cependant, la plupart du temps une application plus complexe aura à réagir à différents événements qu’ils proviennent d’interactions avec le réseau, avec le système de fichier ou bien encore avec la personne qui le commande via le clavier, la souris ou le touché. Le plus souvent ces événements sont notifiés au programme sous forme de callback.

Sous iOS, un callback résultant d’un événement peut prendre 3 formes:

  • Le callback de type Target-action: Lorsqu’un événement survient, un objet target est notifié par l’objet source de l’événement. Pour cela il va appeler un selecteur. Ce selecteur correspond à l’action. Les timers iOS implémentent ce mécanisme:
#import <foundation /Foundation.h>
#import "Logger.h"

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        Logger *logger = [[Logger alloc] init];

        NSTimer *timer : [NSTimer scheduledTimerWithTimeInterval: 1.0
                                                          target: logger
                                                        selector: @selector:(onTick:)
                                                        userInfo: nil
                                                         repeats: YES];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
    return 0;
}

#import </foundation><foundation /Foundation.h>

@interface Logger : NSObject
- (void)onTick:(NSTimer *)timer;
@end
#import </foundation><foundation /Foundation.h>

@implementation Logger
- (void)onTick:(NSTimer *)timer {
    NSLog(@"Tick!");
}
@end

 

  • Le callback de type Helper objects: Un helper object sera utilisé lorsque l’objet target est susceptible de traiter plusieurs types d’événements. Pour cela, le helper object implémentera un protocole. Les helpers objects sont souvent identifiés par les termes delegate ou bien encore data sources. Une exemple sera donné dans la section suivante.
  • Le callback de type Notification: Lorsque plusieurs objets sont susceptibles d’être intéressés par un ou plusieurs types d’événements, alors ces objets souscrivent au centre de notification. Le centre notification est une classe qui permet de dispatcher des événements à des objets qui se sont au préalable inscrits:

 

#import </foundation><foundation /Foundation.h>
#import "Logger.h"

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        Logger *logger = [[Logger alloc] init];

        [[Reachability sharedReachability] setHostName:@"blog.helyx.org"]];
        [Reachability sharedReachability].networkStatusNotificationsEnabled = YES;

        [NSNotificationCenter defaultCenter]
                                        addObserver:self
                                           selector:@selector(handleReachability: )
                                               name:@"kNetworkReachabilityChangedNotification" object:nil];

        ...

        [[NSRunLoop currentRunLoo] run];
    }
    return 0;
}
#import </foundation><foundation /Foundation.h>
#import "Reachability.h"

@interface Logger : NSObject
- (void)handleReachability:(NSNotificationCenter*)notification;
@end
#import </foundation><foundation /Foundation.h>

@implementation Logger

- (void)handleReachability:(NSNotificationCenter*)notification {
    if([[Reachability sharedReachability] remoteHostStatus] == NotReachable) {
        NSLog(@"Network not reachable!");
    }
    else {
        NSLog(@"Network reachable!");
    }
}

@end

Les protocoles (@protocol)

Le design pattern Delegate (Delegation sur Wikipedia) est un pattern majeur en Objective-C. Il est implémentable à travers la notion de protocole (@protocol). Un protocole est très similaire à une interface java, puisqu’il permet de déclarer des méthodes qui ont pour but d’être implémentées par une classe. Les protocoles se différencient cependant par le fait qu’ils permettent de déclarer des méthodes requises ou optionnelles. Les méthodes optionnelles seront déclarées après le mot clé: @optional, alors que les méthodes requises seront déclarées après le mot clé @required. Lorsqu’une méthode est déclarée sans être précédée par un des mots clés @required ou @optional, le comportement associé par défaut à la méthode est @required.

@protocol MyProtocol

- (void)aRequiredMethod;

@optional
- (void)anOptionalMethod;
- (void)anotherOptionalMethod;

@required
- (void)anotherRequiredMethod;

@end

Cette différentiation entre méthode requise et optionelle est parfaitement adaptée à un modèle de programmation événementielle, puisqu’une classe qui aura la possibilité d’implémenter certaines méthodes de callbacks sans pour autant devoir toutes les implémenter.

Une classe adoptera un protocole via la déclaration suivante sur son interface:

@interface ClassName : SomeSuperclass < protocol list >

De la même manière une categorie adoptera un protocole via la déclaration suivante:

@interface ClassName ( CategoryName ) < protocol list >

Une classe qui adoptera plusieurs protocoles sera déclarée de la façon suivante:

@interface Formatter : NSObject < Formatting, Prettifying >

 

Exemple de programme complet implémentant le protocole NSURLConnectionDelegate via l’utilisation d’un delegate:

#import </foundation><foundation /Foundation.h>
#import "Logger.h"

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        Logger *logger = [[Logger alloc] init];

        NSURL *url = [NSURL URLWithString: @"http://blog.helyx.org"];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest: request
                                                                      delegate: logger
                                                              startImmediately: YES];


        ...

        [[NSRunLoop currentRunLoo] run];
    }
    return 0;
}
#import </foundation><foundation /Foundation.h>

@interface Logger : NSObject {
    NSMutableData *data;
}
@end
#import </foundation><foundation /Foundation.h>
@implementation Logger

- (void)connection:(NSURLConnection *)connection didReceivedData:(NSData *)dataChunk {
    if (!data) {
        data = [[NSMutableData alloc] init];
    }

    [data appendData: dataChunk];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSString *content = [[NSString alloc] initWithData:data encoding: NSUTF8StringEncoding];
    NSLog(@"Received content: %@", content);
    data = nil;
}

- (void)connection:(NSURLCOnnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"Connection failed: %@", [error localizedDescription]);
    data = nil;
}
@end

Les blocks

Les blocks sont une extension du langage C, et sont complètements supportés par le langage Objective-C. Ils sont comparables dans une certaine mesure aux lambda expressions de Java 8 : ils permettent d’encapsuler un segment de code (unit of work) qui peut être exécuté à tout moment. Ce sont des fonctions anonymes portables qui peuvent être passés en tant qu’argument de méthodes ou bien retournés en argument de retour.
Un block a une liste d’arguments typés, et peut avoir un type de retour inféré ou bien déclaré. Il peut également être affecté à une variable et appelé tel une fonction.

Le symbole ^ est utilisé comme marqueur syntaxique pour identifier un block. Dans l’exemple suivant, la variable de block Add prend deux paramètres entiers et retourne un entier en réponse.

int (^Add)(int, int) = ^(int op1, int op2) {
    return op1 + op2;
};
int result = Add(3, 8);

Lorsqu’un block est passé en argument de fonction, il peut être considéré comme un callback ou bien comme une forme de délégation limitée au scope de la fonction.

Comme vu précédemment, le centre de notification utilise un pattern target/action pour enregistrer un callback:

- (void)viewDidLoad {
   [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWillShow:)
                                                 name:UIKeyboardWillShowNotification
                                               object:nil];
}

- (void)keyboardWillShow:(NSNotification *)notification {
    // Code de gestion de la notification
}

Ceci peut être avantageusement remplacé par l’usage d’un callback de type block comme le permet la méthode addObserverForName:object:queue:usingBlock: :

- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotification
                                                      object:nil
                                                       queue:[NSOperationQueue mainQueue]
                                                  usingBlock:^(NSNotification *notification) {
             // Code de gestion de la notification
    }];
}

Le code est non seulement moins verbeux, mais la déclaration inlinée du block rend également la lecture du code plus claire et plus fluide.

Les blocks ont un avantage crucial par rapport aux autres formes de callbacks, puisqu’ils ont accès aux données présentes dans le scope local dans lequel ils sont déclarés. Ainsi, lorsqu’un block est déclaré dans une méthode, non seulement, il aura accès aux variables locales de la méthode, à ses paramètres, mais également aux fonctions et variables globales incluant les variables d’instance. Autrement dit, un block est une closure.
Il faut toutefois noter que les blocks n’ont accès à ces données qu’en lecture seule par défaut. Pour obtenir le droit de modification sur une donnée, il faut déclarer une variable avec le modifier: __block.
Un block est capable d’un tel fonctionnement même après destruction du contexte local dans lequel il a été déclaré tout simplement parce qu’il persiste dans sa structure les variables auquel il a accès.

Les cas d’utilisation des blocks sont nombreux, parmis eux, nous pouvons compter les suivants:

  • Handlers de complétion, de notification, d’erreur
  • Enumération d’éléments
  • Tri d’éléments

Si vous souhaitez énumérer les éléments d’une liste d’objets, vous pouvez utiliser la méthode:

NSArray *deviceManufacturers =
    [NSArray arrayWithObjects:@"Apple", @"Samsung", @"HTC", @"BlackBerry", @"Nokia", nil];

[deviceManufacturers enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
    NSLog(@"%@ constructor at index %d", object, index);
}];

Ce block prend 3 paramètres: l’élément courant du tableau, son index, ainsi qu’un boolean indiquant si l’énumération doit s’arrêter.

Déclarations

La déclaration de block la plus simple est la suivante:

^ {
    NSString *content = @"Some content";
    NSLog(@"The content is: '%@'", content);
};

Cette déclaration intervient lorsqu’un block est déclaré en tant que paramètre de fonction. Dans ce cas nous n’avons ni de paramètre d’entrée, ni de paramètre de retour.

Dans le cas, où nous souhaiterions déclarer un block en tant variable, nous devrions écrire la déclaration suivante:

void (^doSomething)(void) = ^ {
    NSString *content = @"Some content";
    NSLog(@"The content is: '%@'", content);
};

Dans l’exemple précédent, la déclaration de la variable de block indique que le type de retour est void tout comme le paramètre d’entrée. Son nom est doSomething, il est préfixé d’un caret ^ et se trouve entre parenthèses.

void (^doSomething)(void)

Comme indiqué précédemment, les variables de block sont appelables comme des fonctions. il est ainsi possible d’exécuter le block déclaré via l’appel suivant:

doSomething();

Un block étant une closure, il a accès au contexte local dans lequel il est déclaré, il est ainsi possible d’utiliser des variables du contexte local:

NSString *content = @"Some content";

void (^doSomething)(void) = ^ {
    NSLog(@"The content is: '%@'", content);
};

doSomething();

Lorsqu’un block a un type de retour , ou bien des paramètres d’entrée différents de void, la déclaration prend la forme suivante:

int (^Add)(int, int) = ^(int x, int y) {
    return x + y;
};

Ici, la méthode prend 2 entiers en entrée, tandis qu’elle retourne un entier.

Création de types

Il est tout à fait possible de créer des types spécialisés remplaçant avantageusement les déclarations verbeuses de blocks. Leur utilisation rend la lecture d’un code source plus claire.

Ainsi, au lieu d’utiliser une signature de ce type:

- (void)loadDataWithParam: (NSString*)param
                   onLoad:void(^DidLoadObjectsBlock)(NSArray *objects)
                  onError:void(^DidFailWithErrorBlock)(NSError *error) {
   ...
}

Vous pouvez déclarer avantageusement les types de blocks suivants:

typedef void(^DidLoadObjectsBlock)(NSArray *objects);
typedef void(^DidFailWithErrorBlock)(NSError *error);

Et les utiliser pour simplifier la déclaration précédente comme suit:

- (void)loadDataWithParam: (NSString*)param
                   onLoad:(DidLoadObjectsBlock)loadBlock
                  onError:(DidFailLoadWithErrorBlock)failBlock {
   ...
}

La déclaration de types permet d’éviter la répétition de déclarations complexes, et donc permet d’éviter les erreurs de déclarations. En outre, le refactoring de ce type de déclaration devient chose aisée puisqu’il suffit de modifier la déclaration du type plutôt qu’un ensemble plus ou moins conséquent de déclarations de blocks éparpillées dans le code.

Gestion de la mémoire

La gestion de la mémoire des blocks n’est pas aussi triviale qu’il n’y paraît. Ainsi si vous créez un block dans une méthode et que vous le retournez, sa zone mémoire aura été allouée sur la zone de la stack correspondant à l’appel de la méthode et non dans la mémoire heap.

Lorsque vous retournez d’une méthode qui a alloué un block sur la stack, sa zone mémoire sera libérée. Le pointeur du block retourné par la méthode ne sera donc pas valide et provoquera selon toute vraisemblance une erreur de type segfault, ou bien vous travaillerez tout simplement avec une zone mémoire corrompue.

Il est donc nécessaire, si vous créez et retournez un block depuis une méthode d’appeler la méthode copy sur votre block afin de dupliquer sa zone mémoire depuis la stack vers la heap, et ainsi travailler avec une zone mémoire valide qui ne sera pas corrompue ou dé-allouée.

Il est bien entendu nécessaire de releaser les blocks après usage. Les block supportent d’ailleurs l’autorelease. Il est donc possible de retourner un block depuis une fonction après l’avoir copié et marqué en autorelease:

typedef void(^SomeBlock)(void);
@interface Logger : NSObject {
 NSString *level;
}
@end
@implementation Logger
-(SomeBlock)logMessage:(NSString*)message {
 return [[^ {
  NSLog(@"%@: %@", level, message);
 } copy] autorelease];
}
@end

Avec l’arrivée de l’Automatic Reference Counting (ARC), toute la gestion de la mémoire a été simplifiée. Cela vaut également pour les blocks. Il n’est donc plus nécessaire de faire de copie ou bien de release explicite des blocks. Cependant, pour que le compilateur puisse savoir quoi faire, il faut éviter de déclarer un block avec le type wildcard (id).

La concurrence

Parce que les blocks sont des objets portables et anonymes encapsulant du code pouvant être exécuté de façon asynchrone, ils sont une pièce centrale du Grand Central Dispatch (GCD) et de la classe NSOperationQueue, qui sont tous deux des technologies recommandées pour le traitement concurrent. Ainsi, deux des fonctions centrales de GCD, dispatch_sync et dispatch_async prennent en second argument un block.

Conclusion

Bien que les blocks soient une petite révolution, ils continuent de cohabiter avec les différents autres moyens mis à disposition par le langage permettant de gérer des événements. Ils apportent néanmoins clareté et concision aux extraits de codes les exploitant. Associés aux dernières évolutions sur les littéraux, les blocks participent grandement à moderniser le langage sans pour autant apporter de nouvelle complexité. Ils représentent un facteur de simplification de premier ordre lorsqu’ils sont appliqués au code existant en permettant de revoir différents concepts avec beaucoup d’efficacité.

Liens utiles et sources

Leave a Reply