iOS/Swift - ARC에 대해 (2)

김영채 (Kevin)·2022년 3월 28일
0

iOS & Swift

목록 보기
106/107

클래스 인스턴스 사이의 Strong Reference Cycle 해결하기


Swift 에서는 strong reference cycle를 해결하기 위해서 2가지 방법을 제공한다.

  1. weak reference
  2. unowned reference

✻ 둘 다 reference cycle에서 하나의 인스턴스가 다른 인스턴스를 “강하게" 참조하지 않게 도와주는 키워드다.

→ 다른 인스턴스의 생명 주기가 더 짧을 때, 즉 더 빨리 메모리에서 해제가 될 때 본인 인스턴스의 프로퍼티를 weak으로 명시하라.

→ 다른 인스턴스의 생명 주기가 본인과 같거나 더 길면 unowned로 명시

Weak References


weak으로 명시가 되어 있으면 강하제 참조하는 것이 아니기 때문에 ARC가 메모리에서 바로 해제할 수 있게 한다.

✻ ARC는 이제 약하게 참조하고 있는 인스턴스가 메모리에서 해제될 때 해당 weak 변수를 nil로 설정한다. 그리고 런타임에 nil로 설정하기 때문에 let이 아닌 var로 설정되어야 하고, Optional로 선언되어야 한다.

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

✻ Person 인스턴스라 함은 “Person(name: "John Appleseed")” 이걸 얘기하는거다 참고로.

위 인스턴스를 참조하는 변수는 john과 Apartment 인스턴스의 tenant가 있다.

john = nil

Person 인스턴스를 참조하고 있던 john이 nil로 설정되면 Person instance는 메모리에서 해제된다. Apartment의 tenant 변수는 Person 인스턴스를 “약하게" 참조하고 있었음으로 참조 횟수 +1 이 되지 않는다. 바로 참조 횟수는 0이 되고 해제

당연히 unit4A를 nil로 설정하면 다 해제

Unowned References


weak reference랑 마찬가지로 unowned reference역시 본인이 참조하고 있는 인스턴스에 대해 강하게 참조하지 않는다.

unowned라고 명시하면 항상 값이 있는 것이 확실할 때 써야한다. 그렇게 때문에 optional이 아니게 되고, ARC는 unowned reference의 값을 nil로 설정하거나 그러지 않는다.

즉, 참조하고 있는 인스턴스가 항상 값이 있고 메모리에서 해제가 되지 않은게 확실할 때 써야함. unowned라고 명시했는데, 이미 메모리에서 해제가 된 클래스 인스턴스를 참조하려고 하면 런타임 에러 (크래쉬)

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

왜 CreditCard의 customer 상수가 unowned인가?

→ 왜냐하면 하나의 CreditCard는 반드시 Customer의 credit card 이어야 하기 때문이다. Customer없이는 credit card가 애초에 생성될 수 없다.

위와 같은 경우에서 unowned 키워드를 쓰지 않으면 strong reference cycle이 발생한다.

var john: Customer?

john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

Customer 인스턴스는 CreditCard 인스턴에 대한 강한 참조를 가지고 있고, CreditCard 인스턴스는 Customer 인스턴스에 대한 unowned reference를 가지고 있다.

john = nil

모두 해제

Unowned Optional References


Optional로 설정되어 있는 특정 클래스에 대한 참조를 unowned로 설정할 수 있긴하다.

unowned optional reference랑 weak reference랑 맥락은 비슷하지만, 전자를 사용할 때는 주의해야 할 점이 항상 존재하는 객체를 참조하는 것이다. 그렇지 않으면 nil로 자동 할당되기 때문이다.

class Department {
    var name: String
    var courses: [Course]
    init(name: String) {
        self.name = name
        self.courses = []
    }
}

class Course {
    var name: String
    unowned var department: Department
    unowned var nextCourse: Course?
    init(name: String, in department: Department) {
        self.name = name
        self.department = department
        self.nextCourse = nil
    }
}

Department가 courses를 “소유"하고 있다. Course는 department랑 nextCourse에 대해 “미소유"하고 있다.

모든 Course는 특정 Department 아래에 의해서 관리되기 때문에 optional이 아니다. 그런데 nextCourse는 필수가 아니기 때문에 optional로 설정되었다.

let department = Department(name: "Horticulture")

let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)

intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]

unowned optional reference는 본인이 참조하고 있는 인스턴스에 대한 강한 참조를 하지 않는다. 일반 unowned reference랑 동일하게 작동은 하지만, unowned optional reference는 nil이 될 수도 있다는게 다른 점이다.

non-optional unowned reference랑 마찬가지로, nextCourse는 항상 메모리에서 해제가 되지 않은 Course를 참조하고 있어야한다.

즉, department.courses 에서 한 Course를 삭제한다면, 다른 코스가 해당 course에 대해 가지고 있는 참조를 해제해야한다.

클로저에서 발생하는 Strong Reference Cycle


클로저 내에서 클로저가 할당된 인스턴스의 프로퍼티를 접근하거나 self를 쓸 때 self가 “capture”된다고 표현. 이때 strong reference cycle이 발생함.

애초에 왜 발생하는데?

→ Closure는 Value Type이 아닌 Reference Type이기 때문이다. 어떤 프로퍼티에 Closure를 할당하면, 해당 클로저에 대한 참조를 할당 하는 것이다.

두 클래스 인스턴스 사이에서 발생하는 strong reference cycle은 아니고, 하나의 클래스 인스턴스랑 하나의 클로저가 서로를 참조하면서 발생하는 strong reference cycle인 셈이다.

이걸 해결하기 위해서는 Swift에서 “closure capture list” 라는 기능을 제공한다.

어떤 경우에서 strong reference cycle이 발생하는지부터 보자

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}

asHTML은 클로저임 → () -> String : 파라미터를 아무 것도 받지 않고 String을 리턴하는 함수.

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

✻ HTMLElement 인스턴스랑 asHTML 클로저 사이에 strong reference cycle이 발생하게 된다.

설명 : asHTML: () → String 클로저 내에서 self를 참조하고 있다. 이 self는 HTMLElement에 대한 인스턴스다. 즉, HTMLElement 클래스 인스턴스를 paragraph도 참조하고 있고, () → String도 참조하고 있다는 것이다: reference count == 2

() → String 내에서 self를 참조하고 있기 때문에 이걸 self를 “capture”하고 있다고 표현한다. capture하고 있는 것은 strong reference를 가진다는 것이다.

때문에 paragraph = nil 로 해도 HTMLElement 인스턴스는 메모리에서 해제가 되지 않는다. 왜냐하면 () → String 클로저 내에서 HTMLElement를 capture하고 있기 때문.

profile
맛있는 iOS 프로그래밍

0개의 댓글