관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 클로저를 사용할 때 주의할 점 (Memory Leaks, Retain Cycle, nested closure) 본문

iOS 응용 (swift)

[iOS - swift] 클로저를 사용할 때 주의할 점 (Memory Leaks, Retain Cycle, nested closure)

jake-kim 2023. 1. 3. 23:34

Memory Leak이란?

  • 메모리에 할당 되었지만, 절대 release되지 않고 reference되지 않는 것
  • 이것을 참조할 수 없기 때문에 절대 release할 수 없는 상태

Retain Cycle이란?

  • 생성자가 할당 해제된 후에도 인스턴스가 할당 해제되지 않도록 하는 reference 상 순환을 이루는 상태
  • 둘 이상의 인스턴스가 서로에 대한 strong 참조를 보유할 때 발생

retain cycle이 발생하지 않는 케이스

  • UIView.animate 클로저
    • 한 번만 실행된 다음 할당이 취소되므로 블록이 실행 될 때 애니메이션 블록의 strong 참조도 해제
UIView.animate(withDuration: 0.22) {
    self.view
}
  • DispatchQueue 클로저
    • 해당 클로저가 외부의 변수를 strong으로 capture했고, 이게 escaping(해당 메소드 블록이 끝난 다음에 실행) 되더라도 참조가
    • 단, asyncAfter일 경우, weak self 없이 storng으로 참조 시 해당 시간이 지난 후 memory 해제 (retain은 안되므로 메모리릭 x)
DispatchQueue.main.async {
    self.view
}

DispatchQueue.main.asyncAfter(deadLine: .now() + 2) {
    self.view
}
  • computed property에서도 memory leak x
var intValue: Int {
    self.age
}

Memory leak 유발하는 closure 구분하기

  • 일반적인 상황, closure에서 self를 쓰면 retain cycle되는 이유
    • B에서 a의 클로저에 접근하는 상황: A는 내부적으로 사용하는 모듈이며, closure를 받아서 escaping하는 상태
    • A -> B 참조(B프로퍼티에 a선언), A <- B 참조(B init부분 클로저에서 self로 참조)
// self에서 escaping하지 않아도 retain되는 원리
// B가 사용하는쪽
// B에서 클로저로 self를 넘기는 상황 -> A에서 내부적으로 escaping하면 retain cycle 발생
class A {
    private var closureEscaper: ((String) -> ())?

    func escape(closure: @escaping (String) -> ()) {
        print("escaping!")
        closureEscaper = closure
    }
}

class B {
    var name = "Jake"
    let a = A() // 1. B에서 A참조

    init() {
        a.escape { string in
            self.name = string // 2. A에서 B참조
        }
    }

    deinit {
        print("DEINIT: B")
    }
}

var b: B? = B()
b = nil
// retain cycle
// A -> B 참조(B프로퍼티에 a선언), A <- B 참조(B init부분 클로저에서 self로 참조)
  • closure에서 단순히 self를 사용하여 캡쳐했지만, 해당 클로저가 escaping되지 않으면 retain cycle이 발생하지 않음
// retain cycle x

class Jake0 {
    let name = "Jake"

    init() {
        // self.closure가 참조 -> print(self.name)
        {
            print(self.name) // print(self.name)이 참조 -> self
        }()
    }

    deinit {
        print("DEINIT Jake0")
    }
}

var jake0: Jake0? = Jake0()
jake0 = nil
// DEINIT Jake0
  • 캡쳐하고 escaping되면 retain cycle 발생
// memory leak o

class Jake {
    let name = "Jake"
    var closure: (() -> ())?

    init() {
        // self.closure가 참조 -> print(self.name)
        closure = {
            print(self.name) // print(self.name)이 참조 -> self
        }
    }

    deinit {
        print("DEINIT")
    }
}

var jake: Jake? = Jake()
jake = nil
// 메모리해제 x
  • DispatchQueue.main.asyncAfter(deadLine:)을 사용하면 n초 후 deinit
class Jake2 {
    let name = "Jake2"

    init() {
        // self.closure가 참조 -> print(self.name)
        let someClosure = {
            print(self.name) // print(self.name)이 참조 -> self
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            someClosure()
        }
    }

    deinit {
        print("DEINIT2")
    }
}

var jake2: Jake2? = Jake2()
jake2 = nil
// 3초 후 DEINIT2
  • nested closure인 경우, 안쪽에 weak self를 쓸 경우 retain cycle
// retain cycle

class Jake3 {
    let name = "Jake3"
    var closure: (() -> ())?
    var closure2: (() -> ())?
    
    init() {
        let outerClosure = {
            let internalClosure = { [weak self] in // 안쪽 closure에서 weak으로 self를 잡을때 이미 outerClosure에서 strong
                print(self?.name)
            }
            internalClosure()
        }
        closure = outerClosure
        closure?()
    }

    deinit {
        print("DEINIT3")
    }
}

var jake3: Jake3? = Jake3()
jake3 = nil
  • 만약 outerClosure에 weak self 쓸 경우 retain cycle x
class Jake3 {
    let name = "Jake3"
    var closure: (() -> ())?
    var closure2: (() -> ())?
    
    init() {
        let outerClosure = { [weak self] in // <- 외부에서 weak으로 self했으므로 retain cycle x
            let internalClosure = {
                print(self?.name)
            }
            internalClosure()
        }
        closure = outerClosure
        closure?()
    }

    deinit {
        print("DEINIT3")
    }
}

var jake3: Jake3? = Jake3()
jake3 = nil
// DEINIT3
  • lazy var 클로저인 경우 주의
// retain cycle o

class Jake4 {
    let name = "Jake"

    lazy var closure: (() -> ())? = {
        print(self.name) // 참조: closure -> self
    }

    init() {
        closure?() // 참조: self -> closure
    }

    deinit {
        print("DEINIT: Jake4")
    }
}

var jake4: Jake4? = Jake4()
jake4 = nil

* 참고

https://help.apple.com/instruments/mac/current/#/dev7b09c84f5

https://developer.apple.com/videos/play/wwdc2018/416/

https://blogs.halodoc.io/methods-to-prevent-memory-leaks-in-ios/

Comments