Servicios y herramientas en iOS
 

Background e hilos

En alguna ocasión tendremos que ejecutar ciertos procesos de forma paralela en nuestras aplicaciones, a esto lo llamamos concurrencia y lo haremos mediante el uso de hilos o threads. La implementación de hilos en aplicaciones para móviles es muy común ya que nos permite continuar con la ejecución mientras realizamos varios procesos de forma simultánea, por ejemplo la carga de imágenes desde un servidor. Estos hilos se pueden ejecutar en segundo plano o background.

Las aplicaciones iOS arrancarán con un único hilo de ejecución (main) y nosotros, mediante código, podremos crear y eliminar hilos según decidamos. Siempre tendremos que tener presente que los hilos que creemos debemos de cerrarlos en algún momento durante la ejecución, sino estos permanecerán ejecutándose y pueden incrementar la carga de procesamiento, CPU, del dispositivo.

Gestión de hilos en iOS

La implementación de hilos siempre ha sido una tarea complicada en el desarrollo software ya que implica poseer conceptos complejos a muy bajo nivel de programación. Gracias al SDK de iOS esta tarea se simplifica bastante y sólo tendremos que "preocuparnos" de un par de clases: NSThread y NSOperation. Mediante el uso de NSThread gestionaremos los hilos a un nivel algo más bajo que con NSOperation. El la mayoría de casos nos sobraría con utilizar esta última.

Creando un hilo

En este apartado aprenderemos como crear un hilo de ejecución usando las clases NSOperationQueue y NSInvocationOperation. El proceso que debemos de seguir para creación de un hilo es el siguiente:

  • Instanciamos un objeto de la clase NSOperationQueue
  • Instanciamos un objeto de la clase NSInvocationOperation indicando un selector (método que se ejecutará en el hilo) y un parámetro del método.
  • Añadimos la operación (objeto de la clase NSInvocationOperation) a la cola que hemos definido en el primer paso.

La implementación quedaría como sigue:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
 
NSInvocationOperation *operation = [[NSInvocationOperation alloc] 
	initWithTarget:self
    selector:@selector(metodoEnHilo:)
    object:objectToPassToMethod];
 
[queue addOperation:operation];    

La clase NSOperationQueue es la encargada de ejecutar las operaciones que le indiquemos. Esta actúa como una cola de operaciones y puede realizar más de una operación de forma concurrente. El número de operaciones concurrentes que deseamos que se realicen se indica con el parámetro maxConcurrentOperationCount. Si indicamos que sólo realice una operación concurrente la clase NSOperationQueue actuará como un servicio de cola estricto, es decir, sólo se ejecutará un proceso si el anterior ha finalizado. Por defecto, el máximo número de operaciones concurrentes está definido en el máximo permitido y lo gestionará el sistema.

La clase NSInvocationOperation es una subclase de NSOperation y es la encargada de gestionar una tarea en concreto, un método. Existe otra subclase de NSOperation que está diseñada para ejecutar bloques o blocks. Los blocks en Objective-C son porciones de código similares a funciones o métodos que realizan alguna tarea determinada. Son muy útiles a la hora de implementar callbacks, es decir, código después de la ejecución de alguna tarea asíncrona como por ejemplo llamadas a un servicio web.

En el siguiente apartado vamos a realizar un ejemplo concreto que implemente el uso de hilos en una aplicación iOS.

Ejemplo: Carga de datos usando hilos

Con el fin de consolidar los conceptos estudiados sobre el uso de hilos en Objective-C vamos a realizar un pequeño ejemplo en el que cargaremos el código html de una serie de páginas web de forma asíncrona y lo mostraremos por consola.

Después de crear un proyecto en XCode usando la plantilla Single View Application abrimos el fichero de la vista principal, en nuestro caso es UAViewController.m. Ahí vamos a rellenar un array con un listado de al menos diez sitios web que más adelante cargaremos de forma asíncrona usando hilos.

Primero implementamos el método encargado de cargar una página web, obtener todo su código html y mostrarlo por consola:

-(void) obtieneHtmlSitioWeb:(NSString *)url
{

    NSString *webpageString = 
    [NSString stringWithContentsOfURL:[NSURL URLWithString:url]
                             encoding:NSUTF8StringEncoding error:nil];

   
    NSLog(@"HTML WEB: %@", url);
    NSLog(@"%@", webpageString);
}
    

Ahora en el método viewDidLoad rellenamos un array con las url de los sitios web:

// Rellenamos un array de sitios web de prueba
NSMutableArray *arraySitiosWeb = [[NSMutableArray alloc] init];
    
[arraySitiosWeb addObject:@"http://www.ua.es"];
[arraySitiosWeb addObject:@"http://www.umh.es"];
[arraySitiosWeb addObject:@"http://www.jtech.ua.es"];
[arraySitiosWeb addObject:@"http://web.ua.es/especialistamoviles"];
[arraySitiosWeb addObject:@"http://www.google.es"];
[arraySitiosWeb addObject:@"http://www.yahoo.com"];
[arraySitiosWeb addObject:@"http://www.apple.com"];
[arraySitiosWeb addObject:@"http://web.ua.es/es/especialistamoviles/desarrollo.html"];
[arraySitiosWeb addObject:@"http://web.ua.es/es/especialistamoviles/profesorado.html"];
[arraySitiosWeb addObject:@"http://web.ua.es/es/especialistamoviles/contenidos.html"];

Por último tenemos que implementar el sistema de hilos y colas estudiado en el apartado anterior. Esto lo realizaremos al final del método viewDidLoad tal y como se puede ver en el fragmento de código siguiente:

// Creamos una cola
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
// Recorremos el array creando una operación para cada item y añadiendolo a la cola
for (NSString *sitioWeb in arraySitiosWeb)
{
	// Creamos una operacion para cargar la web
	NSInvocationOperation *operation = [[NSInvocationOperation alloc] 
		initWithTarget:self
			  selector:@selector(obtieneHtmlSitioWeb:)
				object:sitioWeb];
        
	// Añadimos la operacion a la cola
	[queue addOperation:operation];
}
    

Con esto ya tenemos implementado nuestro sistema de hilos concurrentes. Si ejecutamos la aplicación veremos que aparecerán por consola, y de una forma desordenada, las cargas de las páginas web (código html) sin bloquear la interfaz de usuario.

Bloqueo de interfaz
Para comprobar que la interfaz no se bloquea durante la carga de los sitios web podemos incluir una vista de tabla (TableView) en la vista principal. Si podemos hacer scroll en la tabla mientras se están cargando las páginas web significa que estamos usando los hilos de forma correcta.

Ejecución de métodos entre distintos hilos

En el apartado anterior hemos estudiado como crear hilos y ejecutarlos de forma que no bloqueen la interfaz principal. Este bloqueo lo evitamos porque mantenemos en todo momento, precisamente mediante el uso de hilos, la ejecución principal (hilo principal) libre de carga.

Si queremos devolver un resultado desde un hilo secundario al hilo principal podemos usar el método de la clase NSObject: performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait , por ejemplo:

// Después de realizar una carga de datos para mostrar en una tabla
// recargamos la tabla
[self.tableView performSelectorOnMainThread:@selector(reloadData) 
withObject:nil 
waitUntilDone:YES];   

Además del método anterior existen otros también bastante útiles que podemos encontrar en la documentación oficial de Apple.

Modo background

El modo background o multitarea fue introducido por Apple a partir de la versión 4.0 de iOS. Gracias a el podemos ejecutar ciertos procesos una vez que la aplicación se encuentre en segundo plano. En este apartado estudiaremos más detenidamente cómo funciona el modo background en iOS, así como la forma en la que podemos hacer uso de el en nuestras aplicaciones y sus restricciones.

Estados de ejecución en iOS

A la hora de desarrollar para iOS debemos de conocer los diferentes estados de ejecución que puede adoptar nuestra aplicación. Concretamente, tendremos que controlar si la aplicación está funcionando en un momento dado en segundo plano (background) o en primer plano (foreground).

Si la aplicación se encuentra activa en primer plano, esta será capaz de gestionar todas las interacciones con la interfaz de usuario, todos los controles UIKit así como el resto de procesos que implementemos.

Por otro lado, si la aplicación se encuentra en segundo plano, la mayoría de procesos dejarán de ejecutarse (controles UIKit, procesos internos...) y tendremos que definir cuales queremos que continuen ejecutándose y por cuanto tiempo. Debemos de tener en cuenta que la ejecución de procesos en segundo plano tiene una serie de limitaciones que analizaremos más adelante.

En el gráfico siguiente podemos ver claramente el flujo de los diferentes estados por los que pasa una aplicación iOS desde que empieza a ejecutarse hasta que se detiene:

Estados de ejecución en iOS

Como desarrolladores tendremos que tener muy en cuenta los estados de background y de foreground y gestionarlos correctamente desde nuestro código.

Detectando si nuestra aplicación entra en background o foreground

Como se ha comentado anteriormente es importante que gestionemos en nuestras aplicaciones los procesos que se van a ejecutar (si es que se ejecuta alguno) en modo background o segundo plano. Detectar si la ejecución entra en background es muy sencillo gracias a los métodos del protocolo UIApplicationDelegate, los cuales se definen en la clase delegada AppDelegate.

Para detectar la entrada en background debemos de implementar el siguiente método:

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    // TODO
}

Si por otro lado queremos detectar la entrada en foreground (primer plano) debemos de implementar este otro método:

- (void)applicationWillEnterForeground:(UIApplication *)application
{
    // TODO
}

Estos dos métodos son los que deberemos completar si nuestra aplicación lo requiere. En los apartados siguientes estudiaremos como ejecutar procesos en background y cuales de ellos podemos ejecutarlos de manera indefinida.

Ejecutando procesos con tiempo determinado en background

Las aplicaciones pueden ejecutar (o seguir ejecutando) ciertos procesos durante un tiempo muy limitado cuando estos entran en background. Esto se debe de usar para tareas "importantes" y que necesitan algo más de tiempo para finalizar su ejecución, como por ejemplo procesos de descargas de datos que no se pueden interrumpir. Para solicitar un tiempo "extra" de ejecución en background haremos uso del método beginBackgroundTaskWithExpirationHandler: perteneciente a la clase UIApplication.

Mediante el uso del método beginBackgroundTaskWithExpirationHandler: conseguimos algo más de tiempo antes de que la aplicación entre en suspensión (modo no activo). El tiempo extra que se consigue no es siempre el mismo y depende de la carga de procesos que se estén ejecutando en ese momento en el dispositivo. Aproximadamente el sistema iOS admite como máximo 10 minutos de ejecución de procesos determinados en background.

A continuación podemos ver la estructura básica del método applicationDidEnterBackground: que ejecuta uno o varios procesos determinados en background:

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        
		// Terminamos de forma definitiva los procesos pendientes
		
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
 
    // Implementamos las tareas determinadas
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
        // Realizamos las tareas background
 
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    });
}

Ejecutando procesos con tiempo indeterminado en background

Apple permite ejecutar ciertos procesos en segundo plano de forma ilimitada. Estos procesos son los siguientes:

  • Audio: Reproducción de sonidos, música, sistema Airplay... Ej.: Apps de música, Apps que ofrezcan video en background a través de Airplay, etc...
  • Servicios de localización: obtención de datos de la posición actual del usuario. Framework CoreLocation.
  • Voz ip: realizar llamadas voip en background
  • Contenido del newsstand: si la app es de tipo newsstand se permite la descarga de contenidos en background
  • Accesorios externos: se permite la comunicación con hardware externo usando el framework ExternalAccesory
  • Comunicación entre disposivos bluetooth: envio de mensajes entre dispositivos con bluetooth usando el framework CoreBluetooth

Si necesitamos que nuestra aplicación pueda ejecutar alguno de los procesos anteriores deberemos de indicarlo en el fichero Info.plist del proyecto mediante la clave UIBackgroundModes e indicando como valor un array con al menos un valor de entre los siguientes: audio, location, voip, newsstand-content, external-accessory, bluetooth-central, bluetooth-peripheral.

En el caso de que implementemos alguna de las funciones anteriores pero no lo indiquemos en el fichero Info.plist, el proceso automáticamente se detendrá cuando esta pase a segundo plano.

Notificaciones locales

Las notificaciones locales sirven para captar la atención del usuario de nuestra aplicación cuando esta se encuentra en modo background o incluso cerrada. A diferencia de las notificaciones push, estas no se crean ni se lanzan desde servidores externos. Las notificaciones locales se crean desde nuestra propia aplicación y se pueden programar para que aparezcan en una fecha concreta.

Estas notificaciones pueden aparecer en forma de texto, sonido, badge en el icono de la aplicación o una combinación de las tres. Podemos usar las notificaciones locales para avisar al usuario de que es la hora de levantarse para ir al trabajo, usándolas como alarmas, por ejemplo. Otro ejemplo de uso muy extendido es en el ámbito de los juegos, usadas para avisar al usuario de ciertas acciones del juego o simplemente para recordarle que tiene que volver a jugar...

Notificación local

Es importante recordar que no se debe de abusar de las notificaciones (tampoco de las notificaciones push), ya que esto provocaría el efecto opuesto que queremos al usuario. Para implementar las notificaciones locales en nuestras aplicaciones iOS tendremos que usar la clase UILocalNotification. Un ejemplo sencillo de su uso podría ser el siguiente:

    UIApplication* app = [UIApplication sharedApplication];
    
	UILocalNotification* alarm = [[UILocalNotification alloc] init];
    if (alarm)
    {
        alarm.fireDate = fechaNotificacion;
        alarm.timeZone = [NSTimeZone defaultTimeZone];
        alarm.soundName = @"alarmsound.caf";
        alarm.alertBody = @"Hora de despertarse!";
 
        [app scheduleLocalNotification:alarm];
    }    

En el código anterior creamos una instancia de la clase UILocalNotification y la configuramos a nuestro gusto para que se ejecute en una fecha determinada (objeto de la clase NSDate) según un calendario por defecto (el del propio dispositivo). También estamos indicando que cuando esta se ejecute active un sonido determinado (este sonido debe de estar en el bundle de nuestra aplicación) y muestre un mensaje determinado.

La notificación local aparecerá automáticamente en la fecha indicada. Si queremos cancelar alguna o todas las notificaciones que nuestra aplicación tiene programadas debemos de hacer uso del método cancelAllLocalNotifications de la clase UIApplication tal y como se puede ver en el código siguiente:

    UIApplication* app = [UIApplication sharedApplication];
    
    NSArray *notificacionesProgramadas = [app scheduledLocalNotifications];
 
    // Cancelo todas las notificaciones programadas
    if ([notificacionesProgramadas count] > 0)
        [app cancelAllLocalNotifications];    

Si se recibe una notifiación local cuando la aplicación está en un estado activo de foreground, se ejecutará el método application:didReceiveLocalNotification: de la clase AppDelegate.

Limitaciones y recomendaciones

La ejecución en background es extremadamente útil para muchos casos, incluso se puede decir que es muchas veces imprescindible. En la App Store existen en la actualidad una gran cantidad de aplicaciones que hacen uso de esta funcionalidad. Para su correcto funcionamiento Apple ha publicado una serie de recomendaciones que debemos de tener en cuenta a la hora de implementar estas funcionalidades:

  • No hacer llamadas OpenGL desde código. A tener en cuenta sobre todo en el desarrollo de juegos.
  • Cancelar cualquier servicio relacionado con Bonjour. Cerrar sockets abiertos, etc.
  • Implementar código para contemplar posibles fallos de conexión relacionados con internet en background.
  • Guardar el estado de la aplicación justo antes de entrar en modo background.
  • Eliminar referencias strong, retain antes de entrar en modo background. Especiamente imagenes que se mantienen en caché.
  • No se deben de usar servicios como la Agenda o calendarios en background.
  • No se deben de actualizar vistas ya que estas no se ven en modo background.
  • Responder a la conexión o desconexión de dispositivos externos en background, si nuestra aplicación los usa.
  • No mostrar ventanas de alerta (Alert View, Action Sheet) cuando la aplicación esté en background.
  • Eliminar de la vista información sensible (contraseñas, nombres de usuario, etc...) antes de entrar en background ya que el sistema realiza una captura del estado de la aplicación justo antes de entrar a background y toda esta información se almacenaría sin nuestro permiso en el dispositivo.
  • Hacer el mínimo trabajo de procesamiento mientras la aplicación se encuentre en background. Sólo realizar el trabajo imprescindible.

Teniendo en cuenta estas recomendaciones y haciendo un uso moderado del modo background se comportará bien nuestra aplicación y no se consumirán recursos innecesarios del sistema.