Apple define una fuga de memoria como: “Memoria que estaba localizada en un punto pero, al no publicarla, tu aplicación ya no tiene referencia de ella. Por lo que, si no hay ninguna referencia de esta, no puedes volver a publicarla ni utilizarla”. Para entender al 100% fuga de memorias, primero tenemos que desarrollar la gestión de la memoria en el sistema operativo iOS.

Automatic reference counting (ARC)

ARC mantiene la cuenta de las veces que una referencia aparece.
Puede que sean weak o strong. En el caso de que se aparezca una referencia strong, el contador incrementa en 1 punto. Cuando se libera esa referencia, la cuenta decrece 1 punto. Por lo que si esa cuenta llega a 0, el sistema puede eliminarlo por completo de la memoria heap ya que no hay uso de ello. Las referencias weak no tienen valor en la cuenta de referencias por lo que se liberan la cuenta llega a 0.

 

Retain cycles

Como developer, este es el principal problema con el que nos enfrentamos cuando lidiamos con ARC. Aparece cuando dos objetos presentan fuertes referencias entre ellos. La cuenta de la referencia del primer objeto no puede ser 0 hasta que no haya release del segundo objeto y a la misma vez, la cuenta de la referencia del segundo objeto no puede ser 0 hasta que haya release del primer objeto.

Aquí tenéis un ejemplo de retención de ciclos:


class Dog {
    var owner: Person?
}

class Person {
    var dog: Dog?
}

let mark = Person()
let pancho = Dog()
mark.dog = pancho
pancho.owner = mark

Dog y Person tiene referencias fuertes entre ellas y nunca se eliminarán de la memoria.

 

Rompiendo los retain cycle

Como hemos explicado antes, las referencias pueden ser strong o weak. A diferencia que las referencias strong, las weak no aumentan la cuenta de referencia por lo que son opcionales. No garantizan que la referencia tenga valor en el runtime. Hay dos tipos de referencias weak:

 

Weak

Este tipo de referencia es weak y tenemos que fijarnos si la referencia de ese objeto sigue siendo válido cuando accedemos a él. Este tipo de referencia se suele utilizar cuando no estamos seguros si el tiempo de vida de la referencia es menor o mayor que el objeto haciéndole referencia.

 

Unowned

Esta referencia no es opcional por lo que sólo podemos utilizarla cuando estamos seguros de que el tiempo de vida de la referencia es mayor (o igual) que el objeto haciéndole referencia. Si intentamos acceder a una referencia unowned que ya no está en la memoria, tendremos una runtime exception

Refactorizando el ejemplo de arriba para break the retain cycle y evitar fuga de memorias sería así:


class Dog {
    weak var owner: Person?
}

class Person {
    var dog: Dog?
}

let mark = Person()
let pancho = Dog()

mark.dog = pancho
pancho.owner = mark

Ahora pancho tiene una referencia weak hacia el owner, mientras que mark sigue teniendo una referencia strong hacia Dog. Cuando se libere mark, ARC se liberará automaticamente a pancho.

 

Retain cycles en closures

Los Closures son una de las características más conocidas de Swift, sin embargo, es fácil crear un retain cycle si no se usa de manera correcta. Los Closures son un tipo de referencia, por lo que pueden provocar una memory leak si haces referencia a un objeto con una referencia a los closure. Échale un vistazo al ejemplo de abajo, el cuerpo de la closure hace referencia a la class.


class TestClass {
    let constant = "Hello"

    lazy var closure: (() -> ())? = {
        print(self.constant)
    }
}

Esto se puede resolver mediante un capture list. Definiendo el tipo de referencia que utilizamos dentro del cuerpo de la closure; si marcas la self reference como débil, retain cycle se rompe.


 lazy var closure: (() -> ())? = { [weak self] in
        print(self?.constant)
    }

 

Detectando memory leaks

El caso de arriba es simple y posiblemente lo podamos detectar únicamente observando el código, pero en memory leaks de aplicaciones de verdad puede que no sea tan obvio.
Por lo que nos harán falta herramientas y técnicas. Apple presentó el Memory graph debugger en Xcode 9 que permitía detener la aplicación y conseguir una imagen del actual estado de la memoria. Podemos obtener una lista de los objetos en la memory heap si seleccionamos un objeto; seleccionandolo podemos ver todos los objetos que le hacen referencia a la vez que todo su backtrace.

ios developer memory leak

Para detectar memory leaks ha de ejecutarse la aplicación y navegar a través de todos los flows posibles y abrir varias veces los mismos view controllers.Luego abrir el memory graph debugger y echar un vistazo a la memory heap. Busca objetos que no deberían estar en la memory heap, por ejemplo:

  • Un ViewController que ya no está en la aplicación.
  • Un número distinto de instancias de un mismo objeto.

El memory graph debugger muestra una exclamación púrpura entre objetos que puede que estén sufriendo una leak. Aún así, deberás comprobar si es una memory leak o un falso positivo.

 

Testing fuga de memorias

Es fácil y simple realizar test unitarios de fugas de memoria. Comprobando que el sistema bajo test (SUT) es nulo después de que la ejecución del test haya terminado. Para ello, asignamos el SUT a una referencia débil y nos fijamos si la referencia sigue siendo nulo en el método teardown.


class MemoryLeakTestTests: XCTestCase {

    weak var weakSUT: Person?
    
    func testPersonLeak() {
        let mark = Person()
        weakSUT = mark
        let pancho = Dog()
        
        mark.dog = pancho
        pancho.owner = mark
    }
    
    override func tearDown() {
        XCTAssertNil(weakSUT, “SUT is leaking”)
    }

}

Utilizando el ejemplo de Person – Dog podemos ejecutar un test y ver si hay alguna memory leak. Si rompemos el retain cycle cambiando el tipo de referencia del owner del Dog a weak y ejecutamos el test otra vez, vemos que ahora sí pasa el test. WeakSUT es nulo por lo que esta referencia ha sido liberada.

 

Suscríbete a nuestro newsletter para estar al día de desarrollo de aplicaciónes moviles e fuga de memorias en iOS en concreto!

 

Si este artículo sobre fuga de memorias en iOS te gustó, te puede interesar:

 

iOS snapshot tests

La capa de datos con MoyaRx y Codable

Mejorar la testabilidad de CLLocationManager

Espresso UI Test

F-Bound en Scala

Tendencias en aplicaciónes móviles

Patrón MVP en iOS

Debugging con Charles Proxy en Android emulator

Por qué Kotlin?   

Integración Continua en iOS usando Fastlane y Jenkins  

Cornerjob – iOS objective-C app un caso de exito