습관적 weak self 와 클로저 캡처링

김상우·2024년 1월 31일
3

ARC.

Swift 는 ARC 라는 메모리 관리 시스템을 사용한다.
Automatic Reference Counting 이라는 문자 그대로 객체의 Reference 가 몇 번 카운팅 되었는지를 계산하며, Reference Count 가 0 이 되면 메모리에서 삭제된다.

예를 들어, 다음과 같은 코드가 있을 때 SomeClass 에 대한 메모리는 계속 남게된다.

class SomeClass {
	// 메모리가 해제 되면 "deinit" print.
    deinit { print("deinit") }
}

var someClass1: SomeClass? = SomeClass() // reference count : 1
var someClass2 = someClass1  // reference count : 2
someClass1 = nil  // reference count : 1

// refernce count > 0 이므로 "deinit" 은 출력되지 않음.

클로저에서의 캡처링.

Swift 는 클로저라는 문법을 지원한다.
클로저 내부에서는 값의 캡처링이 일어나며, 참조 타입이 캡처되면 reference count 가 증가한다.

class SomeClass {
    // 메모리가 해제 되면 "deinit" print.
    deinit { print("deinit") }
}


var someClass: SomeClass? = SomeClass()  // reference count : 1

// 캡처 하면서 rc 증가. reference count : 2
var someClosure: (() -> ())? = { [someClass] in
    _ = someClass
}

someClosure?()
someClass = nil  // reference count : 1

// refernce count > 0 이므로 "deinit" 은 출력되지 않음.

weak self.

따라서 클로저에서 캡처링이 일어나면서 rc 가 증가하지 않도록, weak 키워드를 사용한다.

class SomeClass {
    // 메모리가 해제 되면 "deinit" print.
    deinit { print("deinit") }
}


var someClass: SomeClass? = SomeClass()  // reference count : 1

// weak 캡처. 약한 참조.
// rc 증가 하지 않음. reference count : 1
var someClosure: (() -> ())? = { [weak someClass] in
    _ = someClass
}

someClosure?()
someClass = nil  // reference count : 0

// refernce count == 0 이므로 "deinit" 출력.

개발자의 습관.

위와 같은 이유 때문에 대부분 클로저 안에서 습관적으로 weak self 를 붙이게 된다.
그리고 거의 대부분은 그게 올바른 선택이다.

UIView.animate()

다음과 같은 코드가 있다. [weak self] 를 하지 않고 있다. 약참조 캡처링을 하지 않고 있다.

UIView.animate(withDuration: 1,
               animations: { self.label.alpha = 0.5},
               completion: { _ in self.value += 1 })

하지만 이 코드는 메모리 누수가 일어나지 않는다.
UIView . animate 이기 때문이다. UIView 라는 타입에 대고 메서드를 호출하고 있다.
static 한 호출을 하고 있기 때문에 self 의 reference count 는 증가하지 않는다.

아래 사진은 UIView.animate 의 정의부를 캡처한 것이다.

class function (타입 메서드)이다.
UIView 의 인스턴스 self 가 어떤 것이던 무관하게 동작하는 (= static 한) 메서드이다.

습관적 weak self.

또한, 이런 상황에서는 weak self 캡처링을 하는 것이 적절하지 않다.

class Food {
    let name: String
    init(_ name: String) {
        self.name = name
    }
}

extension Food {
	// 클로저 내부에서 weak self 캡처링.
    // Food 의 인스턴스가 캡처된다.
    func getFoodName() -> (() -> String) {
        return { [weak self] in
            guard let self else { return "none" }
            // Food 의 name 을 return 한다.
            return self.name
        }
    }
}

class Human {
    let getFavoriteFood: () -> String
    init() {
    	// Food 의 name 이 "chicken" 으로 세팅 되었고,
        // getFavoriteFood 초기화.
        self.getFavoriteFood = Food("chicken").getFoodName()
    }
}

let human = Human()
print(human.getFavoriteFood())	

// "chicken" 이 출력되길 바라지만, "none" 이 출력 된다.

원하지 않는 결과가 나오는 이유는 다음과 같다.

  1. Human 의 init 내부에서 Food() 인스턴스가 생성된다.
  2. Food 의 getFoodName() 내부의 self 는 그 인스턴스를 바라본다.
  3. Human init 이 종료되면 Food() 인스턴스가 함께 소멸된다.
  4. Food 의 getFoodName() 내부의 self 가 바라보고 있던 인스턴스가 nil 이 되었다.
  5. 원하지 않는 결과 발생.

따라서 다음과 같이 캡처링을 해야한다.

extension Food {
    func getFavoriteFood() -> (() -> String) {
    	// self 가 아닌 name 을 캡처한다.
        return { [name] in
            return name
        }
    }
}

// 이후 원하는 결과 출력됨.
profile
안녕하세요, iOS 와 알고리즘에 대한 글을 씁니다.

0개의 댓글