Class의 인스턴스를 생성하면 ARC가 메모리를 할당하게 된다
이 메모리에는 인스턴스의 타입이나 저장 프로퍼티의 값들이 저장된다
이후에 인스턴스가 더이상 필요없다고 판단되면, 메모리 자원 활용을 위해 해당 인스턴스에 잡혀있던 메모리를 해제한다
usage가 남아있는 인스턴스의 메모리를 해제해버리면 APP이 Crash나므로 인스턴스가 필요없는게 맞는지 추적해야 한다
각 인스턴스가 현재 얼마나 많은 프로퍼티 + 상.변수에게 참조되고 있는지를 Count 하는 방식으로 추적한다
해당 인스턴스에 대해 Active Reference가 1개라도 있다면 해제하지 않는다
구체적으론, 인스턴스를 어떤 프로퍼티+상.변수로 참조시킬때마다 강한 참조
라는게 만들어진다
"강한"이란 표현을 쓴 이유는 약한참조도 존재한다는 뜻이며 (추후설명) 강한 참조가 남아있다면 메모리는 해제되지 않는다
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")
// Prints "John Appleseed is being initialized"
reference2 = reference1
reference3 = reference1
reference1 = nil
reference2 = nil
reference3 = nil
// Prints "John Appleseed is being deinitialized"
이를 해결하는 방법으로, 일부 Class간 관계를 weak 참조 혹은 unowned 참조로 정의할 수 있다
약한 참조를 배우기 전에 강한 참조 순환이 어떻게 발생하는지 살펴보자
예제
#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 }
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
//참고로, 내부 프로퍼티에 접근하려면 Unwrapping 해야한다
두 Class 인스턴스가 서로를 참조하고 있다
#2
john = nil
unit4A = nil
john과 unit4A의 참조를 끊어도 인스턴스간 연결은 끊어지지 않아 메모리가 해제되지 않고 소멸자는 호출되지 않는다
개념
2가지 방법을 제공한다. weak
와 unowned
두 keyword는 인스턴스 간 참조하여도 강하게 hold하지 않고 강한 참조 Cycle을 형성하지 않는다
weak
: 참조하려는 인스턴스가 먼저 해제될 때
Ex) 위의 코드에서 Apartment를 살펴보면 자기 자신보다 거주자에 해당하는 Person이 먼저 해제될 것 같으니 weak를 사용할 수 있다
unowned
: 참조하려는 인스턴스가 더 나중에 혹은 동시에 해제될 때
weak
💡 참고로, ARC에 의한 값변경(→ nil)은 Property Observer를 호출하진 않는다
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") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
Person 인스턴스를 살펴보자.
이전 예시와는 달리 강한 참조는 john이 유일하다
#2
john = nil
// Prints "John Appleseed is being deinitialized"
유일한 강한 참조인 john을 해제하면 Person 인스턴스를 강한 참조하는 것이 존재하지 않게 되고 Person 인스턴스도 ARC에 의해 해제된다
참조하던 인스턴스가 해제되면 자동으로 weak 참조는 nil로 설정된다
#3
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
unowned
weak와 마찬가지로 ARC의 자동 메모리 해제를 막지 않는다
하지만 unowned는 weak와 반대로, 참조 대상이 동시에 or 더 늦게 해제될 것 같을 때 사용된다 (=값이 있음을 확신할 수 있을 때)
또한, weak와는 달리 참조 대상이 해제되어도 자동으로 nil이 되지 않는다
그러므로 unowned는 항상 값이 있다고 가정되므로 Optional 타입일 필요가 없고 상수로도 사용가능하다
예제 - 두 인스턴스가 동시에 해제되는
#1
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가 없을 수 있지만 / Card는 customer가 반드시 있다
= 적어도 Customer가 혼자 먼저 해제되는 경우는 없다
= 동시에 해제되거나 Card가 먼저 해제될 것이다
= Customer를 unowned
로 선언
이를 위해, CreditCard의 생성자에 customer를 반드시 받도록 정의하였다
#2
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
#3
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
john의 강한 참조를 끊으면 Customer 인스턴스에 대한 어떤 강한 참조도 없으므로 Customer 인스턴스가 해제된다
동시에, CreditCard 인스턴스에 대한 강한 참조도 사라져 해제된다
unowned
unowned
를 Optional 타입으로 선언하면 사실상 weak
와 같은 문맥에서 사용된다
Optional 타입이므로 nil check가 동반된다
예제
#1
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
}
}
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]
아래 코드와 같이 생성자 내에서 다른 Class의 생성자를 호출하는 경우이다
capitalCity가 만약 Optional이 아니라면?
City 생성자를 호출할 때 self를 전달하는데 저번에 배웠듯이 fully initialized되기 전에는 self를 사용할 수 없어서 호출이 안된다
✅ Point
Optional로 정의하면 자동으로 nil 초기화되므로 name만 값을 설정해주면 fully initialized 되어 self를 사용할 수 있게 된다
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
}
}
이렇게 서로 참조하는 프로퍼티를 가진 관계의 Class가 생성자 내에서 인스턴스를 생성하려는 경우
self를 사용하기 위해 암시적 추출 Optional을 활용할 수 있다
이전까지는 여러 Class가 서로의 인스턴스를 프로퍼티로 가지는 경우였다
순환참조가 발생하는 또 다른 경우를 살펴보자
바로, self를 참조하는 Closure를 프로퍼티로 가졌을 때이다
이 상황을 Closure가 self를 capture
했다고 표현한다
코드를 먼저 보자.
class HTMLElement {
let name: String
let text: String?
//asHTML 변수는 self를 참조하는 Closure 프로퍼티이다
//참고로 lazy 선언이 없으면 Two-Phase rule에 위배되어
// self를 참조하는 default를 설정할 수 없다
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")
}
}
✅ Point
Closure가 순환참조를 유발할 수 있는 이유는 Closure 역시 참조타입이기 때문이다
본체가 어딘가에 따로 있고 asHTML이 그것을 강한참조하고 있는 것이다
Closure 역시 내부에서 self를 강한참조하고 있으므로 순환참조가 발생한다
❗ 주의
Closure가 self를 여러 번 참조하고 있지만, 1개의 강한참조만 존재한다
인스턴스와 Closure간 순환참조는 어떻게 해결할까? weak나 unowned??
이 경우엔 다른 해결책이 제시된다.
그것은 바로.. **Closure Capture List
!**
이것 또한 참조를 약하게 만드는 방식 중 하나이다
❗ 주의
Swift는 Closure 내에서 member를 참조할때 self를 붙일 것을 요구한다
Closure가 실수로 self를 capture할 수 있음을 인지하는데 도움을 준다
참조를 약하게 만들고싶은 것들을 weak나 unowned 키워드와 쌍으로 명시해준다
예시를 살펴보자
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// closure body goes here
}
우선 unowned
를 사용해야 하는 경우는
Instance가 항상 존재한다고 생각할 때이다
이 경우 Instance와 Closure는 운명 공동체이므로 동시에 해제될 것이다
반대로 weak
를 사용해야 하는 경우는
참조하는 Instance가 (미래의 어느 시점?) nil이 될 수 있음을 알고 있다
그래서 weak로 선언하면 Optional 타입으로 접근해야 한다
또한, Instance가 해제되면 자동으로 해당 참조는 nil이 된다
✅ Point
Capture하려는게 사용시점에 nil이 될 수 없다면unowned
를 쓰고
nil이 될 수 있다면weak
를 써라
위 예제에서 참조순환을 풀어보자
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")
}
}
여기선 왜 unowned
를 선택했을까?
아직 명확한 정답이 떠오르진 않지만 생각을 말해보면
Closure가 self의 프로퍼티인데 자기자신(self)을 해제하고 사용되는 경우가 존재할까?