관리 메뉴

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

[iOS - swift] DispatchQueue에서 [weak self]를 안써도 되는 이유 이해하기 본문

iOS 응용 (swift)

[iOS - swift] DispatchQueue에서 [weak self]를 안써도 되는 이유 이해하기

jake-kim 2024. 2. 10. 01:03

기본지식 1) capture list 개념 

closure = { [weak self] in // <- capture list
}
  • 실행이 지연되는 @escaping closure와 같은 클로저에서 메모리 릭을 피하기 위해 사용하는 방법
    • @escaping closure는 단어 그대로 "해당 클로저를 벗어난다"라는 의미, 즉 코드 위치상 클로저 안에 있는 실행이 소스코드 순서에 맞추어서 바로 실행되지 않고 지연될 수 있다는 의미
    • 지연된다는 의미는 해당 클로저를 내부적으로 참조하여 저장하고 있다가, 필요할때 실행하겠다는 의미

기본지식 2) @escaping을 붙이지 않는 경우

  • 아래 closure 프로퍼티는 특정 클로저를 저장할 수 있는 stored property이고 이 저장된 클로저는 늦게 실행될 수 있어서, @escpaing을 붙여야 되겠다고도 생각할 수 있음
  • 하지만 closure 프로퍼티가 선언된 위치는 SomeClass 블럭 안이고 외부에서 이것을 저장하고 실행이 딜레이 될 수 있다는 것이 아니므로 @escaping 키워드를 안쓰는 것
class SomeClass {
    var closure: (() -> Void)?
}
  • 그래서 보통 위와 같은 전역변수가 아닌, 함수 or 메서드에 @escaping 키워드를 붙이는 것
func someFunc(_ closure: @escaping (Void) -> Void) {
}

기본지식 3) [weak self]를 안써서 memory leak을 일으키는 코드

  • SomeClass의 setupClosure() 메서드를 보면 closure를 assign할 때 self를 집어넣음 -> retain cycle 발생 
    • 1) SomeClass 인스턴스가 closure 프로퍼티 참조
    • 2) closure 프로퍼티가 SomeClass 인스턴스 참조 (이 부분이 클로저 안에서 self를 참조해서 발생)
class SomeClass {
    var closure: (() -> Void)?
    
    func setupClosure() {
        closure = {
            // retain cycle
            print("SomeClass instance is \(self)")
        }
    }
    
    deinit {
        print("SomeClass instance is being deinitialized.")
    }
    
    func someFunc(_ closure: @escaping (Void) -> Void) {
    }
}

func createMemoryLeak() {
    let object: SomeClass = SomeClass()
    object.setupClosure()
    object.closure?()
}

DispatchQueue.main.async 형태 생각해보기

  • DispatchQueue는 class 타입임을 확인
open class DispatchQueue : DispatchObject, @unchecked Sendable {
}
  • DispatchQueue처럼 비슷하게 만들어보기
class MyDispatchQueue {
    private var closure: (() -> Void)?
    static let main = MyDispatchQueue()
    
    private init() { print("INIT: MyDispatchQueue") }
    
    func async(_ closure: @escaping () -> ()) {
        self.closure = closure
    }
    
    deinit { print("DEINIT: MyDispatchQueue") }
}
  • 사용하는쪽
    • MyDispatchQueue의 main 프로퍼티에 접근하는 순간, main 프로퍼티가 생성
class VC2: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        MyDispatchQueue.main.async {
            print("\(self)")
        }
    }
    
    deinit { print("DEINIT: VC2") }
}
  • 여기서 [weak self]를 사용하지 않고 self로 접근하는데, 이렇게해도 되는 이유는 VC2에서 DispatchQueue.main 프로퍼티를 참조하고 있지 않기 때문에 VC2는 메모리 릭이 걸리지 않음
    •  단, DispatchQueue.main 프로퍼티는 싱글톤이므로 메모리에서 내려가지 않고 앱이 실행하는동안 계속 메모리에 남아있게됨 (싱글톤의 단점)
  • 만약 VC2에 main 프로퍼티를 참조하는것을 둔다면 VC2가 메모리 릭이 발생할것
    • (VC2를 dismiss해도 DEINIT: VC2가 호출되지 않음)
class VC2: UIViewController {
    var main: MyDispatchQueue?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .lightGray
        

        self.main = MyDispatchQueue.main // <-
        
        MyDispatchQueue.main.async { 
            print("\(self)")
        }
    }
    
    deinit { print("DEINIT: VC2") }
}

정리

  • retain cycle이 되는 원리를 이해
    • A와 B가 retain cycle이 되려면 서로 참조해야함
    • A instance <-> B instance
  • DispatchQueue.main은 static 프로퍼티인데, 이 프로퍼티는 따로 전역변수에 저장하지 않으면 memory leak 발생 x
    • 단 main 프로퍼티는 static으로 선언된 프로퍼티이므로 한번 이상 접근하면 항상 메모리에 상주
  • 만약 static이 아닌 인스턴스가 별도로 필요하고 이 인스턴스를 전역으로 가지고 있으면 weak self를 서야함
    • 만약 전역으로 참조하고 있지 않다면 retain cycle이 발생하지 않을 것이므로 weak self를 쓰지 않아도됨
class SomeFuncClass {
    var closure: (() -> ())?
    
    func someFunc(_ closure: @escaping () -> ()) {
        self.closure = closure
    }
}

class VC2: UIViewController {
    var main: MyDispatchQueue?
    var storedSomeFuncClass: SomeFuncClass?
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // retain cycle
        storedSomeFuncClass = SomeFuncClass()
        storedSomeFuncClass?.someFunc {
            print("\(self)")
        }
        
        // retain cycle x
        let c = SomeFuncClass()
        c.someFunc {
            print("\(self)")
        }
    }
    
    deinit { print("DEINIT: VC2") }
}

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

 

Comments