lunes, 23 de julio de 2012

Automatic Reference Counting en Objective-C

Cuando uno empieza a programar en Objective-C, una de las mayores dificultades que se encuentra es con el manejo de memoria.

Objective-C utiliza una técnica que se llama "reference counting", que resuelve el manejo de memoria en tiempo de compilación (a diferencia de "garbage collection"), pero que hasta la aparición de iOS 5 y OS X 10.7, lo dejaba en manos del programador.

Cómo funciona el "reference counting"

Básicamente, la técnica de "reference counting" funciona así:
  • cuando quiero quedarme con una referencia a un objeto tengo que mandarle un retain, esto incrementa el retainCount en uno
  • cuando ya no lo necesito más le mando un release, esto  decremente el retainCount también en uno
  • si no quiero quedarme con una referencia pero tampoco quiero que el objeto se libere inmediatamente, puedo mandarle un autorelease que hace un release pero en el futuro(1)
  • cuando el objeto queda con retainCount en cero, el mismo objeto se encarga de llamar a su propio método dealloc que libera la memoria.
(1)La explicación de como funciona el autorelease puede ser un poco compleja si no se conoce el mecanismo. El autorelease se ejecuta en el contexto de un "autorelease pool" que mantiene una referencia a cada uno de los objetos que se les envió el mensaje, y cuando se libera manda un release por cada autorelease que hice.

Además en Objective-C se usan convenciones de nombres para saber si un método devuelve un objeto con o sin retain. Por ejemplo, si el nombre del método empieza con "new", ya viene con el retain.

Esto si se usa bien funciona, pero puede generar dos tipos de errores:
  • memory leaks: a un objeto se le mandan más retain que release, por lo que la memoria nunca se libera
  • zombies: el objeto se usa luego de quedar con retainCount en cero, y por lo tanto se accede a memoria que contiene basura, ya que el objeto fue liberado al llegar a cero.
Automatic Reference Counting

La solución de Apple a partir de iOS 5 y OS X 10.7 es el Automatic Reference Counting (ARC).

Con ARC, se mantiene el mismo esquema de manejo de memoria con "reference counting", solo que no lo hace el programador, sino que lo hace el compilador de forma automática.

Esta técnica tiene ventajas sobre el "garbage collector" usado por otros lenguajes, ya que no es necesario contar con un proceso que recorra la memoria buscando objetos en desuso, que puede (y de hecho lo hace) causar problemas de performance en tiempo de ejecución.

Por lo tanto, con ARC, se tiene lo mejor de dos mundos: el manejo de memoria se hace en tiempo de compilación, y no depende del programador el que se haga bien.

Sobre la "migración"

Para los que venimos programando en Objective-C, al principio ver código escrito para ARC puede resultar un poco chocante. Algunos ejemplos:

Cuando se hace un alloc, siempre corresponde hacer un release en algún momento, y el no verlo cuando se tiene ARC hace pensar que el programa está dejando memory leaks.

El método dealloc con ARC no es muy usado, porque el objeto no tiene que liberar memoria, pero a veces se necesita por ejemplo para dejar de recibir notificaciones. En ese caso, se implementa el método dealloc, pero que no llama al [super dealloc], por lo que a primera vista da toda la impresión de estar mal.

Referencia a objetos ya liberados

Otro problema bastante común, es mantener una referencia a un objeto que ya se liberó.

Por ejemplo, cuando se tiene una referencia de tipo weak (o assign en nomenclatura pre-ARC), si el objeto referenciado quedaba con retainCount en cero, se liberaba y quedaba la referencia apuntando a memoria basura.

Con ARC en iOS 5, estas referencias se borran automáticamente al liberarse el objeto, y quedan con valor nil.

Lamentablemente si la aplicación debe funcionar con iOS 4, si bien se puede usar ARC, las referencias no pueden ser de tipo weak y no quedan con nil luego de liberarse el objeto. Se deben declarar como assign o unsafe_unretained, y se deben seguir manejando igual que antes.

Conclusión

ARC es bastante mejor a lo que teníamos: el manejo de memoria de forma manual, aunque puede costar un poco acostumbrarse.

Además se hicieron mejoras de performance, que junto con las optimizaciones que realiza el compilador, deberían mejorar el rendimiento y el uso de memoria de las aplicaciones.

En GeneXus ya estamos pasando los proyectos a ARC, y el Upgrade 2 de la Evolution 2 será compilado bajo este esquema.