Skip to content
Learni
View all tutorials
Développement iOS

Comment implémenter Blocks et GCD avancés en Objective-C en 2026

Introduction

En 2026, Objective-C reste essentiel pour maintenir des apps iOS et macOS legacy, ou développer des frameworks hybrides avec Swift. Les Blocks et Grand Central Dispatch (GCD) sont des outils avancés pour gérer la concurrence sans threads manuels, évitant les deadlocks et optimisant les performances CPU. Imaginez traiter des images en parallèle : GCD répartit les tâches sur les cœurs disponibles, comme un chef d'orchestre assignant des musiciens. Ce tutoriel avancé vous guide pas à pas pour implémenter des queues personnalisées, des dispatch groups synchronisants et des barriers mutuelles, avec du code 100% fonctionnel compilable via clang. À la fin, vous créerez un processeur d'images asynchrone scalable, bookmarkable pour tout dev senior. (132 mots)

Prérequis

  • Xcode 15+ installé (pour clang et SDK macOS/iOS)
  • Connaissances avancées en Objective-C (pointeurs, mémoire ARC)
  • Terminal macOS/Linux avec clang
  • Fondamentaux C (structs, fonctions)
  • Au moins 1 Go RAM libre pour tests

Définir la classe ImageProcessor avec Block typedef

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

NS_ASSUME_NONNULL_BEGIN

typedef void(^ImageProcessBlock)(UIImage * _Nullable processedImage, NSError * _Nullable error);

@interface ImageProcessor : NSObject

- (void)processImageAsync:(UIImage *)inputImage
                completion:(ImageProcessBlock)completion;

@end

NS_ASSUME_NONNULL_END

Ce header définit un Block typedef pour encapsuler le callback asynchrone : il reçoit l'image traitée ou une erreur. Utilisez NS_ASSUME_NONNULL pour ARC strict, évitant les nullabilité implicites. Piège : oubliez nullable et vous avez des crashes runtime.

Comprendre les Blocks comme closures

Les Blocks en Objective-C sont des closures anonymes capturant le contexte, comme des lambdas en C++. Ils capturent les variables par référence (__block), idéaux pour callbacks sans délégués verbeux. Analogie : un Block est un 'paquet cadeau' contenant code + données, livré asynchrone sans perte.

Implémenter le processing synchrone avec Block capture

ImageProcessor.m
#import "ImageProcessor.h"
#import <UIKit/UIKit.h>

@implementation ImageProcessor

- (void)processImageAsync:(UIImage *)inputImage completion:(ImageProcessBlock)completion {
    // Simulation traitement lourd (flou gaussien)
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        // Capture locale
        __block CGSize size = inputImage.size;
        
        // Traitement simulé
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            UIImage *output = inputImage; // Remplacez par vrai filtre
            if (completion) {
                completion(output, nil);
            }
        });
    });
}

@end

L'implémentation dispatch une tâche sur une queue globale, simule un délai, et callback sur main_queue pour UI. __block permet modifier size si needed. Piège : oublier dispatch_get_main_queue() cause UI freezes ; toujours check if (completion).

Introduire les queues GCD personnalisées

GCD utilise des queues (FIFO) pour ordonner les tâches : globales (system), serial (une à une) ou concurrent (parallèle). Créez-en de custom avec dispatch_queue_create pour isolation.

Ajouter une queue serial custom et dispatch_group

ImageProcessor+Advanced.h
#import "ImageProcessor.h"

NS_ASSUME_NONNULL_BEGIN

@interface ImageProcessor (Advanced)

@property (nonatomic, strong) dispatch_queue_t serialQueue;
@property (nonatomic, strong) dispatch_group_t processGroup;

- (instancetype)initWithCustomQueue;
- (void)processMultipleImages:(NSArray<UIImage *> *)images completion:(ImageProcessBlock)completion;

@end

NS_ASSUME_NONNULL_END

Extension category ajoute queue serial et dispatch_group pour synchroniser N tâches. Properties strong pour ownership ARC. Piège : nil queue cause dispatch sur default, perdant isolation.

Implémenter processing multiple avec dispatch_group

ImageProcessor+Advanced.m
#import "ImageProcessor+Advanced.h"

@implementation ImageProcessor (Advanced)

- (instancetype)initWithCustomQueue {
    self = [super init];
    if (self) {
        _serialQueue = dispatch_queue_create("com.example.serial", DISPATCH_QUEUE_SERIAL);
        _processGroup = dispatch_group_create();
    }
    return self;
}

- (void)processMultipleImages:(NSArray<UIImage *> *)images completion:(ImageProcessBlock)completion {
    dispatch_group_notify(_processGroup, _serialQueue, ^{
        if (completion) completion(nil, nil); // Toutes finies
    });

    for (UIImage *img in images) {
        dispatch_group_enter(_processGroup);
        [self processImageAsync:img completion:^(UIImage *processed, NSError *error) {
            dispatch_group_leave(_processGroup);
        }];
    }
}

@end

Init crée queue serial labeled (debug utile). dispatch_group_enter/leave/notify synchronise : notify fire quand count=0. Piège : déséquilibre enter/leave cause leaks ou hangs infinis.

Maîtriser les barriers pour mutuelle exclusion

Dispatch barriers agissent comme mutex sur concurrent queues : tâches avant/after s'exécutent parallèlement, barrier sérialise. Parfait pour read/write safe.

Queue concurrente avec barrier pour cache thread-safe

ImageProcessor+Barrier.h
#import "ImageProcessor+Advanced.h"

NS_ASSUME_NONNULL_BEGIN

@interface ImageProcessor (Barrier)

@property (nonatomic, strong) dispatch_queue_t concurrentQueue;
@property (nonatomic, strong) NSMutableDictionary<NSString *, UIImage *> *imageCache;

- (void)cacheImage:(UIImage *)image forKey:(NSString *)key;
- (UIImage * _Nullable)cachedImageForKey:(NSString *)key;

@end

NS_ASSUME_NONNULL_END

Ajoute queue concurrent et cache dict. Methods pour read/write avec barrier implicite. Piège : sans barrier, race conditions corrompent cache.

Implémenter barrier read/write

ImageProcessor+Barrier.m
#import "ImageProcessor+Barrier.h"

@implementation ImageProcessor (Barrier)

- (instancetype)initWithCustomQueue {
    self = [super initWithCustomQueue];
    if (self) {
        _concurrentQueue = dispatch_queue_create("com.example.concurrent", DISPATCH_QUEUE_CONCURRENT);
        _imageCache = [NSMutableDictionary dictionary];
    }
    return self;
}

- (void)cacheImage:(UIImage *)image forKey:(NSString *)key {
    dispatch_barrier_async(_concurrentQueue, ^{
        _imageCache[key] = image;
    });
}

- (UIImage *)cachedImageForKey:(NSString *)key {
    __block UIImage *cached = nil;
    dispatch_sync(_concurrentQueue, ^{
        cached = _imageCache[key];
    });
    return cached;
}

@end

dispatch_barrier_async pour write (exclut autres), dispatch_sync pour read safe. Concurrent queue maximise throughput. Piège : dispatch_sync sur même queue peut deadlock si recursive.

Main complet : compiler et exécuter

main.m
#import <Foundation/Foundation.h>
#import "ImageProcessor.h"
#import "ImageProcessor+Advanced.h"
#import "ImageProcessor+Barrier.h"
#import <UIKit/UIKit.h> // Pour UIImage mock

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ImageProcessor *processor = [[ImageProcessor alloc] initWithCustomQueue];
        
        // Mock images
        UIImage *img1 = [UIImage imageNamed:@"test"]; // Remplacez par data
        
        processor.processImageAsync = ^(UIImage *img, NSError *err) {
            NSLog(@"Processed!");
        };
        
        NSLog(@"Setup OK");
    }
    return 0;
}

Main testable : init processor, mock UIImage (utilisez data en prod). Compilez avec clang. Piège : oubliez @autoreleasepool fuit mémoire.

Script compilation bash

build.sh
#!/bin/bash

clang -framework Foundation -framework UIKit -fobjc-arc \
  -o image_processor \
  main.m ImageProcessor.m ImageProcessor+Advanced.m ImageProcessor+Barrier.m

./image_processor

Compile tous fichiers avec ARC (-fobjc-arc), lie frameworks. Exécute. Piège : manquez -framework UIKit crash sur UIImage.

Bonnes pratiques

  • Toujours label queues : dispatch_queue_create("label.unique", ...) pour Instruments debug.
  • Priorisez QoS : QOS_CLASS_USER_INTERACTIVE pour UI, UTILITY pour background.
  • Évitez capture forte : utilisez __weak self dans Blocks pour éviter retain cycles.
  • Testez leaks : Instruments Leaks + Zombies pour Blocks/GCD.
  • Migrez vers Swift Concurrency si possible, mais gardez ObjC pour perf legacy.

Erreurs courantes à éviter

  • Deadlock dispatch_sync sur main_queue depuis main_thread.
  • Retain cycle Block : [weakSelf method] sans __weak.
  • Group imbalance : plus leave que enter crash assert.
  • Barrier sur serial queue : inutile, perd perf ; utilisez concurrent only.

Pour aller plus loin