Automatic Reference Counting
앱의 메모리 사용량을 추적하고 관리합니다. 대부분의 경우, 메모리 관리는 Swift에서 작동 한다는 것을 의미하며 메모리 관리를 직접 생각할 필요가 없습니다. 해당 인스턴스가 더 이상 필요하지 않을 때 인스턴트에서 사용하는 메모리를 자동으로 해제합니다.
Reference Counting은 class의 인스턴스에만 해당됩니다.
ARC Works
클래스의 새 인스턴스를 생성 후, ARC는
- 해당 인스턴스에 대한 정보를 저장하기 위해 메모리 청크를 할당
- 인스턴스 형식에 대한 정보와 해당 인스턴스와 연결된 저장된 속성 값을 저장
- 인스턴스가 더 이상 필요하지 않은 경우 메모리를 해제
- 현재 각 클래스 인스턴스를 참조하는 속성, 상수 및 변수의 수를 추적
- 해당 인스턴스에 대한 active한 참조가 있다면 해제 불가
- 인스턴스가 확고하게 유지하고 참조한다면 strong reference
Strong reference
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
unit4A!.tenant = john
john = nil
unit4A = nil
해제가 되지 않는 문제가 발생
Resolving Strong Reference Cycles
weak 와 unowned 키워드를 사용하여 Strong reference를 사용하지 않고 참조할 수 있게 됩니다.
다른 인스턴스의 수명이 더 짧은 경우, 즉 다른 인스턴스를 먼저 할당 취소할 수 있는 경우 weak reference를 사용합니다.
반대로, 다른 인스턴스의 수명이 같거나 수명이 더 긴 경우 unowned reference를 사용합니다.
Reference Count
객체에는 3개의 refcount가 있습니다.
- strong RC
- unowned RC
- weak RC
isa 다음 필드에 inline으로 저장되거나 side table entry에 저장됩니다.
isa * 객체가 인스턴스인 클래스에 대한 포인터로 런타임에서 어떤 종류의 객체인지 알 수 있는 방법으로 사용 |
strong RC
객체에 대한 strong reference를 계산 합니다.
0에 도달하면 객체는 deinited되며, unowned는 error되며, weak는 nil이 됩니다.
물리적 필드가 0이면 논리적 값은 1 입니다.
unowned RC
unowned reference를 계산합니다.
strong reference를 대신하여 +1 추가합니다. deinit이 끝나면 +1은 감소됩니다.
0이 되면, 객체의 allocation을 freed합니다.
weak RC
weak reference를 계산합니다.
unowned reference를 대신하여 +1 추가합니다. 객체의 allocation이 freed되면 +1은 감소됩니다.
0에 도달하면 객체의 side table entry가 freed됩니다.
객체는 side table 없는 상태로 생성됩니다. side table을 얻는 방법으로는 아래와 같습니다.
- 객체에 weak reference가 형성될 때
- strong RC or unowned RC overflows
- 객체에 대한 추가적인 공간이 필요할 때
side table entry를 얻는 것은 단방향 연산이며, side talbe entry가 있는 객체는 이를 절대 잃지 않습니다.
이는 race condition을 방지합니다.
strong과 unnowned variables는 객체를 가리킵니다. weak variables는 객체의 side table을 가리킵니다.
HeapObject {
isa
InlineRefCounts {
atomic<InlineRefCountBits> {
strong RC + unowned RC + flags
OR
HeapObjectSideTableEntry*
}
}
}
HeapObjectSideTableEntry {
SideTableRefCounts {
object pointer
atomic<SideTableRefCountBits> {
strong RC + unowned RC + weak RC + flags
}
}
}
LIVE
side table이 없이
- Object's refcounts are initialized as 1 strong, 1 unowned, 1 weak.
- strong variable 연산은 정상적으로 작동합니다.
- unowned variable 연산은 정상적으로 작동합니다.
- weak variable 로드를 발생할 수 없습니다. (No weak RC storage)
- strong RC가 0에 도달하면 deinit()을 호출하며, 객체는 deiniting 됩니다.
side table이 있는
- weak variable 연산은 정상적으로 작동합니다.
- 다른 것은 위와 동일 합니다.
DEINITING
side table이 없이
- unowned variable 로드를 swift_abortRetainUnowned()에서 중지됩니다.
- unowned variable 저장은 정상적으로 작동합니다.
- weak variable 로드는 발생할 수 없습니다.
- weak variable 저장소에 nil을 저장합니다.
- deinit() 완료되면 swift_deallocObject를 호출합니다.
- canBeFreedNow()을 호출하면서 weak 또는 unowned references가 없는지 확인합니다.
- canBeFreedNow()가 동작하면 객체는 freed 됩니다. 그렇지 않다면 unowned RC가 감소하고 객체는 DEINITED 됩니다.
side table이 있는
- side table을 DEINITING
- weak variable 로드는 nil을 반환합니다.
- weak variable 저장소는 nil을 저장합니다.
- canBeFreedNow()는 항상 거짓으로 DEAD로 전환되지 않습니다.
- 다른 것은 위와 동일합니다.
DEINITED
side table이 없이
- deinit() 완료되었지만, unowned referenc가 남아 있습니다.
- strong variable 연산을 발생하지 않습니다.
- unowned variable 저장을 발생하지 않습니다.
- unowned variable 로드를 swift_abortRetainUnowned()에서 중지됩니다.
- weak variable 연산은 발생하지 않습니다.
- unowned RC가 0에 도달하면, 객체는 freed되며 DEAD
side table이 있는
- weak variable 로드는 nil을 반환합니다.
- weak variable 저장을 발생하지 않습니다.
- unowned RC가 0에 도달하면, 객체는 freed되며 weak RC는 감소하고 객체는 FREED됩니다.
- 다른 것은 위와 동일합니다.
FREED
side table이 없이
- 발생하지 않습니다.
side table이 있는
- 객체는 freed되었지만 side table에 weak reference가 남아있습니다.
- strong variable 연산을 발생하지 않습니다.
- unowned variable 연산을 발생하지 않습니다.
- weak variable 로드는 nil을 반환합니다.
- weak variable 저장을 발생하지 않습니다.
- weak RC가 0에 도달하면, side table freed되며 객체는 DEAD됩니다.
DEAD
객체 및 side table이 사라졌습니다.
낮은 레벨의 언어일수록 포인터 주소 및 메모리 관리를 직접하게 됩니다. C와 같은 언어는 속도가 대체적으로 빠르지만, 직접 관리를 해줘야 합니다. 그 중에서도 조심해야되는 부분은 댕글링 포인터 입니다.
댕글링 포인터
댕글링 포인터를 방지하기 위한 방법으로 메모리는 해제를 했지만, 해당 메모리에 접근을 할 수 있게 되면서 생각치 못한 문제가 발생하는 것을 방지하기 위해 해제를 하고 나서도 null을 저장시켜야 합니다.
unowned reference
Swift 5부터는 unowned reference는 optional을 지원하게 됩니다.
그렇지만, 중요한 부분은 참조가 항상 할당 해제가 되지 않은 인스턴스를 참조한다고 확신하는 경우에 사용됩니다.
강한 참조를 하지 않는 시나리오가 3가지가 있습니다
- nil이 허용된 두 속성
- nil이 허용된 속성과 nil이 될 수 없는 다른 속성
- nil이 될 수 없는 두 속성
차례대로 시나리오에 대한 답을 하자면, weak refererence, unowned optional reference. unowned reference를 사용하는 것이 가장 좋습니다.
모든 키워드는 사용처에 맞게 사용을 해야 되는 것을 알려줍니다.
[WWDC21] ARC in Swift
의도하지 않은 공유의 위험을 피하기 위해서는 값 유형을 사용하라고 권합니다.
- 객체의 수명은 init()에서 시작하여 마지막 사용 시 종료됩니다.
- ARC는 수명이 끝나면 객체를 할당 해제합니다.
- ARC는 참조 횟수로 객체의 수명을 추적합니다.
- Swift 컴파일러는 유지/해제 연산을 삽입합니다.
- Swift 런타임은 참조 횟수가 0인 객체를 할당 해제합니다.
Swift 객체 수명은 사용 기반입니다.
개체의 수명이 닫는 중괄호에서 끝나는 것을 보장하는 C++와 같은 언어와는 다릅니다.
개체의 보장된 최소 수명은 초기화에서 시작하고 마지막 사용에서 끝납니다.
그렇기 때문에 weak reference를 사용하게 되면 문제를 발생할 수도 있다는 것을 말합니다.
class Traveler {
var name: String
var account: Account?
}
class Account {
weak var traveler: Traveler?
var points: Int
func printSummary() {
if let traveler {
print("\(traveler.name) has \(points) points")
}
}
}
func test() {
let traveler = Traveler(name: "Lily")
let account = Account(traveler: traveler, points: 1000)
traveler.account = account
account.printSummary()
}
traveler는 printSummary 메서드를 호출하기 전에 Traveler 개체의 할당이 취소될 수도 있습니다.
그렇기에 잠재적인 버그를 가지게 됩니다.
해당 문제를 해결하기 위해서는 더 나은 API로 클래스를 재설계를 추천합니다.
- printSummary() 메서드를 Traveler에서 동작하도록 변경
- 순환 클래스 관계를 트리 구조로 변환
- 추가적인 비용이 있을 수 있지만, 잠재적 버그를 제거하는 확실한 방법
◆ 개체의 수명에 대해서 생각하고 메서드를 구현
◆ weak은 비용이 크기 때문에, unowned 사용 가능을 확인하고 사용
참고 사이트
'Language > Swift' 카테고리의 다른 글
[Swift] AutoLayout 오토레이아웃 변경 (0) | 2023.11.07 |
---|---|
[SwiftUI] WWDC23 Discover Observation in SwiftUI (1) | 2023.09.17 |
[WWDC16] Understanding Swift Performance (3) (0) | 2023.08.02 |
[WWDC16] Understanding Swift Performance (2) (0) | 2023.08.02 |
[WWDC16] Understanding Swift Performance (1) (0) | 2023.08.02 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!