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. Bruno says:

    La verdad que me parece demasiado complicado, si haces una aplicacion en Objective-C vas pasar mas tiempo en el tema de manejo de memoria que en el dominio del problema. No conosco Objective-C pero pense que era mas facil el manejo de memoria. Me asombra que Objective-C NO tenga Garbage Collector automatico, y que el Garbage Collector sea una Reference Counting. En la mayoria de los Smalltalk el GC es un Generational, es extraño que no tengan solucionado un problema que esta solucionado desde los años 80 en Smalltalk, de todas formas como NO conosco en detalle a Objective-C no puedo hablar mucho…

  2. varrojo says:

    Hola Bruno,

    Veo que tu posees mucha experiencia en Smalltalk, uno de los lenguajes padre de Objective-C. El asunto no es que Objective-C no tenga Garbage Collection, sino que más bien, el uso del mismo es opcional.

    A partir de Objective-C 2.0, quienes desarrollan para Mac OS X pueden hacer uso del mismo, olvidándose del manejo de memoria.

    El problema del Garbage Collection es que se trata de una operación muy intensiva en memoria y por lo tanto su uso no es algo común en ambientes embebidos. Es por esto que cuando desarrollas para el iPhone debes optar por el mecanismo de Reference Counting.

    Saludos!

  3. Bruno says:

    Ahora que me decis lo de los dispositivos embebidos tiene mas sentido. Lo que tienen algunos Smalltalk (Visual Works y creo que VA Smalltalk) es que podes darle muy baja prioridad al GC, por lo que no te afectara la performance de tu aplicacion, la prioridad del GC es seteable.
    Podrias hacer algo asi:
    [MemoryManager current collectGarbage] forkAt: 4
    “esto es codigo de Dolphin Smalltalk, que es el que uso yo”
    Lo que hace esto es invoar al GC en un hilo separado y con prioridad 4 que es bastante baja, 10 es la mayor. Podes hacer que el hilo sea un “native thread” o “green thread”

    No conosco el ambiente COCOA pero quizas puedas armarte un test genericos para controlar donde puede haber memory leaks, lo tiro por arriba porque desconosco en detalle el ambiente de desarrollo.
    Te mencionaba lo del GC pq te puede consumir mucho tiempo el manejo de memoria (si no lo tenes bien organizado el tema), y en lugar de estar en el dominio del problema podes estar con el manejo de memoria.
    De todas formas con lo de IPhone esta bastante tentador, tengo un conocido que se esta por animar con Objective-C.

    Saludos,
    Bruno

  4. varrojo says:

    Que interesante, no sabía que la notación de corchetes era heredada de Smalltalk. Me da curiosidad por aprender sobre ese lenguaje también 😉

    Un saludo.

  5. Bruno says:

    Si pones codigo entre [] estas instanciado un objeto de la clase BlockClosure.
    Se usan ampliamente con las colecciones y para manejar excepciones.
    (cuando pasa un BlockClosure como parametro estas pasando codigo como parametro)

    tablaDel8 := [:value | value * 8]
    tablaDel8 value: 5 “hace 5 * 8”
    tablaDel8 value: 9 “hace 9 * 8”

    Si queres ordenar colecciones tambien podes usar Blocks:

    colleccionDeCuentas asSortedCollection: [:a :b | a numero <= b numero].
    "ordena por numero"

    colleccionDeCuentas asSortedCollection: [:a :b | a saldo <= b saldo]
    "orden por saldo"

    Con las excepciones se usan asi:
    Banco
    obtenerCuentaNumero: anInteger sinoExiste: aBlock

    ^cuentas detect: [:each | each numero = anInteger] ifNone: aBlock

    Itera sobre y devuelve la cuenta qe cumple (numero = anInteger),
    si no existe esta cuenta –> evalua aBlock.

    unBanco obtenerCuentaNumero: 12586 sinoExiste: [MessageBox notify: ‘No Existe’].
    “o se puede llamar:”
    unBanco obtenerCuentaNumero: 12586 sinoExiste: [‘No Existe’].
    “o se puede llamar:”
    unBanco obtenerCuentaNumero: 12586 sinoExiste: [CuentaNoExisteException signal].

    Podes usarlos a gusto…

    Perdon por el post tan largo

    Saludos,
    Bruno

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

Comments are closed.