Se produce una fuga de memoria en una aplicación iOS cuando la memoria que ya no está en uso no se borra correctamente y continúa ocupando espacio. Esto puede dañar el rendimiento de la aplicación y, eventualmente, cuando la aplicación se quede sin memoria disponible, causará un bloqueo. Para comprender mejor cómo se producen las fugas de memoria, es importante saber primero cómo las aplicaciones iOS administran su memoria. Echemos un vistazo a cómo evitar fugas de memoria en cierres rápidos.

Conteo automático de referencias

En iOS, la memoria compartida se administra haciendo que cada objeto realice un seguimiento de cuántos otros objetos tienen una referencia a él. Una vez que este recuento de referencias alcanza 0, lo que significa que no hay más referencias al objeto, se puede borrar de forma segura de la memoria. Como su nombre indica, este recuento de referencias solo es necesario para los tipos de referencia, mientras que los tipos de valor no requieren gestión de memoria. El recuento de referencias se hacía manualmente en el pasado llamando a retain en un objeto para aumentar su recuento de referencias y luego llamando a release en el objeto para disminuir su recuento de referencias. Este código era casi en su totalidad placa de caldera, aburrido de escribir y propenso a errores. Así que, como la mayoría de las tareas de poca importancia, se automatizó a través del Conteo Automático de Referencias (ARC), que hace las llamadas de retención y liberación necesarias en su nombre en tiempo de compilación.

Aunque ARC en su mayoría disminuyó la necesidad de preocuparse por la administración de la memoria, aún puede crear fugas de memoria rápidas siempre que haya referencias circulares. Por ejemplo, digamos que tenemos una clase Person que tiene una propiedad para un apartment y la clase Apartment tiene una propiedad Person llamada tenant:

class Person { var name: String var age: Int init(name: String, age: Int) {…} var apartment: Apartment?}class Apartment { let unit: String init(unit: String) {…} var tenant: Person?}let person = Person(name: "Bob", age: 30)let unit4A = Apartment(unit: "4A")person.apartment = unit4Aunit4A.tenant = person

Cuando creamos un nuevo Person y Apartment y los asignamos a las propiedades de los demás, ahora se hacen referencia entre sí de manera circular. Las referencias circulares también pueden ocurrir con más de dos objetos. En esta situación, tanto el Person como el Apartment mantienen una referencia al otro, por lo que en este caso ninguno de los dos se reducirá a un recuento de referencias de 0 y se mantendrán en memoria, aunque ningún otro objeto tenga referencias a ninguno de ellos. Esto se conoce como ciclo de retención y causa una fuga de memoria.

Relacionados: Tres características que serían reales Swifty

La forma en que ARC trata los ciclos de retención es tener diferentes tipos de referencias: fuertes, débiles y sin usar. Las referencias fuertes son del tipo de las que ya hemos hablado, y estas referencias aumentan el recuento de referencias de un objeto. Las referencias débiles, por otro lado, si bien todavía le dan referencia al objeto, no aumentan su recuento de referencias. Así que si tomamos la propiedad tenant en Apartment y la convertimos en una referencia débil, romperíamos nuestro ciclo de retención:

class Apartment { let unit: String init(unit: String) {…} weak var tenant: Person?}

Ahora nuestro objeto Person todavía mantiene su Apartment en memoria, pero lo contrario ya no es cierto. Por lo tanto, cuando la última referencia fuerte a Person se haya ido, su recuento de referencias se reducirá a 0 y liberará su Apartment, cuyo recuento de referencias también se reducirá a 0, y ambos se pueden borrar correctamente de la memoria.

En Swift, las referencias débiles deben ser opcionales var s porque, si no asume la responsabilidad de mantener un objeto en memoria, no puede garantizar que el objeto no cambie o salga de la memoria. Aquí es donde entra en juego el tercer tipo de referencia. las referencias no usadas son como referencias débiles, excepto que pueden ser lets no opcionales, pero solo deben usarse cuando esté seguro de que el objeto nunca debe ser nil. Al igual que los opcionales sin envolver a la fuerza, le estás diciendo al compilador «No te preocupes, yo me encargo. Confía en mí.»Pero, al igual que las referencias débiles, la referencia no usada no hace nada para mantener el objeto en la memoria y, si deja la memoria e intentas acceder a ella, la aplicación se bloqueará. De nuevo, al igual que los opcionales sin envolver a la fuerza.

Si bien los ciclos de retención son fáciles de ver con dos objetos apuntando uno al otro, son más difíciles de ver cuando se involucran cierres en Swift, y aquí es donde he visto que ocurren la mayoría de los ciclos de retención.

Evitar ciclos de retención En Cierres

Es importante recordar que los cierres son tipos de referencia en Swift y pueden causar ciclos de retención con la misma facilidad, si no más, que las clases. Para que un cierre se ejecute más tarde, necesita retener cualquier variable que necesite para que se ejecute. De manera similar a las clases, un cierre captura referencias como fuertes de forma predeterminada. Un ciclo de retención con cierre se vería algo como esto:

class SomeObject { var aClosure = { self.doSomething() } ...}

Relacionado: ¿Cuándo Debo Usar Bloques y Cierres o Delegados para Devoluciones de Llamada?

 Nueva llamada a la acción

En este caso, la clase SomeObject tiene una referencia fuerte a aClosure y aClosure también ha capturado self(la instancia SomeObject) con fuerza. Esta es la razón por la que Swift siempre le hace agregar self. explícitamente mientras está en un cierre para ayudar a evitar que los programadores capturen accidentalmente self sin darse cuenta y, por lo tanto, lo más probable es que provoque un ciclo de retención.

Para tener variables de captura de cierre como débiles o sin usar, puede dar instrucciones de cierre sobre cómo capturar ciertas variables:

class SomeObject { var aClosure = { in self.doSomething() delegate?.doSomethingElse() } ...}

Más información sobre la sintaxis de cierre aquí.

La mayoría de los ejemplos de cierre que he visto en tutoriales o ejemplos parecen capturar self como sin usar y llamarlo un día, ya que capturar self como débil lo hace opcional, lo que puede ser más incómodo de trabajar. Pero como aprendimos antes, esto es inherentemente inseguro, ya que habrá un bloqueo si self ya no está en la memoria. Esto no es muy diferente a simplemente forzar el desenvolver todas sus variables opcionales porque no desea hacer el trabajo para desenvolverlas de forma segura. A menos que pueda estar seguro de que self estará presente mientras dure su cierre, debe intentar capturarlo débil en su lugar. Si necesita un auto no opcional dentro de su cierre, considere usar un if let o guard let para obtener un fuerte self no opcional dentro del cierre. Debido a que hizo esta nueva referencia sólida dentro del cierre Rápido, no creará un ciclo de retención, ya que esta referencia se liberará al final del cierre:

var aClosure = { in if let strongSelf = self { doSomethingWithNonOptional(strongSelf) doSomethingElseNonOptional(strongSelf) }}

o incluso mejor:

var aClosure = { in guard let strongSelf = self else { return } doSomethingWithNonOptional(strongSelf) doSomethingElseNonOptional(strongSelf)}

Capturar uno mismo fuertemente

Aunque es una buena práctica capturar self débilmente en cierres, no siempre es necesario. Los cierres que no son retenidos por el self pueden capturarlo fuertemente sin causar un ciclo de retención. Algunos ejemplos comunes de esto son:

Trabajando con DispatchQueues en MCD

DispatchQueue.main.async { self.doSomething() // Not a retain cycle}

Trabajar con UIView.animate(withDuration:)

UIView.animate(withDuration: 1) { self.doSomething() // Not a retain cycle}

La primera no es una retener el ciclo desde self no retiene la DispatchQueue.main singleton. En el segundo ejemplo, UIView.animate(withDuration:) es un método de clase, que self tampoco tiene parte en la retención.

Capturar self fuertemente en estas situaciones no causará un ciclo de retención, pero también puede no ser lo que desea. Por ejemplo, volver a GCD:

DispatchQueue.main.asyncAfter(deadline: .now() + 60) { self.doSomething()}

Este cierre no se ejecutará durante otros 60 segundos y conservará self hasta que lo haga. Este puede ser el comportamiento que desea, pero si desea que self pueda dejar memoria durante este tiempo, sería mejor capturarla débilmente y solo ejecutarla si self todavía está disponible:

DispatchQueue.main.asyncAfter(deadline: .now() + 60) { in self?.doSomething()}

Otro lugar interesante donde self no necesitaría ser capturado fuertemente es en lazy variables, que no son cierres, ya que se ejecutarán una vez (o nunca) y luego se liberarán después:

lazy var fullName = { return self.firstName + " " + self.lastName }()

Sin embargo, si una variable lazy es un cierre, tendría que capturarse débilmente. Un buen ejemplo de esto viene de la guía del Lenguaje de programación Swift:

class HTMLElement { let name: String let text: String? lazy var asHTML: () -> String = { in if let text = self.text { return "<(self.name)>(text)</(self.name)>" } else { return "<(self.name) />" } }}

Este también es un buen ejemplo de una instancia que es razonable usar una referencia no propietaria, ya que el cierre asHTML debería existir mientras exista HTMLElement, pero ya no.

TL;DR

Cuando trabaje con cierres en Swift, tenga en cuenta cómo está capturando variables, en particular self s. Si self retiene el cierre de alguna manera, asegúrese de capturarlo débilmente. Solo capture variables sin usar cuando pueda estar seguro de que estarán en memoria cada vez que se ejecute el cierre, no solo porque no desee trabajar con un selfopcional. Esto le ayudará a evitar fugas de memoria en cierres rápidos, lo que conduce a un mejor rendimiento de la aplicación.

Deja una respuesta

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