관리 메뉴

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

[iOS - swift] lazy var 클로저 사용 주의 (리테인 사이클, 메모리 릭) 본문

iOS 응용 (swift)

[iOS - swift] lazy var 클로저 사용 주의 (리테인 사이클, 메모리 릭)

jake-kim 2023. 1. 13. 23:48

Lazy var 클로저 사용시 주의사항

  • lazy var 클로저 사용 시 retain cycle이 발생하는지?
  • 아래 1)번과 2)번 구분 (아래에서 계속)

1)

private let text = "label"

private lazy var label: () -> UILabel = {
    let label = UILabel()
    label.text = self.text // self를 안쓰면 컴파일에러 발생
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
}

2)

private let text = "label"

private lazy var label: UILabel = {
    let label = UILabel()
    label.text = text  // self를 안써도 컴파일 에러 x
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
}()

1) escaping 클로저

  • 클로저 안에서 self 키워드를 붙여서 다른 프로퍼티 접근 (self 키워드 없을 시 컴파일에러 발생)
  • self의 reference count가 늘어나는 동시에, 해당 클로저는 바로 사라지지 않으므로 self에 관해 reference count 유지
  • 동시에 self에서도 해당 label을 참조하므로 retain cycle 발생
private let text = "label"

private lazy var label: () -> UILabel = {
    let label = UILabel()
    label.text = self.text // self를 안쓰면 컴파일에러 발생
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
}
  • 예시
    • 버튼을 눌렀을 때 VC2를 띄우게 해놓고 다시 VC2를 dismiss 시켰을때 deinit이 호출되는지(메모리 해제가 되는지) 체크
    • > deinit이 안불리고 있으므로 retain cycle 발생
class VC2: UIViewController {
    private lazy var label: () -> UILabel = {
        let label = UILabel()
        label.text = self.text
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }
    
    private let text = "label"
    
    deinit {
        print("DEINIT: VC2")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .gray
        
        let label = label()
        view.addSubview(label)
        NSLayoutConstraint.activate([
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        ])
    }
}
  • 발생한 retain cycle 형태

  • text와 viewDidLoad는 self이므로 결국 아래처럼 retain cycle 형태
    • VC2를 참조하고 있는 인스턴스가 VC2를 dismiss 시켜도, VC2의 인스턴스는 retain cycle이 발생했으므로 reference count가 0이 아니게되어 메모리 릭 발생

2) non-escaping 클로저

  • 아래 클로저는 괄호 끝에 ()가 있으므로 즉시 적용된 클로저로 수행하는 것으로 인식되어, 컴파일러도 non-escaping으로 받아들여 self키워드도 사용하지 않고 사용 가능
private let text = "label"

private lazy var label: UILabel = {
    let label = UILabel()
    label.text = text  // self를 안써도 컴파일 에러 x
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
}()
  • 예시
    • retain cycle이 없으므로 dismiss 하면 바로 메모리 해제 ('DEINIT: VC3' 출력)
class VC3: UIViewController {
    private lazy var label: UILabel = {
        let label = UILabel()
        label.text = text
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    private let text = "label"
    
    deinit {
        print("DEINIT: VC3")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .gray
        
        view.addSubview(label)
        NSLayoutConstraint.activate([
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        ])
    }
}

// dismiss하면 DEINIT: VC3 출력

결론

  • 클로저 끝에 ()가 붙어있으면 해당 클로저가 지연되지 않고 즉시 실행
  • 클로저가 지연되지 않고 즉시 실행된다는 의미?
    • 컴파일러가 파악할 수 있으므로 따로 self키워드를 사용하지 않고 사용할 수 있고 retain cycle도 발생하지 않음
  • 클로저 끝에 ()가 붙어있지 않은 lazy var는 escaping되며 retain cycle이 발생

* 전체 코드: https://github.com/JK0369/ExLazyInit

Comments