Ingens Networks  Ingens Developments  Ingens Biometrics  Ingens Hostings

Multiprocesamiento con hilos en Objective-C: Grand Central Dispatch (NSOperation)

30 julio 2012 10:53 by rafael.aguilar

Grand Central Dispatch


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.

 

Resultado final

 

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 ;)

 

 

Tags: , , , , ,

iOS

Comentarios (11) -

30/07/2012 16:42:08 #

Max

Muy bueno didáctico y preciso.
Felicitaciones por el post, a buen seguro será de utilidad y contribuirá a la mejora global de este mundo de tecnología (de todos)!

Max

30/07/2012 16:55:19 #

Josep Rivas

Tema muy interesante, y como de costumbre muy claramente explicado...por cierto muy bueno el inciso sobre libdispatch, tiene mucho juego... ;)

Josep Rivas

30/07/2012 16:55:32 #

Bilito

Otra perla más que nos suelta nuestro amigo Rafa.  Estas sencillamente impresionante y nos estás ayudando a descubrir nuevas cosas que en un principio ni se me pasarían por la cabeza.
Sigue a este nivel tan alto y serás una estrella en esto.

Bilito

30/07/2012 19:57:22 #

Mazagonero

Buenísimo, como siempre.

Mazagonero

31/07/2012 8:39:36 #

Al

Muy buen post Rafa. Gracias!!

Al

01/08/2012 19:48:27 #

Javier Soto

GCD (libdispatch) no es NSOperation y NSOperationQueue. Éstas clases existen desde mucho antes de que GCD llegara. libdispatch es una librería escrita en C, basada en los blocks de Objective-C, y aunque su uso tiene relación con NSOperation, etc, no tienen nada que ver.

Javier Soto

01/08/2012 20:04:17 #

rafael.aguilar

Hola, Javi.

Tienes razón que NSOperation y NSOperationQueue están antes que llegara GCD pero a partir de iOS 4 ya utilizan GCD. Documentación oficial de Apple "In iOS 4 and later, operation queues use Grand Central Dispatch to execute operations. Prior to iOS 4, they create separate threads for non-concurrent operations and launch concurrent operations from the current thread."

Como digo en el post "...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." Hago una muy breve introducción a GCD para que la gente sepa de lo que estamos hablando y ya me centro en NSOperation.

Gracias por tu aporte

rafael.aguilar

01/08/2012 20:13:08 #

Javier Soto

Ok, es el título el que es un poco "misleading".

Otro consejo: enseñar a la gente a nombrar las variables en español es una malísima práctica... pero bueno, es sólo una opinión.

Otro detalle: cuando la operación termina, y "posteas" la NSNotification, esa se "entrega" en la misma queue en la que la envías. Luego en el controlador, estás modificando un UIActivityIndicator desde una queue diferente a la main_queue ;)

Y por último, otra crítica constructiva sería la cantidad de código repetido y "magic numbers" que tienes en el método main de la operation...

Javier Soto

01/08/2012 20:51:09 #

rafael.aguilar

Muchas gracias Javi por tus comentarios. Voy a ver para corregirlos y tenerlos en cuenta para la próxima ;)

rafael.aguilar

02/08/2012 10:04:15 #

rafael.aguilar

He actualizado el contenido para cuando mande la notificación nos aseguremos que lo hace en el hilo principal.

Gracias Javi.

rafael.aguilar

01/02/2013 20:53:59 #

J

Los desarrolladores Mac utilizan el su procesamiento múltiple de Java para optimizar este tipo de eventos?

J

Agregar comentario

biuquote
  • Comentario
  • Vista previa
Loading

Archivo

Pregunta

¿Cuanto dinero gastas mensualmente en la store de tu smartphone (googleplay, appstore, marketplace,...)?





Show Results
 Ingens Networks SL en LinkedIn Ingens Networks SL en Twitter