
Grand Central Dispatch (GCD) es una tecnología desarrollada por Apple que permite a los desarrolladores crear con mayor facilidad programas que exprimen hasta la última gota la potencia de los procesadores multicore. El sistema es el que gestiona los hilos de ejecución. GCD está disponible a partir de Mac OSX 10.6 (Snow Leopard) y en iOS a partir del 4.
Como curiosidad comentar que en Septiembre de 2009 Apple decidió hacer el proyecto de GCD Open Source para que la comunidad pueda consultar las APIs de esta, extender esta tecnología y mejorarla. Os dejo el enlace donde podréis encontrar más información al respecto libdispatch.
El multiprocesamiento tiene fama de ser difícil e impredecible por eso Fernando Rodríguez ya habló en su blog sobre GCD pero yo hablaré sobre unas clases existentes en Foundation, las cuales aumentan un nivel más de abstracción para facilitar un poco el trabajo del desarrollador.
CLASES
Las clases principales son NSOperation y NSOperationQueue. Con la primera creamos las acciones que queremos mandar a otro hilo que no sea el principal y la segunda es la que gestionará los hilos generados con las tareas que se les ha proporcionado. Para poder trabajar con NSOperation tendremos que crearnos nuestras propias clases que hereden de esta y mandarlas posteriormente a NSOperationQueue pero esto lo veremos en detalle con un ejemplo práctico. Aunque la forma que recomiendo de hacerlo es crear una subclase de NSOperation quiero mencionar que Foundation también nos proporciona algunas clases que nos ahorra algo de trabajo pero no tenemos tanto control sobre el mismo:
- NSInvocationOperation: Genera una tarea que llama a un método(@selector) determinado sobre un objeto ya existente. Es apropiada en el caso que tu código ya realiza una tarea (la que queremos mandar a otro hilo) en una clase ya existente.
// Creamos la cola
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// Creamos la operación
NSInvocationOperation *indexOperation = [[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(downloadImageURLs) object:nil];
// Añadimos la operación a la cola
[queue addOperation:indexOperation];
- NSBlockOperation: Esta es similar a la anterior lo único que en vez de llamar a un método, ejecuta un bloque.
// Creamos la cola
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// Creamos la operación
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock: ^{
NSLog(@"Inicio del bloque...");
// Hacemos algo
}];
// Añadimos la operación a la cola
[queue addOperation:blockOperation];
PROYECTO
He decidido continuar el proyecto que iniciamos en el artículo "Reconocimiento facial en iOS 5 con Core Image" ya que realizábamos acciones que consumían muchos recursos (detectar caras). En nuestro ejemplo veíamos la foto de una mujer y la app tardaba un poco más en arrancar (tiempo que tardaba en detectar la cara). ¿Qué pasa si en vez de una foto con una sola persona seleccionamos una foto donde aparecen varias? El resultado es el siguiente:

Una sola persona

Varias personas
Como podéis observar, cuando hay muchas caras ya tarda un tiempo considerable. ¿Qué es lo que está pasando? Toda aplicación cuando arranca lo hace en un hilo principal. Este hilo debería ser exclusivo para la UI y para acciones que consuman muy pocos recursos para que la UX sea lo mejor posible y no nos encontremos con "parones" en nuestra app a causa de una acción que consuma muchos recursos y el usuario piense que la app tenga algún bug.
Para mejorar la app de "DetectorCaras" vamos a hacer que lo que consume muchos recursos lo mandamos a otro hilo que no sea la principal. En el hilo principal cargará la imagen que hayamos elegido y mostraremos también un UIActivityIndicatorView para mostrarle al usuario que la app está cargando y cuando termine pintará las diferentes caras detectadas y detendremos el UIActivityIndicatorView.
¿Hay algo que no huele bien aquí verdad?. Como he dicho antes, el hilo principal debería ser exclusivo de la UI entonces ¿Como podemos decirle al UIActivityIndicatorView que se pare desde otro hilo? podemos poner en práctica algo que tocamos hace poco: Patrón Observador.
MANOS A LA OBRA
Primero seleccionaremos una imagen que haya un número de personas considerable, yo he elegido esta:

Ahora deberemos crear una subclase de NSOperation que yo la he llamado MarkFaces.
MarkFaces.h
//
// MarkFaces.h
// DetectorCarasMejorado
//
// Created by Rafael Aguilar Martín on 27/07/12.
// Copyright (c) 2012 Rafael Aguilar Martín. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface MarkFaces : NSOperation
{
UIImageView *imagenCara;
CGAffineTransform transform;
UIView *vista;
}
@property (nonatomic, strong) UIImageView *imagenCara;
@property (nonatomic) CGAffineTransform transform;
@property (nonatomic, strong) UIView *vista;
-(id)initWithImage:(UIImageView *)image transform:(CGAffineTransform)transformacion view:(UIView *)vistaPrincipal;
@end
MarkFaces.m
//
// MarkFaces.m
// DetectorCarasMejorado
//
// Created by Rafael Aguilar Martín on 27/07/12.
// Copyright (c) 2012 Rafael Aguilar Martín. All rights reserved.
//
#import <CoreImage/CoreImage.h>
#import <QuartzCore/QuartzCore.h>
#import "MarkFaces.h"
@implementation MarkFaces
-(id)initWithImage:(UIImageView *)image transform:(CGAffineTransform)transformacion view:(UIView *)vistaPrincipal
{
if (![super init]) return nil;
self.imagenCara = image;
self.transform = transformacion;
self.vista = vistaPrincipal;
return self;
}
// Este es el método principal que cargará cuando empiece la operación en otro hilo
-(void)main
{
CIImage *imagen = [CIImage imageWithCGImage:_imagenCara.image.CGImage];
// Para el detector, utilizamos la constante "CIDetectorAccuracyHigh" la cual nos proporciona una precisión mejor pero requiere más tiempo de procesado
// Más info en http://developer.apple.com/library/ios/#documentation/CoreImage/Reference/CIDetector_Ref/Reference/Reference.html
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeFace context:nil options:[NSDictionary dictionaryWithObject:CIDetectorAccuracyHigh forKey:CIDetectorAccuracy]];
// Creamos un array con todas las caras detectadas en la imagen
NSArray *features = [detector featuresInImage:imagen];
// Hacemos un bucle por si detecta más de un rostro en la imagen
for (CIFaceFeature *faceFeature in features) {
// Convertir coordenadas CoreImage a UIKit
CGRect faceRect = CGRectApplyAffineTransform(faceFeature.bounds, _transform);
// Obtener el ancho de la cara
CGFloat faceWidth = faceFeature.bounds.size.width;
// UIView usando las dimensiones de la cara detectada
UIView *faceView = [[UIView alloc] initWithFrame:faceRect];
// Añadimos un borde alrededor del UIView
faceView.layer.borderWidth = 1;
faceView.layer.borderColor = [[UIColor blueColor] CGColor];
// UIKit en el hilo principal
dispatch_async(dispatch_get_main_queue(),^{
[_vista addSubview:faceView];
});
// Ojo derecho
if (faceFeature.hasRightEyePosition) {
// Convertir coordenadas CoreImage a UIKit
CGPoint rightEyePos = CGPointApplyAffineTransform(faceFeature.rightEyePosition, _transform);
// Creamos una UIView con el tamaño del ojo derecho
UIView *rightEye = [[UIView alloc] initWithFrame:CGRectMake(rightEyePos.x - faceWidth * 0.15, rightEyePos.y - faceWidth * 0.15, faceWidth * 0.3, faceWidth * 0.3)];
rightEye.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.4];
rightEye.center = rightEyePos;
rightEye.layer.cornerRadius = faceWidth * 0.15;
// UIKit en el hilo principal
dispatch_async(dispatch_get_main_queue(),^{
[_vista addSubview:rightEye];
});
}
// Ojo izquierdo
if (faceFeature.hasLeftEyePosition) {
// Convertir coordenadas CoreImage a UIKit
CGPoint leftEyePos = CGPointApplyAffineTransform(faceFeature.leftEyePosition, _transform);
// Creamos una UIView con el tamaño del ojo izquierdo
UIView *leftEye = [[UIView alloc] initWithFrame:CGRectMake(leftEyePos.x - faceWidth * 0.15, leftEyePos.y - faceWidth * 0.15, faceWidth * 0.3, faceWidth * 0.3)];
leftEye.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.4];
leftEye.center = leftEyePos;
leftEye.layer.cornerRadius = faceWidth * 0.15;
// UIKit en el hilo principal
dispatch_async(dispatch_get_main_queue(),^{
[_vista addSubview:leftEye];
});
}
// Boca
if (faceFeature.hasMouthPosition) {
// Convertir coordenadas CoreImage a UIKit
CGPoint mouthPos = CGPointApplyAffineTransform(faceFeature.mouthPosition, _transform);
// Creamos una UIView con el tamaño de la boca
UIView *mouth = [[UIView alloc] initWithFrame:CGRectMake(mouthPos.x - faceWidth * 0.20, mouthPos.y - faceWidth * 0.20, faceWidth * 0.40, faceWidth * 0.40)];
mouth.backgroundColor = [[UIColor greenColor] colorWithAlphaComponent:0.4];
mouth.center = mouthPos;
mouth.layer.cornerRadius = faceWidth * 0.20;
// UIKit en el hilo principal
dispatch_async(dispatch_get_main_queue(),^{
[_vista addSubview:mouth];
});
}
}
// Modificado 2 de Agosto del 2012 09:54
// [self completeOperation];
// Según lo comentado por Javier Soto, para mandar correctamente la notificación para parar el UIActivityIndicatorView debemos forzar que se haga en el hilo principal
dispatch_async(dispatch_get_main_queue(),^{
// Enviamos la notificación para parar el UIActivityIndicatorView
[[NSNotificationCenter defaultCenter] postNotificationName:@"CompleteOperationNotification" object:self];
});
}
@end
Ahora deberemos modificar nuestro ViewController para que quede de la siguiente manera.
ViewController.h
//
// ViewController.h
// DetectorCaras
//
// Created by Rafael Aguilar Martín on 26/04/12.
// Copyright (c) 2012 Ingens Networks S.L. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
{
UIActivityIndicatorView *indicator;
}
@property (nonatomic, strong) UIActivityIndicatorView *indicator;
@end
ViewController.m
//
// ViewController.m
// DetectorCaras
//
// Created by Rafael Aguilar Martín on 26/04/12.
// Copyright (c) 2012 Ingens Networks S.L. All rights reserved.
//
#import <QuartzCore/QuartzCore.h>
#import "ViewController.h"
#import "MarkFaces.h"
@implementation ViewController
// Sustituimos viewDidLoad por viewWillAppear ya que se dijo en la WWDC 2012 que pronto estos (Load,Unload...) estarán deprecated
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Nos registramos para que nos avise cuando haya terminado la operación en otro hilo
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(completeOperationNotification:) name:@"CompleteOperationNotification" object:nil];
NSLog(@"Llamamos a faceDetector");
[self faceDetector];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (UIInterfaceOrientationIsLandscape(interfaceOrientation));
}
-(void)faceDetector
{
// Carga la imagen de la cara
UIImageView *imagen = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"mucha_gente.png"]];
[self.view addSubview:imagen];
self.indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.indicator.hidesWhenStopped = YES;
self.indicator.center = CGPointMake(480/2.0, 320/2.0);
[self.indicator startAnimating];
[self.view addSubview:self.indicator];
// El origen del sistema de coordenadas de CoreImage están en la esquina inferior izquierda
// y el de UIKit está en la esquina superior izquierda. Por eso necesitamos invertir las posiciones
// antes de pintarlas por pantalla.
CGAffineTransform transform = CGAffineTransformMakeScale(1, -1);
transform = CGAffineTransformTranslate(transform,
0,-imagen.bounds.size.height);
// Creamos la cola
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// Inicializamos nuestra NSOperation
MarkFaces *markFaces = [[MarkFaces alloc] initWithImage:imagen transform:transform view:self.view];
// Lo añadimos a la cola
[queue addOperation:markFaces];
}
-(void)completeOperationNotification:(NSNotification*) notification
{
[self.indicator stopAnimating];
}
@end
Con esto conseguimos que la app arranque de manera rápida y la detección de caras pase a segundo plano y saldrá cuando tengamos resultados y mientras tanto podríamos seguir interactuando con la app.

Podéis descargar el proyecto DetectorCarasMejorado.zip (419,89 kb)
NOTA 1: Esto solo es la "punta del iceberg" y es súper apasionante el mundo de Grand Central Dispatch. Os recomiendo encarecidamente que le echéis un vistazo a este enlace.
NOTA 2: Parece ser que el reconocimiento de Core Image no es tan fiable sino veamos el resultado final que no detecta todas las caras :P. Esto se deberá a la baja calidad de la imagen, así que probad con imágenes con mayor resolución y os sorprenderá sus resultados ;)