Más sobre Objective-C

Gestión de memoria

Hasta ahora hemos visto cómo reservar memoria con alloc pero no cómo liberarla. Esto es porque desde hace unas cuantas versiones de iOS la liberación de memoria es automática.

Lenguajes como Java usan recolección de basura para liberar automáticamente la memoria. El recolector de basura decide cómo actuar en tiempo de ejecución. Por contra, en iOS el compilador inserta automáticamente las instrucciones que liberan la memoria en el punto adecuado. El momento en que se libera la memoria está determinado en tiempo de compilación.

Cuenta de referencias

Para saber si un objeto puede ser destruido se usa una cuenta de referencias:

  • Cuando un objeto se crea con alloc la cuenta de referencias es 1.

  • Cuando se asigna a otra variable aumenta automáticamente la cuenta.

  • Otras operaciones, como poner a nil una variable que referenciaba al objeto, disminuyen la cuenta. También por ejemplo si esa variable se sale del ámbito.

Cuando la cuenta de referencias llega a 0, automáticamente se libera la memoria.

La discusión anterior supone que estamos usando cuenta de referencias automática (ARC) (es el valor por defecto en los proyectos creados con Xcode 5/6). En caso contrario el incremento/decremento de la cuenta debe hacerlo explícitamente el programador.

Ver Why mobile apps are slow para una discusión más detallada de por qué este mecanismo es mucho más eficiente que la recolección de basura en un entorno de memoria restringida, como los dispositivos móviles.

Propiedades strong vs. weak

El incremento automático en la cuenta de referencias cuando se asigna un objeto a una variable se da por defecto cuando se usa una variable local o una propiedad definida con @property. Se dice que son referencias de tipo strong.

La existencia de una referencia strong debería reflejar “propiedad” o “responsabilidad” de un objeto sobre otro.

Por desgracia el uso de referencias strong puede llevar a la creación de ciclos de objetos que ARC no puede liberar.

Para solucionar el problema de los ciclos se introducen las referencias de tipo weak. La memoria de un objeto puede ser liberada si solo tiene referencias de este tipo apuntando hacia él.

Las referencias weak reflejan “colaboración”, pero no “propiedad”. Por ejemplo, XCode usa weak por defecto al crear gráficamente un outlet, ya que la clase no suele ser la dueña de este elemento gráfico (lo será la vista “madre”).

Bloques

Los bloques permiten tratar un fragmento de código como un objeto, asignándolo a una variable, pasándolo como parámetro de un método, etc.

Definir y usar un bloque

Definimos un bloque con el símbolo ^, los argumentos y el código que lo compone entre llaves:

Aviso: el siguiente código dará un warning indicando que el bloque está siendo definido pero no usado

^(int n1, int n2) {
  NSLog(@"Suma: %d", n1+n2);
}

Los bloques también pueden devolver un valor. El tipo de retorno se especifica tras el símbolo ^, pero también podría omitirse

//También valdría ^(int n1, int n2)
^ int (int n1, int n2) {
  return n1+n2;
}
`

Si no hay argumentos se pueden omitir los paréntesis:

^{
  NSLog(@"Soy un bloque");
}

Si escribimos los ejemplos anteriores en XCode veremos que generan warnings. Es por la misma razón que lo daría la sentencia 3+2;: definimos algo pero no lo estamos usando

La versión complicada es cuando queremos definir y ejecutar nosotros mismos el bloque. Debemos seguir estos pasos: 1. Declarar la variable que va a contener el bloque 2. Definir el bloque en sí y asignarlo a dicha variable 3. Ejecutar el bloque

//declara “miBloque” como un bloque que no devuelve nada ni tiene parámetros
void(^miBloque)();
//define el bloque en sí
miBloque = ^{
   NSLog("@Soy un bloque");
};
//lo ejecuta
miBloque();
`

En muchos casos el uso será más sencillo al ser bloques anónimos, definidos sobre la marcha. Por ejemplo, algunos métodos de Cocoa requieren bloques en sus parámetros. El siguiente ejemplo de animación con animateWithDuration:animations:completion usa dos parámetros que son bloques de código:

  • En animations especificamos el estado final. iOS interpolará entre el estado actual y el deseado para hacer la animación

  • En completion ponemos un bloque de código a ejecutar cuando termine la animación

[UIView animateWithDuration:5.0
 animations:^{
 self.myLabel.center = CGPointMake(100, 100);
 }
 completion:^(BOOL finished){
 NSLog(@"Fin!");
 }
];
`

Clausuras

Los bloques de código capturan el valor de las variables que referencian, de modo que pueden usarlo aunque estas variables hayan desaparecido del ámbito largo tiempo antes de que se ejecute el bloque. Una variante del ejemplo anterior:

//Si esto es un view controller, la propiedad "view" referencia a la vista
// y el método "center" nos devuelve las coordenadas de su centro
CGPoint centro = [self.view center];
[UIView animateWithDuration:2.0
            delay:10
            options:UIViewAnimationOptionAutoreverse
                      |UIViewAnimationOptionRepeat
            animations:^{
                         self.myLabel.center = centro;
                     }
            completion:nil];

En el ejemplo anterior, establecemos un retardo de 10 segundos para comenzar la animación, lo que significa que cuando esta empieza, el método donde estuviera este código seguramente ya ha finalizado. Y aún así dentro del bloque ¡podemos hacer referencia a la variable centro que ya debería estar fuera de ámbito!. Esto ocurre porque los bloques actúan como una clausura, capturando el valor de las variables que referencian.

Que el valor de las variables esté capturado no significa que sigan existiendo. Dentro del bloque no podríamos asignarle un nuevo valor a centro.

Bloques y concurrencia

Los bloques nos dan la posibilidad de usar una sintaxis concisa y relativamente “limpia” para especificar código que se debe ejecutar de manera concurrente.

Aunque en iOS podríamos trabajar directamente con threads, es más sencillo usar abstracciones de un nivel más alto, como las colas de operaciones y la librería Grand Central Dispatch (GCD). Vamos a ver aquí la primera de estas tecnologías, aunque hay que puntualizar que las colas de operaciones usan GCD en su implementación.

Con las colas de operaciones podemos ejecutar trabajos de forma asíncrona. Si es necesario un orden de ejecución podemos especificar dependencias entre los trabajos.

Podemos crear una cola con el siguiente código:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

Podemos especificar un trabajo de diversos modos. Lo más directo es en un bloque con el código a ejecutar (clase NSBlockOperation). Para añadirlo a la cola antes creada haríamos:

[queue addOperation:^{
       NSLog(@"Yo soy una operación concurrente);
 }];

Podemos especificar dependencias entre trabajos, de modo que solo se comenzará con uno si han terminado los trabajos de los que se dependía.

NSBlockOperation *op_a = [NSBlockOperation
        blockOperationWithBlock:^{ NSLog(@"Soy la operación a");}];
NSBlockOperation *op_b = [NSBlockOperation
        blockOperationWithBlock:^{ NSLog(@"Soy la operación b");}];
[op_a addDependency:op_b];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op_a];
[queue addOperation:op_b];
`

En el ejemplo anterior, primero se realizará la operación “b” y luego la “a”, ya que esta depende de la anterior, aunque se hayan encolado en el orden contrario.

Como norma general se recomienda colocar el código que actualice el interfaz de usuario en el thread principal del programa, ya que aquí es donde se procesan por defecto los eventos y donde hace su trabajo la librería de UI (UIKit). Trabajando aquí nos evitamos problemas de concurrencia con el GUI. Podemos acceder a este thread con la cola de operaciones “principal”, accesible con el método de clase [NSOperationQueue mainQueue]

De este modo, si una de nuestras tareas tuviera que hacer cierto procesamiento de datos y luego actualizar el UI con los resultados, haríamos algo como:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//La operación de procesamiento va en otra cola
[queue addOperation:^{
      datos = procesar_datos();
      //Pero la de actualización del UI la hacemos en el hilo principal
     [[NSOperationQueue mainQueue] addOperation: ^{
           dibujar_datos(datos);
       }];
 }];

Key-Value coding

Para usar un getter o un setter convencional debemos saber en tiempo de compilación qué propiedad queremos mostrar o modificar, respectivamente. ¿Pero qué ocurre si la queremos determinar dinámicamente?. Por ejemplo, queremos mostrar todas las propiedades de un objeto pero no sabemos cómo se llaman ni cuántas hay.

Con KVC especificamos el nombre de la propiedad a obtener/fijar como un NSString y por tanto podemos calcularlo dinámicamente, en tiempo de ejecución.

NSString *nomPropiedad;
...
//Le damos el valor que sea a "nomPropiedad"
...
//Accedemos al valor de la propiedad con nombre "nomPropiedad"
id valor = [unObjeto valueForKey:nomPropiedad];
//La cambiamos
id nuevoValor = ...
[unObjeto setValue:nuevoValor forKey:nomPropiedad]

Ejemplo: mostrar todas las propiedades de un objeto

unsigned int num_props;
objc_property_t *props = class_copyPropertyList([UAPersona class], &num_props);
for(int i=0; i<num_props; i++) {
    NSString *nomPropiedad = [NSString
       stringWithUTF8String:property_getName(props[i])];
    NSLog(@"%@:%@", nomPropiedad, [unObjeto
          valueForKey:nomPropiedad]);
}
free(props);

Para que una clase sea “KVC-compliant” debe seguir las convenciones de nombre que ya hemos visto para el getter y el setter.

Cuidado, podemos usar KVC para obtener el valor de cualquier variable de instancia, aunque esté marcada como @private (o esté definida en la implementación). Además, automáticamente se asumirá que la variable de instancia se llama como la propiedad precedida del subrayado

Si aplicamos KVC sobre una colección es como si estuviéramos aplicándolo secuencialmente sobre cada uno de los elementos. Por ejemplo podemos obtener un array con todos los valores de determinada propiedad

NSArray *nombres = [lista valueForKey:@"nombres"];`

O Podemos cambiar determinada propiedad para todos los componentes de una colección

[lista setValue:@"100" forKey:@"credito"]

Extender clases sin usar herencia: categorías y extensiones

Algunas veces necesitamos “personalizar” una clase, ampliando su comportamiento. Para esto, en POO habitualmente se usa la herencia, pero no siempre es adecuada.

Categorías

Nos permiten añadir comportamiento a una clase ya existente sin modificar directamente su código ni usar herencia. Esto se conoce en programación como “monkey patching

Solo podemos añadir métodos, no propiedades ni variables de instancia.

Hay dos casos de uso típicos:

  • Uso 1: cuando no queremos o podemos modificar el código o la herencia no es adecuada o es imposible

    • Por ejemplo, queremos ampliar el comportamiento de la clase NSString No podemos modificarla ya que no tenemos los fuentes.

    • Incluso aunque los tuviéramos, si heredamos de `NSString```nos dejamos fuera por ejemploNSMutableString`. Y si heredamos de esta última estamos obligando a usar esta clase para tener la funcionalidad.

  • Uso 2: modularizar las funcionalidades de la clase dividiendo el archivo en varios

Vamos a ver un ejemplo: añadir una capitalización “alternating caps” a la clase NSString(EsTo Es AlTeRnAtInG CaPs)

  • Se define la categoría como si estuviéramos “creando de nuevo” la clase “original”. El nombre de la categoría se pone a continuación entre paréntesis.

    • Por convención, el fichero donde se define la categoría se nombra como nombre_clase_original+nombre_categoría

//Fichero “NSString+NSStringPlus.h”
import <Foundation/Foundation.h>

@interface NSString (NSStringPlus)
   -(NSString *)alternatingCaps;
@end


 //Fichero “NSString+NSStringPlus.m”
import "NSString+NSStringPlus.h"
import "ctype.h"

@implementation NSString (NSStringPlus)
-(NSString *)alternatingCaps {
int largo = self.length;
char copia[255];
//Convertimos a array de chars de C
[self getCString:copia
maxLength:255
encoding:NSUTF8StringEncoding];
//Manipulamos los caracteres
for (int i=0; i<largo; i++) {
    if (i%2==0)
        copia[i] = tolower(copia[i]);
    else
        copia[i] = toupper(copia[i]);
}
//Devolvemos el array convertido a NSString
return [NSString stringWithUTF8String:copia];
}
@end

Extensiones

Permiten ampliar o modificar el API interno de una clase. Se pueden añadir métodos, propiedades y variables de instancia que desde fuera no van a ser visibles.

Se usa una sintaxis similar a las categorías pero omitiendo nombre. La extensión se declara en el .m y si declara métodos estos se implementan en el bloque de @implementation.

Casos de uso típicos:

  • Uso 1: establecer un API interno “formal”, que pueda chequear el compilador

  • Uso 2: redefinir propiedades del API público por ejemplo para cambiar su accesibilidad (de solo lectura en público a escritura en privado)

En versiones antiguas del compilador había que declarar los métodos privados antes de poder usarlos, así que al parecer muchos desarrolladores usaban extensiones para esto, ya que de todas formas tenían que declarar estos métodos.

Gestión de errores

Objective-C tiene dos mecanismos estándar para la gestión de errores:

  • Excepciones

    • Manejo “estilo C” con NSError.

      Al contrario que en lenguajes como C++ y Java, las excepciones no deben usarse para errores “previsibles”, como por ejemplo un fallo de la conexión de red, sino para errores de programación, como por ejemplo salirse de un array. Para los primeros se debería usar NSError

Excepciones

  • Uso básico

@try {
// do something that might throw an exception
 }
  @catch (NSException *exception) {
  // deal with the exception
  }
  @finally {
  // optional block of clean-up code
  // executed whether or not an exception occurred
} 
  • Foundation tiene una clase NSException, que podemos usar como base de la herencia para nuestras propias excepciones

  • A diferencia de Java, se puede lanzar cualquier objeto (en Java solo se puede lanzar algo que herede de Throwable)

    @throw @"Han pasado cosas muy malas"

  • Aserciones: en cualquier punto del código podemos comprobar si se cumple una condición. Si no se cumple se generará automáticamente una NSInternalInconsistencyException.

Gestión “clásica”

En los lenguajes que no usan excepciones, normalmente es el programador el responsable de comprobar si se ha producido o no un error tras ejecutar una sentencia y seguir el curso de acción adecuado en los dos casos. Esto es lo habitual en C.

En Objective C se recomienda esta filosofía cuando se trate de errores “previsibles” (fichero no encontrado, conexión de red no disponible,…)

Foundation nos ofrece la clase NSError para representar un error. Contiene los siguientes campos:

  • Un NSInteger*, código del error. Se asume que los códigos son únicos en un “dominio” o campo de aplicación, pero pueden repetirse en global

    • Un NSString* con un mensaje de error

    • Un NSDictionary* con información adicional

Muchos métodos de Cocoa devuelven los errores por referencia, por lo que para obtener el error tendremos que pasar un puntero a NSError*

NSError *miError;
BOOL ok = [receivedData writeToURL:someLocalFileURL
  options:0
  error:&miError];
if (!ok) {
  NSLog(@"Se ha producido un error: %@", miError);
}

Ejercicios

Ejercicio de gestión de memoria

  • En los materiales de la sesión hay una plantilla con un ejemplo muy sencillo de fuga de memoria (memory leak).

  • En el modelo de datos de la aplicación hay dos clases, Usuario y Direccion. Un usuario tiene una dirección, y como en un futuro queremos poder buscar usuarios sabiendo su dirección, esta también “apunta” a su usuario. Nótese que estamos creando un ciclo de referencias, en el que cada uno apunta al otro.

  • Si ponemos en marcha la aplicación (que en realidad no hace nada útil :( ) podremos ver que en el viewDidLoad del ViewController se crean 1000 instancias de usuarios y direcciones, que al ser variables locales deberían liberarse automáticamente cuando termine el método (pero no va a ser así por la existencia de ciclos)

  • Podemos depurar las fugas de memoria con la aplicación Instruments. Para arrancarla, con el proyecto abierto en Xcode, seleccionar la opción de menú de Product > Profile.

  • Aparecerá una ventana con todas las plantillas de profiling que tiene Instruments. Seleccionar la de leaks (si no la encontramos podemos teclear el nombre en el cuadro de búsqueda de la parte superior derecha)

  • En la ventana de Instruments, pulsar sobre el botón que tiene un icono de grabar al estilo VCR. El programa va grabando cada cierto número de milisegundos el estado de la memoria y va detectando posibles fugas. Pasados unos segundos veremos que se detecta una fuga de memoria. Podemos hacer click sobre ella para ver los objetos que participan, e incluso ver un grafo con el ciclo que se ha producido

  • Necesitamos que un Usuario apunte a su dirección y viceversa, pero queremos evitar los ciclos de referencias fuertes.¿Cómo arreglarías el código para evitar las fugas de memoria?. Cuando lo hayas arreglado comprueba que la fuga ya no aparece en Instruments.

Cuando arregles el código asegúrate de que sales de Instruments y también del simulador iOS, antes de probar de nuevo el profiling. En caso contrario es posible que sigas ejecutando en Instruments la versión anterior de la aplicación.

Ejercicio de bloques y concurrencia

Creación del interfaz de la aplicación

  • Vamos a crear una pequeña aplicación en la que se pueda teclear una URL y podamos ver el contenido como si fuera un pequeño navegador. Lo primero es crear el interfaz, que debe tener 3 componentes:

    • Un campo de texto de 1 línea, para teclear la URL (text field)

    • Un botón pulsable que pondrá “ver web” (button)

    • Un campo de texto de varias líneas (text view) para mostrar el contenido web de la URL. Queremos que en este texto se puedan mostrar formatos (negritas, enlaces, ….) Para ello, con el campo seleccionado, en el panel de utilidades tenemos que pulsar sobre el icono del “attributes inspector” (el 4º). Y en la propiedad “text”, que por defecto marca “plain” elegir “attributed”.

  • El segundo paso es crear un action desde el botón a un método en ViewController.m, que responda a la pulsación en el botón. Podéis llamar al action como queráis, por ejemplo botonPulsado. Para simplificar, y aunque no sea muy elegante, dentro de este método insertaremos todo el código del ejercicio.

  • Para terminar el enlace entre el interfaz y nuestro código, debemos crear dos outlet:

    • Uno desde el campo de texto de 1 línea hasta una propiedad en ViewController.h. Podemos llamarla por ejemplo campo_url. Nos servirá para poder saber qué URL se ha tecleado

    • Otro desde el campo de texto de varias líneas hasta otra propiedad en ViewController.h. Podemos llamarla por ejemplo campo_texto. Nos servirá para poder mostrar el contenido de la URL.

Código concurrente, versión inicial

  • Queremos que la solicitud a la URL se efectúe en background para que aunque tarde demasiado el interfaz siga respondiendo. Esto lo podemos hacer a través del método de clase sendAsynchronousRequest:queue:completionHandler de la clase `NSURLConnection. Veamos cómo sería el código inicial de nuestro action botonPulsado. Copiadlo y pegadlo en vuestro proyecto y probad que funciona. Si tecleáis una dirección web (por ejemplo http://www.ua.es) en el log aparecerá directamente el HTML “en crudo”.

- (IBAction)botonPulsado:(id)sender {
    NSLog(@"Botón pulsado");
    //suponemos que el outlet del campo de la URL se llama "campo_url"
    NSURL *url = [NSURL URLWithString:self.campo_url.text];
    //Creamos una petición, pero todavía no la lanzamos
    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
    //Creamos una cola de tareas
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [NSURLConnection
     //Lanzamos la petición de forma asíncrona
     sendAsynchronousRequest:urlRequest
     //Usando la cola ya definida
     queue:queue
     //Y cuando acabe, ejecutamos este bloque
     completionHandler:^(NSURLResponse *response,
                         NSData *data,
                         NSError *error) {
         //Mostramos los datos en forma de texto.
         //Hace falta initWithData:encoding para pasar de NSData->NSString
        NSString *resultado = [[NSString alloc] initWithData:data
''                                encoding:NSUTF8StringEncoding];
         NSLog(@"%@", resultado);
      }];

Nótese que el método sendAsynchronousRequest se encarga de “empaquetar” la petición web en una “operación” y enviarla a la cola especificada, no lo hacemos nosotros directamente con addOperation

Mostrar los datos en el campo de texto de varias líneas

  • En lugar de mostrar los datos en el log, modificad el código para que se muestren en el campo de texto de varias líneas.

  • Para ello, lo primero es recordar que el código de actualización del interfaz de usuario debe hacerse en la cola principal. Así que dentro del bloque “completionHandler” tenéis que

    • obtener el acceso a la cola principal

    • añadir a la cola principal un bloque que simplemente asigne la variable “resultado” al atributo “text” de nuestro outlet “campo_texto”_.

  • Comprobad que funciona. Quedará, eso sí, un poco “raro” ya que se verá el HTML “en crudo”.

  • Se puede mejorar visualmente el resultado convirtiendo el HTML en texto con formato y asignando ese texto formateado al atributo “attributedText” del outlet “campo_texto”. Para convertir el HTML en texto con formato hay que usar el siguiente código:

    NSError *error;
    NSDictionary *options = @{NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType};
    NSAttributedString *texto_formato = [[NSAttributedString alloc]
          initWithData:data                                                     options:options
          documentAttributes:nil
          error:&error];

MUY IMPORTANTE: aunque no está actualizando directamente la interfaz de usuario el código anterior hay que ejecutarlo en la cola principal, ya que los APIs que utiliza internamente así lo exigen. En caso contrario generará un error en tiempo de ejecución.

Gestión de errores

  • Modificar el código para que en caso de haberse producido un error al hacer la petición web, aparezca el texto de dicho error en el campo de texto de varias líneas.

Last updated