본문 바로가기
iOS/Swift

[iOS/Swift] weak self를 활용한 메모리 누수 관리 예제 살펴보기

by iosdevlime 2023. 9. 30.

@escaping Closure의 정의와 기능,

메모리 누수(Memory leak)까지 앞선 포스팅을 통해 살펴보았습니다.

 

그렇다면

실제 프로젝트에서는 어떤 방식으로 위와 같은 개념을 다루게 되며,

참조 타입인 weak을 통해 메모리 누수를 해결하는지 살펴보도록 하겠습니다.

 

 

 


 

 

weak self는 어떤 경우에 사용하나요?

지연 할당으로 인해 순환 참조로 인한 메모리 누수가 발생될 가능성이 있는 경우

 

프로젝트를 진행하는 과정에서 활용되는 API Parsing 이나 Timer 와 같이

@escaping Closure를 통한 지연 할당, 즉 어떠한 동작 이후에 실시되는 행위를 구현하고자 할 경우 

반드시 특정 객체에 대한 Reference, 참조가 발생하게 됩니다.

 

만약, 두 개 이상의 객체가 강한 참조(Strong Reference)상태라면?

순환참조로 인해 메모리 누수가 발생하게 되며, 앱이 종료되는 상황이 벌어지게 됩니다.

 


 

다음과 같은 예제 코드를 통해 위 상황에 대해 보다 자세히 살펴보도록 하겠습니다.

UI구성 등 자세한 코드는 작성하지 않을 예정이오니, 소스코드를 참고해주시길 바랍니다.

(예시 프로젝트 환경은 Xcode 15.0로 구현하였습니다)

 

👉🏻 Source Code (Github)

Timer를 활용하여 초당 1씩 증가하는 타이머를 구현하고자 합니다.

1. 버튼을 통해 타이머가 동작하는 ViewController로 이동하기 위한 StartViewController가 존재합니다.
2. 타이머 ViewController는 2개로 나눠져 있으며, [Weak self]를 사용하는 경우그렇지 않은 경우로 나뉩니다.
3. 각각의 타이머 ViewController에서 Dismiss를 통해 StartViewController로 이동할 경우, 서로 상이한 결과가 나옵니다.

 

 

시작되는 VC (StartViewController)

  • StartVC에서는 Click버튼의 Action을 통해 각각의 하위 VC로 이동합니다.
    • 이 때, Navigation Controller로 인해 이미 Reference Count(이하 RC)가 증가하게 됩니다. (RC+1)
    // MARK: - ViewDidLoad
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        title = "Start"

        // push next viewController
        firstPushButton.addTarget(
            self,
            action: #selector(didTapFirstVCPushButton),
            for: .touchUpInside
        )

        // push next viewController
        secondPushButton.addTarget(
            self,
            action: #selector(didTapSecondVCPushButton),
            for: .touchUpInside
        )
    }
    
    
    ...
    
    // MARK: - push next viewController (Action)
    @objc private func didTapFirstVCPushButton() {
        let firstVC = MemoryLeakCountViewController()
        firstVC.navigationItem.largeTitleDisplayMode = .never
        self.navigationController?.pushViewController(firstVC, animated: true) // Reference Count +1
    }

    @objc private func didTapSecondVCPushButton() {
        let secondVC = UseWeakSelfViewController()
        secondVC.navigationItem.largeTitleDisplayMode = .never
        self.navigationController?.pushViewController(secondVC, animated: true) // Reference Count +1
    }

 


 

 

메모리 누수가 진행되는 경우 (MemoryLeakCountViewController)

  • StartVC에서 'Memory leak이 발생하는..' 하위에 Click버튼을 눌러 이동합니다.
    • 아래와 같이 timer를 초기화하고, UILabel을 통해 count 증가를 나타냅니다.
    • 현재 참조하고 있는 RC값의 메모리 해제 여부를 파악하기 위해 deinit 메서드를 작성합니다.
class MemoryLeakCountViewController: UIViewController {

    private var countNumber: Int = 0

    // MARK: - Components
    // 1. Timer
    private lazy var timer: Timer = Timer()

    // 2. Count Label
    private lazy var countLabel: UILabel = {
     // ...
    }()

    // MARK: - ViewDidLoad
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        title = "Counting System"

        setTimer()
    }

    // MARK: - Timer Setting
    private func setTimer() {
        self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in
            self.countNumber += 1
            print("---> 현재 Count : \(self.countNumber)")
            // countLabel text값 할당
            self.countLabel.text = "\(self.countNumber)"
        })
    }
    
    // MARK: - deinit (메모리에서 해제되는지 여부 파악) --> 실행되지 않음
    deinit {
        print("Count 값이 메모리에서 해제되었습니다")
        self.timer.invalidate()
    }

 

  • 정상적으로 Count가 증가하고 있으나, 다음과 같이 StartVC로 이동한 이후에도 증가되고 있습니다.
    • deinit이 호출되지 않고, 타이머 Count가 지속적으로 동작하고 있습니다.
    • 결과적으로 이는 메모리 누수가 발생한 상황입니다.

Memory leak 발생
Memory leak 발생

 

 


 

 

메모리 누수가 발생된 원인

  • 순차적으로 Reference Count의 흐름(증감)을 파악해 볼 필요가 있습니다.
    • setTimer() 메서드 내 self 캡쳐로 인해, 객체간의 참조 관계가 여전히 성립됩니다.
    • 즉, StartVC로 이동 한 이후에도 RC가 1이 남아있으므로, 메모리 누수가 발생합니다. 
1️⃣ StartVC에서 NavigationController를 참조, 다음 VC로 넘어갈 때 (RC + 1)
2️⃣ setTimer() 메서드 내부에서 현재 VC를 'self'로 캡쳐하여 사용 (RC + 1)
3️⃣ BackButton을 눌러 StartVC로 이동할 경우, pop 발생 (RC -1)

➟ StartViewController로 돌아갔음에도 불구하고, 결과적으로 RC는 1이 남게 됨

 


 

 

[weak self] 를 활용한 메모리 관리

  • StartVC에서 '[weak self]를 사용했을 경우..' 하위에 Click버튼을 눌러 이동합니다.
    • setTimer() 메서드에서 Timer 클로저에 [weak self] 키워드를 작성합니다.
    • weak reference는 참조하는 객체의 RC를 증가시키지 않으므로, 메모리 누수를 해결할 수 있습니다.
    // MARK: - Timer Setting -> [Weak self]를 통해 약한 참조를 하는 경우
    private func setTimer() {
        self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] _ in
            // 메서드 내부에서 참조하는 'self'(ViewController)에 대한 Optional Binding을 실시함
            guard let self = self else { return }
            self.countNumber += 1
            print("---> 현재 Count : \(self.countNumber)")
            // countLabel text값 할당
            self.countLabel.text = "\(self.countNumber)"
        })
    }

 

weak self 키워드를 활용한 경우
weak reference 활용

 

  • [weak self]를 활용한 경우, Reference Count의 흐름을 파악해보자면 다음과 같습니다.
    • self로 인해 객체 간 강한 참조가 발생되지 않아 Reference Count는 증가되지 않습니다.
    • setTimer() 내부에서는 임시적으로 guard 구문을 통해 Optional 타입인 객체를 언래핑 합니다.
      (weak reference의 특징 참고)
1️⃣ StartVC에서 NavigationController를 참조, 다음 VC로 넘어갈 때 (RC + 1)
2️⃣ setTimer() 메서드 내부에서 현재 VC를 'weak reference'로 캡쳐, 기존 RC는 유지됨
3️⃣ BackButton을 눌러 StartVC로 이동할 경우, pop 발생 (RC -1)

➟ StartViewController로 돌아갈 때, 결과적으로 RC는 0으로 초기화됨

 


 

 

프로젝트 예제를 통해

[weak self]를 왜 사용하는지, 어떻게 활용되는지 살펴본 포스팅이었습니다.

댓글