[Swift] Automatic Reference Counting

Lena·2020년 12월 20일
0
post-custom-banner

Automatic Reference Counting

Swift는 ARC를 사용해서 앱의 메모리 사용을 관리합니다. 즉, 사용자는 메모리 관리에 대해 고려할 필요가 없으며 ARC가 자동으로 인스턴스가 더 이상 사용되지 않으면 메모리에서 해제해줍니다.
하지만 특별한 경우(Strong Reference Cycles이 발생할 가능성이 있는 경우) ARC를 직접 다뤄야 할 필요가 있습니다.

How ARC Works

ARC는 인스턴스가 생성되면 메모리를 할당하고, 인스턴스가 더 이상 사용되지 않으면 메모리에서 해제합니다. 아래 코드는 ARC가 어떻게 동작하는지 보여줍니다.

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

var reference1: Person?
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "John Appleseed") // reference counting +1
// Prints "John Appleseed is being initialized"

reference2 = reference1 // reference counting +1
reference3 = reference1 // reference counting +1

reference1 = nil // reference counting -1
reference2 = nil // reference counting -1

reference3 = nil // reference counting -1
// Prints "John Appleseed is being deinitialized"

Person 인스턴스를 참조하는 모든 변수가 nil이 될 때 비로소 Person 인스턴스가 더 이상 사용되지 않습니다. 따라서, deinit이 호출되며 메모리에서 해제됩니다.

Strong Reference Cycles Between Class Instances

두 class의 인스턴스가 서로를 참조하게 되면 Strong Reference Cycle이 발생합니다.
아래의 경우, PersonApartment의 인스턴스가 할당된 johnunit4Anil이 되어도 reference count가 0이 아니므로 인스턴스가 메모리에서 해제되지 않습니다.

// Here’s an example of how a strong reference cycle
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 }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

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

john!.apartment = unit4A // strong reference
unit4A!.tenant = john // strong reference

john = nil
unit4A = nil
// neither deinitializer was called
// causing a memory leak in your app.

Resolving Strong Reference Cycles Between Class Instances

스위프트에서는 class의 프로퍼티가 strong reference cycle을 만드는 것을 피하기 위해 두 가지 방법을 제공합니다.

Weak References

변수 앞에 weak를 붙여서 strong reference를 피할 수 있습니다. 참조하는 인스턴스가 해제될 때 weak 변수에 할당된 인스턴스도 ARC에 의해 함께 해제됩니다.
해제된 변수는 nil이 되기 때문에 weak는 항상 var와 함께 선언되어야 합니다.

Property observers aren’t called when ARC sets a weak reference to nil.

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

var john: Person?
var unit4A: Apartment?

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

john!.apartment = unit4A
unit4A!.tenant = john
john = nil
// Person 인스턴스에 strong reference가 없으므로 deinit called
// Prints "John Appleseed is being deinitialized"

print(unit4A!.tenant) // nil
unit4A = nil
// Prints "Apartment 4A is being deinitialized"

Unowned References

weak와 달리 unowned는 항상 값이 있습니다. nil이 될 수 없으므로, 참조하고 있는 인스턴스가 해제되지 않을 것을 확신하는 경우에만 사용해야 합니다.
아래 예제는 Customer 인스턴스가 해제되면 CreditCard도 함께 메모리에서 해제됩니다.

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") }
}

var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john = nil
// Customer instance에 strong reference된게 없으므로 deinit
// 이어서 CreditCard instance에 strong reference된게 없으므로 deinit
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

Unowned References and Implicitly Unwrapped Optional Properties

weakunowned 모두 일반적인 strong reference cycle을 해결할 수 있습니다.

  • 인스턴스의 프로퍼티가 nil이 될 수 있고, strong reference cycle 예상된다면 weak를 사용할 수 있습니다.
  • unowned는 하나는 nil이 될 수 있고, 다른 하나는 그렇지 않을 때 사용할 수 있습니다.

However, there’s a third scenario, in which both properties should always have a value, and neither property should ever be nil once initialization is complete. In this scenario, it’s useful to combine an unowned property on one class with an implicitly unwrapped optional property on the other class.

만약 두 프로퍼티가 항상 값을 가져야하고 한번 초기화 된 이후에는 nil이 될 수 없다면 unowned와 implicitly unwrapped optional을 사용할 수 있습니다.

class Country {
    let name: String
    var capitalCity: City! // implicitly unwrapped optional
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country // unowned reference
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

CountrycapitalCity처럼 implicitly unwrapped optional 타입은 옵셔널과 같이 기본 값(default value)은 nil이지만 unwrapping 없이 접근할 수 있습니다.

var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"

Strong Reference Cycles for Closures

두 class 인스턴스에서 발생하는 strong reference cycle처럼, class 인스턴스와 closure 간에 발생하는 strong reference cycle를 capture list를 작성해서 피할 수 있습니다.
closure의 정의부에서 captured reference가 weak 또는 unowned reference가 될 수 있도록 capture list를 선언할 수 있습니다.

class HTMLElement {

    let name: String
    let text: String?

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

Strong Reference Cycles for Delegates

delegate pattern에서도 strong reference cycle이 발생할 수 있습니다.

class ParentViewController: UIViewController, ChildViewControllerProtocol {
    let childViewController = ChildViewController()
    func prepareChildViewController(){
        childViewController.delegate = self
    }
}

protocol ChildViewControllerProtocol {
    // important functions
}

class ChildViewController: UIViewController {
    var delegate: ChildViewControllerProtocol?
}

childViewControllerdelegateParentViewController를 설정했습니다. 이로써 두 viewController는 서로 강한 참조 관계가 됩니다.
strong reference cycle을 피하기 위해delegate 변수를 weak로 선언해줄 수 있습니다.

weak var delegate: ChildViewControllerProtocol?

References

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html
https://baked-corn.tistory.com/30

post-custom-banner

0개의 댓글