Manejo de Memoria en Objective-C y Cocoa

La semana pasada realizábamos en este mismo blog una sencilla introducción al lenguaje Objective-C, definiendo e implementando una clase denominada MyClass. Esta clase era instanciada desde una función main y se mostraba como ésta se instanciaría y como se le podrían enviar mensajes.

Desafortunadamente, este sencillo ejemplo hace un muy mal uso de memoria y termina haciendo un leak de la instancia creada. Esta semana estaremos expandiendo dicho ejemplo agregando código para manejar manualmente nuestra memoria y evitar este tipo de problemas.

Antes que nada, un elemento importante a destacar es que a partir de Objective-C 2.0, existen dos maneras de realizar manejo de memoria: la tradicional (presente en Objective-C 1.x) que consiste en manejarla “manualmente” mediante reference counting, o bien hacer uso del nuevo Garbage Collection opcional. Habilitar Garbage Collection en Objective-C es muy simple, únicamente debemos agregar el flag -fobjc-gc a la invocación del compilador y éste quedará habilitado, encargándose automáticamente de reclamar la memoria de los objetos que no son referenciados desde ningún otro objeto.

El problema de hacer uso de Garbage Collection (más allá de los costos en términos de uso de memoria) es que éste no se encuentra disponible en el iPhone. El resto de este artículo se estará basando en manejo de memoria mediante reference counting.

Reference Counting

La forma en que funciona reference counting en Cocoa (el conjunto de Frameworks de programación de Mac OS X y del iPhone) consiste en que cada objeto derivado de NSObject incluye un número entero que representa la cantidad de objetos que lo referencian. Esta cuenta se asigna en 1 cuando un nuevo objeto es creado (mediante la llamada al método alloc) y debe disminuirse a 0 (mediante el método release) para que éste sea eliminado. Un objeto puede incrementar el contador manualmente (mediante invocaciones al método retain), lo cual suele utilizarse para indicar interés en que dicho objeto no sea eliminado porque está en uso. Se dice que ahora el objeto creado es propiedad de ambos.

Cuando esto ocurre se debe determinar quién será el encargado de eliminar este objeto. En Cocoa se toma la convención de que el objeto, método o función que instancia a un objeto es también el encargado de liberar su memoria. En el contexto del ejemplo de la semana pasada deberíamos modificar la función main para que llame release sobre la instancia antes de terminar, como se muestra a continuación:

#import "MyClass.h"
int main()
{
   MyClass* myInstance = [[MyClass alloc] init];
   [myInstance initFromCoords:1 y:2];
   [myInstance printCoords];

   [myInstance release]; //Liberar recursos

   return 0;
}

Nótese que adicionalmente en este ejemplo se agregó la invocación del método init de myInstance. Embeber la llamada a alloc junto con init constituye otra convención de desarrollo de Cocoa, en este caso para la instanciación e inicialización de objetos. init sería el equivalente de invocar al constructor de la clase.

El Lado Oscuro

Con esto hemos visto un flujo muy común (y muy simple) para la creación y destrucción de objetos. Si bien a simple vista podría parecer sencillo apegarse a la convención de que “quien crea, destruye”, ésta tiene un lado oscuro.

El problema que surge en la práctica es el siguiente: supongamos que tenemos una función f() que ha de crear y devolver un objeto con datos. Si nos apegamos a la convención, f() debería ser quien libere el objeto, sin embargo, si llamamos a release antes de hacer el return, quien invocó a f() no tendrá la oportunidad de invocar a retain antes de que éste sea liberado y el programa cancelará con un error al intentar hacer uso de un objeto ya destruido.

La solución a este predicamento es hacer uso de las llamadas AutoreleasePools de objetos. La idea de las AutoreleasePools consiste en definir un ámbito (scope) tal que al salir de él todos los objetos que debemos liberar más tarde sean eliminados. Esta brillante idea soluciona el problema de la siguiente manera: se instancia un objeto de tipo AutoreleasePool. Cuando se cae en el problema de f(), en vez de enviarle un mensaje release, se envía un mensaje autorelease. Ésto decrementará el reference count más tarde, en particular cuando el objeto de tipo AutoreleasePool sea destruido, brindando suficiente lapso de vida a nuestro nuevo objeto para que quien lo desea a utilizar pueda invocar retain sobre él.

En términos concretos de código, estaríamos modificando nuestra función main de la siguiente manera:

#import "MyClass.h"
#import <Foundation/NSAutoreleasePool.h>

int main()
{
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

    MyClass* myInstance = [[MyClass alloc] init];
    [myInstance initFromCoords:1 y:2];
    [myInstance printCoords];

    [myInstance autorelease]; //Liberar más tarde
    [pool release]; //Liberar objetos pendientes

    return 0;
}

Si bien en el marco de este ejemplo, hacer uso de un AutoreleasePool no es estrictamente necesario, disponer de un AutoreleasePool en main es algo muy útil, ya que nos asegurará que todos los objetos a los cuales enviamos un mensaje de autorelease eventualmente sean destruidos.

Un comentario final que cabe destacar es que los AutoreleasePools pueden ser embebidos, definiendo nuevos ámbitos de autorelease, evitando así tener que mantener en memoria los objetos a los cuales se les envió un mensaje de autorelease hasta el final de la aplicación.

Esto es gran parte de lo que refiere el manejo de memoria en programas Objective-C y Cocoa. Pueden encontrar más información en la documentación oficial de Apple, aquí.

This entry was posted in Mac OS X, Objective-C, Programacion, Tutoriales. Bookmark the permalink.

6 Responses to Manejo de Memoria en Objective-C y Cocoa

  1. Pingback: Varrojo@Linux » Despedimos el 2009: Un saludos y varios stats

Comments are closed.