자동 참조 카운팅이란?
ARC 동작 예제
// name이라는 상수 프로퍼티를 정의하는 클래스 Person
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?
// 하나의 변수에 Person 인스턴스 생성해 참조
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
reference2 = reference1
reference3 = reference1
// 모두 같은 Person 인스턴스 참조. 참조 횟수는 3
reference1 = nil
reference2 = nil
// 참조 횟수 1, Person 인스턴스 해지되지는 않음
reference3 = nil
// Prints "John Appleseed is being deinitialized"
// 참조 횟수 0, 메모리에서 Person 인스턴스 해지
절대로 메모리에서 해제되지 않는 경우?
→ 클래스 인스턴스간 강한 상호 참조를 하고 있는 경우: 강한 참조 순환
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?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
// Person 인스턴스의 참조 횟수 2
// Apartment 인스턴스의 참조 횟수 2
// 참조 해지
john = nil
unit4A = nil
// 변수가 각각 상호 참조 하고있어 참조 횟수는 1
// 두 인스턴스는 해지되지 않고 메모리 누수가 발생함
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") }
}
// Person 인스턴스와 Apartment 인스턴스의 변수에서 각각 인스턴스를 상호 참조하도록 할당
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
// Prints "John Appleseed is being deinitialized"
// 아래 그림과 같이 Person 인스턴스를 메모리에서 해지함
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
// Apartment 인스턴스를 참조하는 개체도 사라져, Apartment 인스턴스도 메모리에서 해지됨
가비지 콜렉션을 사용하는 시스템에서 weak pointer를 단순한 시스템 캐싱 목적으로 사용하기도 함. (메모리 소모가 많아지면 가비지 콜렉터를 실행해서 강한 참조가 없는 객체를 메모리에서 해제하는 식으로 동작하기 때문) 하지만 ARC는 이 경우와 다르게 참조 횟수가 0이 되는 즉시 해당 인스턴스를 제거하기 때문에 약한 참조를 이런 목적으로 사용할 수 없음.
미소유 참조는 참조 대상 인스턴스가 항상 존재한다고 생각 → 해제됐는데 접근하면 런타임 에러
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") }
}
// Customer는 card 변수로 CreditCard 인스턴스 참조
// CreditCard는 customer로 Customer 인스턴스 참조
// customer는 미소유 참조 unowned로 선언 (신용카드는 없더라도 사용자는 남아있기 때문)
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
// John이 Customer 인스턴스 참조, CreditCard 인스턴스 Customer 인스턴스 미소유 참조
// Customer 인스턴스에 대한 참조 횟수는 1
참조를 끊으면 아래 그림과 같음
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
// 더이상 Customer 인스턴스를 강하게 참조하는 인스턴스 없음
// Customer 인스턴스 해제됨 -> CreditCard 인스턴스 참조하는 개체도 사라짐 -> CreditCard 인스턴스도 메모리에서 해제
해당 참조가 nil이 될 수 있는지 여부에 따라 → 약한 참조, 미소유 참조
제3의 경우도 발생할 수 있음! :
두 프로퍼티가 항상 값을 갖지만 한 번 초기화 되면 절대 nil이 되지 않는 경우
→ 미소유 프로퍼티를 암시적 옵셔널 프로퍼티 언래핑을 사용해 참조 문제를 해결
class Country {
let name: String
var capitalCity: City! // 강제 언래핑
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
/*
Country의 capitalCity는 초기화 단계에서 City 클래스에 초기화된 후 사용됨
옵셔널이 되어야 하는데 여기서는 강제 언래핑 시킴
암시적 언래핑이 되어 Country에서 name이 초기화 되는 시점에 self를 사용할 수 있게 됨
*/
// City에서는 강한 참조 순환을 피하기 위해 미소유 참조로 country를 선언해서 사용
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"
강한 참조 순환 클로저에서도 발생 - 클로저에서는 self를 캡쳐하기 대문
→ 클로저 캡쳐 리스트 사용
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
if let text = **self.text** { // -> self 캡쳐
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 클로저는 다른 클로저로 변경될 수 있음
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
인스턴스와 클로저 간에 강한 참조를 하게 되어 강한 참조 순환에 빠지게 됨
paragraph = nil
// HTMLElement 인스턴스는 해제되지 않음.
캡쳐 참조에 강한 참조 대신 약한 참조 혹은 미소유 참조를 지정할 수 있음
(둘 중 어느 것? ← 상호 관계에 달림)
클로저의 파라미터 앞에 소괄호[]를 넣고 그 안에 각 캡쳐 대상에 대한 참조 타입을 적어줌
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}
클로저의 파라미터가 없고 반환 값이 추론에 의해 생략 가능한 경우 캡쳐리스트 정의를 in 앞에 적어줌
lazy var someClosure: () -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}
참조가 먼저 해제되는 경우 → 약한 참조
참조가 같은 시점이나 나중 시점에 해제되는 경우 → 미소유 참조
만약 캡쳐리스트가 절대 nil이 될 수 없으면 미소유 참조 리스트로 캡쳐되어야 함
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) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
// paragraph의 참조를 제거하면 HTMLElement 인스턴스가 바로 메모리에서 해제되는 것 확인
paragraph = nil
// Prints "p is being deinitialized"