Swift – Gestión de Memoria

Hoy aprenderemos sobre gestión de memoria, es decir sobre ARC (Contador Automático de Referencia). Otro tema de esos que son pilares dentro del desarrollo Swift / iOS.

La gestión de memoria en Swift es relativamente automática. Sí, aunque la mayoría de los problemas relacionados con la memoria son gestionados automáticamente. Swift no usa un garbage collector (Recolector de Basura), en lugar de esto aboga por un sistema de conteo de referencias.

Es decir, Swift utiliza el sistema ARC (Automatic Reference Counting).

Asignación y liberación de memoria

La asignación y el manejo de memoria en los tipos por valor y por referencia en el lenguaje Swift no se lleva a acabo de la misma manera. En el caso de los tipos por valor es bien sencilla: se reserva un segmento de memoria para cada instancia, cuando la almacenamos en una propiedad o incluso cuando la pasamos a una función se crea una copia de la misma.

Memoria que es liberada una vez terminada la ejecución de la función o el ámbito donde se encuentra la propiedad  ha finalizado, así de manera automática sin que nosotros tengamos que hacer algo al respecto.

En este artículo nos enfocaremos en las clases cuya asignación funciona similar a la de los tipos por valor, pero que a diferencia de estos últimos cuando pasamos nuestra instancia a una función (o la almacenamos en una propiedad) no se crea una copia de la instancia. En lugar de esto, se genera una referencia al espacio de memoria que fue asignado para la instancia de la clase.

Es decir que la instancia de una clase en sí, representa un apuntador a su espacio de memoria y cuando suceden los eventos antes comentados se crea una referencia adicional que apunta al mismo espacio de memoria. En este punto podemos encontrarnos ante  la clásica duda:

¿Qué sucedería si una de las referencias modifica una propiedad miembro de la clase?

Pues este cambio sería común para el resto de referencias que apuntan al mismo segmento de memoria.

Como podemos constatar este comportamiento es bastante similar a lo que vemos en otros lenguajes pero que a diferencia de estos Swift no requiere que nosotros gestionemos la memoria de manera manual, al mismo tiempo que tampoco contamos con un garbage collector cuidando de estos detalles en tiempo de ejecución.

Conteo automático de referencias

Los creadores de Swift idearon un sistema que se implementa en tiempo de compilación y donde a cada instancia de clase se le asigna un contador de referencias, el cual representa el número de referencias existentes que apuntan al espacio de memoria de la instancia.

Dado que las referencias generan dependencia sobre nuestra instancia, los recursos de la misma no serán liberados hasta que el contador de referencia no sea igual a cero, momento en el que el método deinit es ejecutado.

Todo esto comenzó hace un tiempo atrás no muy lejano, donde las aplicaciones en Objective-C también usaban el conteo de referencia pero a diferencia del actual este era manual, es decir que este sistema requería (como su nombre indica) de una gestión manual sobre el conteo de referencias.

Cada clase tenía un método llamado retain que se encargaba de reclamar y conservar la propiedad sobre el objeto al mismo tiempo que incrementaba el contador de referencias en uno. En conjunto con el método retain también estaba otro de nombre release cuya función era la inversa: liberaba o renunciaba a su propiedad sobre la instancia disminuyendo el contador de referencias en uno.

Como podemos imaginar el conteo de referencias manual trae consigo muchos errores, ya sea si hacemos retain demasiadas veces sobre una instancia y luego no podemos desasignar esa memoria se generaba un memory leak (Fuga de Memoria) y así también nos encontrábamos errores cuando ejecutando release sobre un segmento de memoria que ya había sido liberado y quizás hasta reasignado, etc.

Debido a lo anteriormente comentado y como parte también de la evolución natural de la tecnología, Apple en el año 2011 lanza ARC para Objective-C. Luego de su implementación el compilador pasaba a ser el encargado de analizar el código y de insertar (como ya hemos comentado) las llamadas a retain y a release en todos los lugares apropiados.

Igual sucede con Swift que ha sido construido de la mano con ARC, no tenemos que gestionar la asignación o liberación de la memoria de manera manual.

Aun así es muy bueno entender como este sistema trabaja y de hecho en pos de evitar problemas con la gestión de memoria, sí pues aunque ARC es bastante inteligente hay situaciones en las que ciertos enfoques de diseño pueden generar problemas relativamente importantes.

Referencias strong

Hablemos de strong reference cycles pero antes crearemos una aplicación de consola donde probaremos los ejemplos siguientes:

class Person: CustomStringConvertible {
    
    let name: String
    
    var description: String {
        
        return name
        
    } // description
    
    init (name: String) {
        
        self.name = name
        
    } // init
    
    deinit {
        
        print("\nLa memoria ocupada por el objeto \(self) está siendo liberada.")
        
    } // deinit

} // Person

Aquí tenemos una clase de nombre Person en representación de una persona, cuenta con una propiedad llamada name donde almacenaremos el nombre de la persona.

Esta clase implementa el protocolo CustomStringConvertible y como tal se define la propiedad computada description. El bloque init inicializa la propiedad name y el último bloque de nombre deinit se ejecuta cuando la instancia es destruida.

Ahora, si aplicamos lo que hemos comentado anteriormente pudiéramos decir que el bloque deinit se ejecuta cuando el contador de referencias de la instancia ha llegado a cero.

Probemos esto añadiendo el siguiente código al final del anterior:

var Gochi: Person? = Person(name: "Gochi")

print("\nObjeto \(Gochi!) creado.")

Gochi = nil

print("\nEL objeto Gochi ahora es: \(Gochi).")

print()

la salida en pantalla sería:

Objeto Gochi creado.

La memoria ocupada por el objeto Gochi está siendo liberada.

EL objeto Gochi ahora es: nil.

Primero hemos creado una instancia de Person, luego la igualamos a nil y vemos en la salida como acto seguido el bloque deinit se ha ejecutado, luego al intentar imprimir la descripción de la instancia obtenemos que esta es nil, es decir que ya no existe.

La variable Gochi es una instancia opcional de la clase Person y por defecto todas las referencias que creamos son referencias fuertes o como me gusta más llamarle strong references. Esto significa que el contador de referencias se incrementa en uno automáticamente al crear la instancia Gochi.

Veamos ahora otra clase:

class Asset: CustomStringConvertible {
    
    let name: String
    let value: Double
    var owner: Person?
    
    var description: String {
        
        if let actualOwner = owner {
            
            return "Asset (\(name), valor \(value), propiedad de \(actualOwner))"
            
        } else {
            
            return "Asset (\(name), valor \(value), no es propiedad de nadie)"
            
        } // else
        
    } // description
    
    init(name: String, value: Double) {
        
        self.name = name
        self.value = value
        
    } // init
    
    deinit {
    
        print("\nLa memoria ocupada por el objeto \(self) está siendo liberada.")
        
    } // deinit
    
} // Asset

La clase Asset es muy similar a la anterior, la diferencia principal sería la línea 5 donde tenemos una propiedad opcional de tipo Person. Veamos en este punto que sucede si hacemos uso de esta clase en conjunto con el ejemplo anterior y sin modificar mucho:

var laptop: Asset? = Asset(name: "Macbook Pro", value: 2500)
var hat: Asset? = Asset(name: "Cowboy Hat", value: 150)
var backpack: Asset? = Asset(name: "Black Backpack", value: 30)

...

laptop = nil
hat = nil
backpack = nil

Luego de hacer esto tendríamos la salida en pantalla:

La memoria ocupada por el objeto Asset (Macbook Pro, valor 2500.0, no tiene propietario) está siendo liberada.

La memoria ocupada por el objeto Asset (Cowboy Hat, valor 150.0, no tiene propietario) está siendo liberada. La memoria ocupada por el objeto Asset (Black Backpack, valor 30.0, no tiene propietario) está siendo liberada.

Hasta aquí nada nuevo, pero aún no hemos usado la instancia opcional de Person que forma parte de la clase Asset, pero si analizamos un poco nos daremos cuenta que nuestros assets pueden tener dueño pero aún las personas no son conscientes de sus posesiones.

Así que necesitamos implementar un mecanismo en Person que nos permita vincularnos con los assets que hemos comprado y al mismo tiempo que estos guarden un registro de sus propietarios. Modifiquemos la clase Person para que luzca como la siguiente:

class Person: CustomStringConvertible {
    
    let name: String
    var assets = [Asset]()
    
    var description: String {
        
        return name
        
    } // description
    
    init (name: String) {
        
        self.name = name
        
    } // init
    
    deinit {
        
        print("\nLa memoria ocupada por el objeto \(self) está siendo liberada.")
        
    } // deinit
    
    func takeOwnershipOfAsset(asset: Asset) {
        
        asset.owner = self
        assets.append(asset)
        
    } // takeOwnershipOfAsset

} // Person

En la línea 4 hemos añadido un arreglo de tipo Asset que almacenará las propiedades de la persona en cuestión. También hemos añadido de la línea 24 a la 29 una nueva función que se encarga de hacernos tomar posesión sobre cierto asset. Tal y como podemos ver en las siguientes líneas:

gochi?.takeOwnershipOfAsset(asset: laptop!)
gochi?.takeOwnershipOfAsset(asset: hat!)

Ahora, cuando unimos todos estos cambios:

import Foundation

class Person: CustomStringConvertible {
    
    let name: String
    var assets = [Asset]()
    
    var description: String {
        
        return name
        
    } // description
    
    init (name: String) {
        
        self.name = name
        
    } // init
    
    deinit {
        
        print("\nLa memoria ocupada por el objeto \(self) está siendo liberada.")
        
    } // deinit
    
    func takeOwnershipOfAsset(asset: Asset) {
        
        asset.owner = self
        assets.append(asset)
        
    } // takeOwnershipOfAsset

} // Person

var gochi: Person? = Person(name: "Gochi")

print("\nObjeto \(gochi!) creado.")

var laptop: Asset? = Asset(name: "Macbook Pro", value: 2500)
var hat: Asset? = Asset(name: "Cowboy Hat", value: 150)
var backpack: Asset? = Asset(name: "Black Backpack", value: 30)

gochi?.takeOwnershipOfAsset(asset: laptop!)
gochi?.takeOwnershipOfAsset(asset: hat!)

gochi = nil

print("\nEL objeto Gochi ahora es: \(gochi).")

laptop = nil
hat = nil
backpack = nil

print()

la salida en pantalla es la siguiente:

Objeto Gochi creado.

EL objeto Gochi ahora es: nil.

La memoria ocupada por el objeto Asset (Black Backpack, valor 30.0, no tiene propietario) está siendo liberada.

Sorpresa! cierto?

Solamente se ha destruido la instancia de la clase Asset correspondiente a la mochila negra (black backpack) la cual fue el único asset que no se le adjudicó a la instancia de Person y al mismo tiempo nos percatamos que la instancia gochi tampoco ha sido liberada luego de haberse igualado a nil.

¿Que sucedió?

Para entenderlo comencemos por analizar la siguiente imagen:

Referencias strong

en esta podemos ver el estado donde nos encontramos en la línea 49, es decir, este sería el estado antes de aplicar nil a las instancias de Asset tal y como sucede en las líneas 50, 51 y 52.

De izquierda a derecha comenzamos por la variable opcional gochi de tipo Gochi la cual como instancia constituye una referencia a su propio espacio de memoria. Luego de las líneas 47 a la 49 creamos tres instancias de Asset las cuales están representadas a la derecha del diagrama.

En las líneas 51 y 52 establecemos a gochi como propietario de los assets Macbook Pro y Cowboy Hat mediante el método takeOwnershipOfAsset, en este igualamos / copiamos la propia instancia (self (en este caso de gochi)) hacia la propiedad de nombre owner, miembro de Asset y evidentemente de tipo Person:

asset.owner = self

En este punto hemos creado una referencia desde la instancia de Asset hacia la de Person, luego en la siguiente:

assets.append(asset)

añadimos el asset en cuestión al arreglo de assets (una persona puede poseer más de un objeto) declarado en la línea 6 de la clase Person y con esta acción creamos una referencia desde la instancia gochi hacia las instancias de laptop y hat.

Me gustaría aclarar que en el gráfico anterior las flechas entrantes son las referencias que generan dependencia y efectivamente las que incrementan el contador de referencias.

Cuando digo dependencia me refiero a que una instancia determinada apunta hacia nosotros, necesita de nosotros para su funcionamiento por lo que si dejamos de existir esta se quedaría apuntando a un espacio de memoria que ya no contiene información válida y con esto vendría consigo los respectivos errores.

Esta es la razón por la que existe el contador de referencias, en la imagen podemos observar que el espacio de memoria correspondiente a la instancia de Person está siendo usada por dos instancias de Asset algo que impide que la ejecución de la instrucción de la línea 46 tenga efecto.

Así que pudiéramos pensar que eliminando (igualándolas a nil) las instancias de Asset sería suficiente, ¿verdad?

Pues no, ya que al mismo tiempo desde Person (el arreglo assets) tenemos otras dos referencias hacia las instancias de Asset y esto impide que la memoria de estas pueda ser liberada, no hay una salida aparente, es un problema cíclico del cual no podemos salir y de ahí su nombre: Ciclos de Referencia Fuerte (Strong Reference Cycles).

Los Ciclos de Referenca Fuerte hay que evitarlos a toda costa ya que aparte que representan un error de diseño (no necesariamente lógico) también constituyen un tipo de fuga de memoria. En nuestro ejemplo hemos asignado la memoria necesaria para las instancias de Person y Asset pero nuestro programa jamás retornó esa memoria al sistema operativo.

Referencias weak

Para todo casi siempre hay una solución aún cuando es desconocida, en este caso la solución es romper el ciclo y para esto contamos con la palabra clave weak que significa débil en inglés. Creo que dicho esto ya deben de estar suponiendo a que se debe su nombre.

Una referencia débil (weak) al contrario de la fuerte (strong) no incrementa el contador de referencias de la instancia a la cual apunta.

Hagamos lo siguiente: declaremos como weak a la propiedad llamada owner en la clase Asset (línea 5):

weak var owner: Person?

Cuando hacemos esto y ejecutamos la función takeOwnershipOfAsset, el comportamiento que logramos es que el contador de referencias de la instancia gochi no se incremente en uno. La única referencia que tendría Person sería su propia instancia que al ser igualada a nil dejaría de existir.

Es decir que luego de esta modificación si ejecutamos nuevamente el programa la salida en pantalla sería:

Objeto Gochi creado.

La memoria ocupada por el objeto Gochi está siendo liberada.

EL objeto Gochi ahora es: nil.

La memoria ocupada por el objeto Asset (Macbook Pro, valor 2500.0, no tiene propietario) está siendo liberada.

La memoria ocupada por el objeto Asset (Cowboy Hat, valor 150.0, no tiene propietario) está siendo liberada.

La memoria ocupada por el objeto Asset (Black Backpack, valor 30.0, no tiene propietario) está siendo liberada.

En esta podemos corroborar que los ciclos de referencia fuerte se han roto. Algo importante a comentar sería que en el caso de no haber igualado a nil las instancias de Asset, la propiedad owner luego de desaparecer la referencia destino (Gochi) sería igual a nil.

Por este motivo es que en Swift las propiedades marcadas como weak tienen que ser variables y opcionales, no pueden ser constantes dado su posible cambio a nil y opcionales pues para que puedan representar el estado mismo de nil.

El siguiente diagrama resume lo antes expuesto, muestra como se encuentran las referencias luego de aplicar weak a la propiedad owner de la clase Asset:

Referencias weak

Las líneas discontinuas representan las referencias weak, al mismo tiempo en la clase Person observamos que solamente cuenta con una referencia, la de su propia instancia. Tal y como explicamos las referencias weak no incrementan el contador de referencias, son referencias débiles que pueden romperse en cualquier momento y por ende no generan dependencia.

Diseño de clases teniendo en cuenta la gestión de memoria

Objetos como un sombrero, una mochila y una laptop pueden existir sin dueño, aún olvidadas o botadas seguirán ahí en algún rincón del basurero. Teniendo esto en cuenta tiene mucho sentido que la referencia hacia la clase Person sea weak, mientras que la de Person  hacia Asset sea strong.

La persona es la que decide hasta cuando les serán útiles estas pertenencias y llegado el momento será el único (como propietario) que podrá romper esos lazos de propiedad. Es decir que si ejecutamos algo como:

laptop = nil

sin antes hacer un:

gochi = nil

La memoria de la instancia laptop no se liberaría ya que la instancia gochi tiene una referencia fuerte hacia esta, es decir que laptop depende de gochi ya que lógicamente ella sola (no tiene conciencia) no puede determinar que ya no tiene dueño.

Dependencias obligatorias y unidireccionales

Una persona es dueño de una tarjeta de crédito pero a su vez la tarjeta de crédito no puede existir sin un dueño. Analizando rápidamente esto generaría una configuración donde la tarjeta de crédito tendría una referencia fuerte hacia Person ya que esta sin un propietario no existiría.

Al mismo tiempo una persona puede no tener una tarjeta de crédito, por lo que este campo sería variable y opcional, una referencia weak. Veámoslo en el siguiente diagrama:

Relacion incorrecta - Person y Creditcard

Este enfoque es incorrecto ya que parte del análisis donde una persona puede o no tener una tarjeta de crédito y esto es cierto, el problema reside en la lógica de que una tarjeta no puede poseer a una persona (aunque algunos bancos así lo quisieran).

Una persona puede existir sin tarjeta de crédito por lo que la dependencia generada con esta configuración, por más que pueda ser posible, no es para nada óptima.

Otro detalle a tener en cuenta sería que Person tiene dos referencias mientras que CreditCard solamente una, así que para que Person deje de tener una tarjeta de crédito esta tendría que (mágicamente (no tiene conciencia)) romper su lazo de propiedad y no al contrario como debería de ser.

Referencias unowned

En este punto llegan las referencias unowned las cuales nos permiten lograr el mismo comportamiento que las weak pero a diferencia de estas pueden ser constantes y por ende no opcionales. Estas referencias vienen a ayudarnos en ocasiones bien determinadas, siempre con la premisa de que su valor jamás mutará a nil y esto es un beneficio que va a depender bastante de nuestro diseño, de lo que estemos intentando lograr.

Por ejemplo si usamos una referencia unowned en la solución que estamos implementando entre las clases Person y CreditCard, nos ayudaría a que la dependencia recayera sobre la tarjeta de crédito en lugar de la persona.

En el caso de la tarjeta de crédito tendríamos la seguridad de que su propiedad miembro owner de tipo Persona jamás será nil, ya que una tarjeta no puede existir sin dueño, mientras que en el caso de gochi su referencia a la tarjeta será fuerte pero opcional. El código quedaría de la siguiente forma:

class CreditCard: CustomStringConvertible {
    
    unowned let owner: Person
    
    init(owner: Person) {
        
        self.owner = owner
        
    } // init
    
    deinit {
        
        print("\nLa memoria ocupada por el objeto \(self) está siendo liberada.")
        
    } // deinit
    
    var description: String {
        
        return "CreditCard"

    } // description
    
} // CreditCard

En le caso de Person:

class Person: CustomStringConvertible {
    
    let name: String
    var assets = [Asset]()
    var creditCards = [CreditCard?]()

    init (name: String) {
        
        self.name = name
        
    } // init
    
    deinit {
        
        print("\nLa memoria ocupada por el objeto \(self) está siendo liberada.")
        
    } // deinit
    
    var description: String {
        
        return name
        
    } // description
    
    func takeOwnershipOfAsset(asset: Asset) {
        
        asset.owner = self
        assets.append(asset)
        
    } // takeOwnershipOfAsset
    
    func takeOwnershipOfCreditCard(myCreditCard: CreditCard) {

        creditCards.append(myCreditCard)
        
    } // takeOwnershipOfAsset

} // Person

Luego de esto creamos las instancias y las respectivas asignaciones:

var masterCard: CreditCard? = CreditCard(owner: gochi!)

gochi?.takeOwnershipOfCreditCard(myCreditCard: masterCard!)

¿Qué sucedería si en este punto establecemos el siguiente orden de instrucciones?

...

masterCard = nil

gochi = nil

...

La salida en pantalla sería:

Objeto Gochi creado.

La memoria ocupada por el objeto Gochi está siendo liberada.

La memoria ocupada por el objeto CreditCard está siendo liberada.

EL objeto Gochi ahora es: nil.

La memoria ocupada por el objeto Asset (Macbook Pro, valor 2500.0, no tiene propietario) está siendo liberada.

La memoria ocupada por el objeto Asset (Cowboy Hat, valor 150.0, no tiene propietario) está siendo liberada.

La memoria ocupada por el objeto Asset (Black Backpack, valor 30.0, no tiene propietario) está siendo liberada.

Lo que sucede es que la instancia masterCard como depende de la instancia gochi mediante la referencia fuerte que existe entre las clases Person y CreditCard.

Aún cuando al objeto de tipo CreditCard se igualó a nil antes que a Person, el segmento de memoria que corresponde a masterCard es retenido hasta que la instancia gochi es liberada. Tal y como se muestra en el orden de las líneas 3 y 5 de la salida en pantalla.

El diagrama de esta nueva configuración sería el siguiente:

Relacion correcta - Person y Creditcard

Antes de finalizar os dejo una pequeña tabla donde podemos consultar las características de las las referencias que hemos visto en este artículo:

ReferenciaVarLetOpcionalNo Opcional
Strong
WeakNoNo
UnownedNo

En este punto creo que es más que evidente la importancia de una buena gestión de la memoria, entender el funcionamiento de ARC, así como dominar los distintos tipos de referencias con los que contamos en Swift.

Sin dudas este conocimiento es fundamental en nuestra formación como iOS Developer, nos ayudará a implementar un mejor código y con esto lograr un producto final de muy alta calidad.

Ciclos de referencia en closures

En un artículo anterior donde profundizamos sobre los Closures, aprendimos que los closures son tipos por referencia. Al recordar esto nos puede surgir la siguiente pregunta:

¿Si un closure es un tipo por referencia, pudiera entonces generar un ciclo de retención?

La respuesta es sí, pueden crear un ciclo de retención o lo que es lo mismo un ciclo de referencia fuerte al igual que las clases.

Continuamos con nuestro proyecto y vamos a la clase de nombre Accountant:

import Foundation

class Accountant: CustomStringConvertible {
    
    typealias NetWorthChanged = (Double) -> ()
    
    var netWorthChangedHandler: NetWorthChanged? = nil
    
    var netWorth: Double = 0.0 {
        
        didSet {
            
            netWorthChangedHandler?(netWorth)
            
        } // didSet
        
    } // netWorth
    
    deinit {
        
        print("\nLa memoria ocupada por el objeto \(self) está siendo liberada.")
        
    } // deinit
    
    var description: String {
        
        return "Accountant"
        
    } // description
    
    func gainedNewAsset(asset: Asset) {
        
        netWorth += asset.value
        
    } // gainedNewAsset
    
} // Accountant

En esta clase definimos un typealias, NetWorthChanged, el cual es un closure que toma como parámetro un tipo Double (como el valor del patrimonio) y no retorna nada.

Tenemos también dos propiedades: netWorthChangedHandler que es un closure opcional al cual llamamos cuando el valor del patrimonio cambia y netWorth donde almacenamos el valor total del patrimonio de una persona.

Esta última propiedad cuenta con un observador didSet que ejecuta el closure netWorthChangedHandler si este no es nil. Finalmente la función gainedNewAsset debería de ser llamada para informar a la instancia que el valor de un nuevo bien debe ser añadido al patrimonio.

Continuaremos por modificar la clase Person en pos de comenzar a hacer uso de la nueva clase Accountant. Luego de esta modificación el archivo Person.swift debe lucir como a continuación muestro:

import Foundation

class Person: CustomStringConvertible {
    
    let name: String
    let accountant = Accountant()
    var assets = [Asset]()
    var creditCards = [CreditCard?]()
    
    init (name: String) {
        
        self.name = name
        
        accountant.netWorthChangedHandler = {
            
            self.netWorthDidChange(netWorth: $0)
            
        }
        
    } // init
    
    deinit {
        
        print("\nLa memoria ocupada por el objeto \(self) está siendo liberada.")
        
    } // deinit
    
    var description: String {
        
        return name
        
    } // description
    
    func takeOwnershipOfAsset(asset: Asset) {
        
        asset.owner = self
        assets.append(asset)
        accountant.gainedNewAsset(asset: asset)
        
    } // takeOwnershipOfAsset
    
    func takeOwnershipOfCreditCard(myCreditCard: CreditCard) {
        
        creditCards.append(myCreditCard)
        
    } // takeOwnershipOfAsset
    
    func netWorthDidChange(netWorth: Double) {
        
        print("El patrimonio neto de \(self) es ahora \(netWorth) $.")
        
    } // netWorthDidChange
    
} // Person

A la clase Person hemos añadido la propiedad de nombre accountant en la línea 6 y cuyo valor por defecto es una nueva instancia de tipo Accountant. Desde este punto ya detectamos un enlace fuerte desde una futura instancia de Person hacia su instancia local de Accountant.

Dentro del inicializador init() hemos igualado a netWorthChangedHandler con un closure donde efectuamos una llamada a la función local netWorthDidChange que registra el nuevo valor del patrimonio.

Por último modificamos la función takeOwnershipOfAsset para que notifique a la instancia de Accountant acerca de las nuevas propiedades adquiridas.

Antes de continuar muestro el contenido del fichero main.swift por aquellos que quizás no han bajado el código de github:

import Foundation

var gochi: Person? = Person(name: "Gochi")

print("\nObjeto \(gochi!) creado.\n")

var masterCard: CreditCard? = CreditCard(owner: gochi!)

gochi?.takeOwnershipOfCreditCard(myCreditCard: masterCard!)

var laptop: Asset? = Asset(name: "Macbook Pro", value: 2500)
var hat: Asset? = Asset(name: "Cowboy Hat", value: 150)
var backpack: Asset? = Asset(name: "Black Backpack", value: 30)

gochi?.takeOwnershipOfAsset(asset: laptop!)
gochi?.takeOwnershipOfAsset(asset: hat!)

masterCard = nil

gochi = nil

print("\nEL objeto Gochi ahora es: \(gochi).")

laptop = nil
hat = nil
backpack = nil

print()

Si compilamos y corremos nuestra aplicación de consola la salida debe lucir como la siguiente:

Objeto Gochi creado.

El patrimonio neto de Gochi es ahora 2500.0 $.
El patrimonio neto de Gochi es ahora 2650.0 $.

EL objeto Gochi ahora es: nil.

La memoria ocupada por el objeto Asset (Black Backpack, valor 30.0, no tiene propietario) está siendo liberada.

Fuga de memoria

En la salida en pantalla nos percatamos de que tenemos una fuga de memoria (memory leak), las instancias laptop y hat no han sido liberadas, tampoco la instancia de CreditCard llamada masterCard y evidentemente tampoco la instancia de Person de nombre gochi.

Pero ¿qué ha pasado? antes de las nuevas modificaciones el código funcionaba perfectamente y sin embargo ahorita nos encontramos con el mismo problema.

[ads3]

La razón está asociada a una referencia fuerte que hemos añadido y que puede no ser muy obvia. Seguramente lo que nos viene rápidamente a la mente es que la clase Person tiene una referencia fuerte hacia Accountant, pero esta última no cuenta con ninguna hacia Person, entonces aparentemente no debería de haber ningún problema.

Cuando analizamos un poco más a fondo y recordamos la capacidad que tiene los closure de capturar las variables de su entorno comenzamos a sospechar del inicializador, específicamente de la línea 16:

self.netWorthDidChange(netWorth: $0)

que tal y como podemos observar hacemos uso de self.

Como parte de nuestra investigación proseguiremos a eliminar self, lo haremos en un intento de que cuando el closure se ejecute en la instancia de Accountant no haya una referencia explícita hacia Person. La nueva línea quedaría entonces como:

netWorthDidChange(netWorth: $0)

y volvemos a compilar. Al hacerlo nos salta un error en tiempo de compilación donde se nos informa los siguiente:

Call to method ‘netWorthDidChange’ in closure requires explicit ‘self.’ to make capture semantics explicit

En este mensaje de error podemos leer / interpretar que para efectuar una llamada a netWorthDidChange dentro del closure tenemos que hacerlo mediante self ya que la captura de valores tiene que ser semánticamente explícita. Con respecto a esto último, según he leído, Swift nos obliga a usar self para forzarnos a considerar que un ciclo de referencia fuerte es posible.

Recordemos que un closure cuenta con un ámbito propio dentro de su definición y que por defecto genera referencias fuertes hacia cualquiera de las variables que este usa dentro de su ámbito.

Así que nuestra sospecha es correcta, cuando llamamos a netWorthDidChange mediante self estamos creando una referencia fuerte desde Accountant hacia Person.

Mientras que ya teníamos una desde Person hacia Accountant, es decir que hemos generado mediante un closure un ciclo de retención con todas las de la ley.

En el diagrama que muestro a continuación podemos constatar lo antes explicado:

Ciclos de Referencia en Closures - Swift

Lista de captura

La solución a este problema reside en una lista de captura de closures (closure capture list en inglés). Modifiquemos el inicializador de Person para que luzca así:

init (name: String) {
        
    self.name = name
        
    accountant.netWorthChangedHandler = { [weak self] in

        self?.netWorthDidChange(netWorth: $0)

    } // closure

} // init

En la línea 7 es donde establecemos la lista de captura, en esta establecemos como weak la referencia que genera self. Como su propio nombre indica, una lista de captura es una lista, así que nos permite declarar más de una referencia siguiendo la sintaxis:

{ [weak referencia, unowned referencia...] (parámetros) -> (tipo de retorno) in

declaraciones

}

Es decir, separaríamos las referencias por comas siempre antecediéndolas con el modificador respectivo. En el caso de un closure con un formato simplificado (como el ejemplo de Person.init) siempre tenemos que añadir in tras la lista de captura.

Pero todo no termina aquí. Si analizamos bien el gráfico y la solución propuesta en la nueva versión de Person.init, quizás podamos aplicar una última mejora. Comencemos por tener en cuenta que Person tiene una referencia fuerte hacia Accountant mediante una instancia local de nombre accountant.

Debido a esto el enlace que se genera desde Accountant hacia Person mediante el closure jamás será nil y esto es debido a que el closure se crea sobre esa misma instancia local (accountant), cuya existencia en memoria depende de Person. Dicho esto y siguiendo las pautas determinadas en la documentación oficial sobre los ciclos de retención en closures y específicamente sobre la lista de captura:

If the captured reference will never become nil, it should always be captured as an unowned reference, rather than a weak reference.

Traduciendo esta nota en la documentación: siempre y cuando tengamos la certeza de que una referencia jamás será nil, el enlace debe ser siempre capturado como unowned en la lista de captura.

Así que modificamos nuevamente Person.init hacia esta versión definitiva:

init (name: String) {
        
    self.name = name
        
    accountant.netWorthChangedHandler = { [unowned self] in                      

        self.netWorthDidChange(netWorth: $0)                  

    }

} // init

En este punto salvamos y compilamos nuevamente, la salida en pantalla debe lucir como:

Objeto Gochi creado.

El patrimonio neto de Gochi es ahora 2500.0$.
El patrimonio neto de Gochi es ahora 2650.0$.

La memoria ocupada por el objeto Gochi está siendo liberada.

La memoria ocupada por el objeto Accountant está siendo liberada.

La memoria ocupada por el objeto CreditCard está siendo liberada.

EL objeto Gochi ahora es: nil.

La memoria ocupada por el objeto Asset (Macbook Pro, valor 2500.0, no tiene propietario) está siendo liberada.

La memoria ocupada por el objeto Asset (Cowboy Hat, valor 150.0, no tiene propietario) está siendo liberada.

La memoria ocupada por el objeto Asset (Black Backpack, valor 30.0, no tiene propietario) está siendo liberada.

Como podemos observar ya el código funciona correctamente.

Me gustaría comentar, antes de finalizar, que la razón de añadir la lista de captura en la línea 7 es meramente por legibilidad y limpieza en el código.

accountant.netWorthChangedHandler = { [unowned self] in                  

    self.netWorthDidChange(netWorth: $0)

}

Pero he visto como algunos iOS Developer la establecen luego de abrir las llaves del closure:

accountant.netWorthChangedHandler = { [unowned self] in

    self.netWorthDidChange(netWorth: $0)

}

Como he comentado en otras ocasiones la elección recae en ustedes, yo personalmente pienso que esta última variante hace que la primera línea luzca ligeramente más críptica y quizás tengamos que hacer una pausa, mientras que la primera opción luce más clara y se puede leer más rápido sin lugar a equivocaciones.

Falta aún mucho por aprender en nuestro camino a convertirnos en iOS Developer. Suscríbete a nuestra lista de correo y síguenos en nuestras redes sociales. Mantente al tanto de todas nuestras publicaciones.

Espero que todo cuanto se ha dicho aquí, de una forma u otra le haya servido de aprendizaje, de referencia, que haya valido su preciado tiempo.

Este artículo, al igual que el resto, será revisado con cierta frecuencia en pos de mantener un contenido de calidad y actualizado.

¡Cualquier duda o sugerencia, ya sea errores a corregir o ejemplos a añadir, será más que bienvenida, necesaria!

Deja una respuesta

Su dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para mejorar su experiencia. Asumiremos que está de acuerdo con esto, pero puede optar por no participar si lo desea. Aceptar Leer Más

RECIBE CONTENIDO SIMILAR EN TU CORREO

RECIBE CONTENIDO SIMILAR EN TU CORREO

Suscríbete a nuestra lista de correo y mantente actualizado con las nuevas publicaciones.

Se ha suscrito correctamente!