이후에 인스턴스가 더이상 필요없다고 판단되면, 메모리 자원 활용을 위해 해당 인스턴스에 잡혀있던 메모리를 해제한다
usage가 남아있는 인스턴스의 메모리를 해제해버리면 APP이 Crash나므로 인스턴스가 필요없는게 맞는지 추적해야 한다
각 인스턴스가 현재 얼마나 많은 프로퍼티 + 상.변수에게 참조되고 있는지를 Count 하는 방식으로 추적한다
해당 인스턴스에 대해 Active Reference가 1개라도 있다면 해제하지 않는다
구체적으론, 인스턴스를 어떤 프로퍼티+상.변수로 참조시킬때마다 강한 참조라는게 만들어진다
"강한"이란 표현을 쓴 이유는 약한참조도 존재한다는 뜻이며 (추후설명) 강한 참조가 남아있다면 메모리는 해제되지 않는다
동작예시
인스턴스를 1개 만들어 3개의 변수가 참조하도록 만들었다
3개의 강한 참조가 존재하는 것이며 3개 모두 nil로 참조를 해제한 시점에서야 비로소 인스턴스가 해제되었다
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 인스턴스 간
강한 참조가 절대 사라질 수 없는 코드가 존재할 수 있다
두 Class 인스턴스가 서로를 강한 참조하는 경우이다.
이를 강한 참조 순환이라고 한다
이를 해결하는 방법으로, 일부 Class간 관계를 weak 참조 혹은 unowned 참조로 정의할 수 있다
약한 참조를 배우기 전에 강한 참조 순환이 어떻게 발생하는지 살펴보자
예제
STEP 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 인스턴스가 서로를 참조하고 있다
STEP2
john = nil
unit4A = nil
john과 unit4A의 참조를 끊어도 인스턴스간 연결은 끊어지지 않아 메모리가 해제되지 않고 소멸자는 호출되지 않는다
강한 참조 순환 해결방법 @Class 인스턴스 간
개념
2가지 방법을 제공한다. weak와 unowned
두 keyword는 인스턴스 간 참조하여도 강하게 hold하지 않고 강한 참조 Cycle을 형성하지 않는다
weak : 참조하려는 인스턴스가 먼저 해제될 때 Ex) 위의 코드에서 Apartment를 살펴보면 자기 자신보다 거주자에 해당하는 Person이 먼저 해제될 것 같으니 weak를 사용할 수 있다
unowned : 참조하려는 인스턴스가 더 나중에 혹은 동시에 해제될 때
weak
weak로 참조된 것은 연결을 끊지 않아도 ARC에 의해 해제될 수 있다
반드시 Optional 타입의 변수(variable)로 선언해야 한다.
ARC가 nil로 값을 변경하여 해제하기 때문
그리고 사용할 때는 nil 체크를 동반하면 된다
참고로, ARC에 의한 값변경(→ nil)은 Property Observer를 호출하진 않는다
예제
STEP1
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이 유일하다
STEP2
john = nil
// Prints "John Appleseed is being deinitialized"
유일한 강한 참조인 john을 해제하면 Person 인스턴스를 강한 참조하는 것이 존재하지 않게 되고 Person 인스턴스도 ARC에 의해 해제된다
참조하던 인스턴스가 해제되면 자동으로 weak 참조는 nil로 설정된다
STEP3
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
unowned
weak와 마찬가지로 ARC의 자동 메모리 해제를 막지 않는다
하지만 unowned는 weak와 반대로, 참조 대상이 동시에 or 더 늦게 해제될 것 같을 때 사용된다 (=값이 있음을 확신할 수 있을 때)
또한, weak와는 달리 참조 대상이 해제되어도 자동으로 nil이 되지 않는다
그러므로 unowned는 항상 값이 있다고 가정되므로 Optional 타입일 필요가 없고 상수로도 사용가능하다
예제 - 두 인스턴스가 동시에 해제되는
STEP1
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가 반드시 있다= 동시에 해제되거나 Card가 먼저 해제될 것이다
= Customer를 unowned로 선언
= 적어도 Customer가 혼자 먼저 해제되는 경우는 없다
이를 위해, CreditCard의 생성자에 customer를 반드시 받도록 정의하였다
STEP2
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
STEP3
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
john의 강한 참조를 끊으면 Customer 인스턴스에 대한 어떤 강한 참조도 없으므로 Customer 인스턴스가 해제된다
동시에, CreditCard 인스턴스에 대한 강한 참조도 사라져 해제된다
Optional unowned
unowned를 Optional 타입으로 선언하면 사실상 weak와 같은 문맥에서 사용된다
Optional 타입이므로 nil check가 동반된다
예제
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]
Course는 없을 수 있어도 Department는 반드시 존재해야 한다
연계 Course가 아닐 수 있으므로 nextCourse는 없을 수 있어 Optional로 선언
Department → Course로의 강한 참조만 해제하면 인스턴스가 해제될 수 있다
암시적 추출이 필요한 경우가 있다
아래 코드와 같이 생성자 내에서 다른 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을 활용할 수 있다
Strong Reference Cycles for Closures
이전까지는 여러 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개의 강한참조만 존재한다
Resolving Strong Reference Cycles for Closures
인스턴스와 Closure간 순환참조는 어떻게 해결할까? weak나 unowned??
이 경우엔 다른 해결책이 제시된다.
그것은 바로.. Closure Capture List!
이것 또한 참조를 약하게 만드는 방식 중 하나이다
❗ 주의
Swift는 Closure 내에서 member를 참조할때 self를 붙일 것을 요구한다
Closure가 실수로 self를 capture할 수 있음을 인지하는데 도움을 준다
Defining a Capture List
참조를 약하게 만들고싶은 것들을 weak나 unowned 키워드와 쌍으로 명시해준다
예시를 살펴보자
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// closure body goes here
}
Weak and Unowned References
우선 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)을 해제하고 사용되는 경우가 존재할까?
댓글