Swift는 순환 참조 문제를 해결하기 위해 weak와 unowned 키워드를 제공한다.
weak와 unowned는 '참조하는 클래스 인스턴스의 RefCnt를 증가시키지 않는다' 라는 공통점이 있지만, 'Optional 가능 여부' 라는 면에서 차이점이 있다.
💡 Swift 5.0부터 unowned에 optional을 지원하게 되면서, optional 가능 여부는 더 이상 의미가 없어졌다.
weak와 unowned optional의 디테일한 차이를 고민하면서 알아보던중,
[Stack Overflow] - 'Optional unowned reference versus weak in Swift 5.0' 이곳에서 힌트를 얻었다.
오버헤드와 시간/공간/계산의 복잡성에서 차이가 있는것 같다
그래서 Swift의 Repo의 RC와 관련된 RefCount.h 를 통해 RC에 대해 공부해 보았다.
🗣️ object는 개념적으로 3개의 refcounts
를 갖는다. refcounts
는 isa 다음 필드에 "inline"으로 저장되거나, isa가 포인터를 가지고 있는 "side table entry"에 저장 된다.
: strong RC는 object에 대한 strong reference하는 개수를 계산한다. strong RC가 0에 도달하면 object는 deinit이 되고, 미소유 참조 (unowned reference) 접근 시 error
를, 약한 참조 (weak reference) 접근 시 nil
이 된다.
strong RC는 extra count (추가 카운트)
인 StrongExtraRefCount에 저장되며 '물리적 필드 0은 논리적 값 1' 이다.
: unowned RC는 object에 대한 unowned reference하는 개수를 계산한다. unowned RC는 강한 참조를 대신해 추가적인 +1 값을 갖고, 이 +1 값은 deinit이 완료되면 감소한다.
unowned RC는 0에 도달하면 'object의 할당을 해제 (= freed)' 한다.
: weak RC는 object에 대한 weak reference하는 개수를 계산한다. weak RC는 미소유 참조를 대신해 추가적인 +1 값을 갖고, 이 +1 값은 object의 할당 해제가 되고 난 뒤 감소한다.
weak RC는 0에 도달하면 'object의 side table entry가 해제' 된다.
object가 처음 생성될 때는 side table이 없다. 객체는 아래와 같은 경우에 side table을 얻을 수 있는데,
side table entry는 one-way operation으로 생성되며, object가 side table entry를 가지게 되면 절대 잃어 버릴수 없다.
이는 'thread races (race condition 또는 race hazard)를 막기 위함' 이다.
⭐strong
과 unonwed
는 object를 직접 가리키며 (참조), weak
는 object의 side table을 가리킨다.⭐
object는 최적화를 위해 "inline" 으로 refcounts를 저장하며,
CASE 1. Weak 참조가 아닐경우 ❌:
strong RC
, unowned RC
, flags
가짐
CASE 2. Weak 참조일 경우 ✅:
HeapObjectSideTableEntry
의 포인터 가짐
↪ HeapObjectSideTableEntry 안에서 strong RC
, unowned RC
, weak RC
, flags
가짐
Understanding Swift Side Tables |
---|
![]() |
ref. Discover Side Tables - Weak Reference Management Concept in Swift
HeapObject
HeapObject의 isa
는 '객체의 클래스 메타데이터를 가리키는 포인터' 이다.
HeapMetadata const *__ptrauth_objc_isa_pointer metadata;
ptrauth를 사용하여 실제 ptr 변수 metadata
가 objc 객체의 포인터를 가리킨다.
Ref.
HeapObjectSideTableEntry
What is flags?
CASE 1. side table이 존재 하지 않는 경우의 lifecycle ❌ (strong, unowned)
LIVE without side table
Strong var
와 Unowned var
의 연산은 정상적으로 작동한다.Weak var
는 side table을 추가하고 저장한 뒤, LIVE with side table 상태가 된다.deinit()
이 호출되어 object는 DEINITING 상태가 된다.DEINITING without side table
object의 deinit()
이 진행 중이므로, Strong var
연산에는 영향을 미치지 않는다.
Unowned var
의 load는 swift_abortRetainUnowned() 에 의해 중단되지만, store에 있어서는 정상적으로 작동한다.
swift_abortRetainUnowned(): runtime 시, unowned 참조가 더 이상 유효하지 않은 객체에 접근하려고 할 때 호출되는 함수
Weak var
는 load 될 수 없으며, stores에 nil
을 저장한다.
deinit()
이 완전히 끝나면, swift_deallocObject를 호출하고 swift_deallocObject는 canBeFreedNow() 를 호출하여 weak
와 unowned
참조가 있는지 없는지 빠르게 확인한다.
True: object는 freed되어 DEAD 상태가 된다.
False: unowned RC를 감소시키고 object는 DEINITED 상태가 된다.
DEINITED
deinit()
이 끝났으나 아직 미소유 참조가 남아있다.Strong var
연산과 Unowned var
저장은 불가능하다.Unowned var
의 load는 swift_abortRetainUnowned() 에 의해 중단되며, Weak var
연산도 불가능하다.FREED
DEAD
CASE 2. side table이 존재하는 경우의 lifecycle ✅ (weak)
LIVE with side table
Weak var
에 대한 연산 처리가 발생하는 것 이외에는 LIVE without side table과 동일DEINITING with side table
weak var
load는 nil
을 반환하고 stores에 nil
을 저장한다.DEINITED
Weak var
load는 nil
을 반환하고, 저장은 불가능하다.FREED
Strong var
와 Unowned var
연산은 불가능하다.Weak var
load는 nil
을 반환하고, 저장은 불가능하다.DEAD
object의 lifecycle을 도식화
ref. Memory management on iOS
Reference
- https://jeonyeohun.tistory.com/373
- https://xing-ou.github.io/2018/08/07/swift%E4%B9%8BARC/
- https://www.jianshu.com/p/f240dec3ac42
- https://github.com/TannerJin/Swift-MemoryLayout/blob/master/Swift/Class.swift
- https://medium.com/@gauravkeshre/swift-object-lifecycle-b39d232cb99b
- https://velog.io/@kimscastle/iOS-unowned%EB%8A%94-%EC%99%9C-%EC%9E%88%EB%8A%94%EA%B1%B4%EC%A7%80%EB%A5%BC-%EB%AA%A8%EB%A5%B4%EA%B2%A0%EC%96%B4%EC%84%9C-%EC%A7%81%EC%A0%91-%EC%95%8C%EC%95%84%EB%B4%A4%EC%8A%B5%EB%8B%88%EB%8B%A4
위 내용을 배경으로 더 깊게 알아가고자 현재 아래의 references를 참고하여 인스턴스의 RC가 시프트 연산자를 사용하여 메모리에 어떻게 저장되는지 알아보고 있다.
Reference