[Swift] weak과 unowned 차이점에 대해서

어흥·2024년 9월 15일

Swift

목록 보기
28/28

오늘은 weak과 unowned 차이점에 대해서 알아보려고 합니다.
이 포스팅은 두 키워드를 사용하면 레퍼런스 카운트가 되지 않는 공통점이 있는데 어떤 이유에서 해당 인스턴스가 해제될 때에는 차이점을 가지고 있는지에 대한 궁금증으로부터 시작되었습니다.

strong, weak, unowned와 ARC에 대해서 아직 잘 모르겠다면 아래 포스팅을 참고하시면 됩니다!
ARC 포스팅

weak, unowned 비교

weak, unowned는 강한 참조를 유지하지 않기 때문에 메모리에 인스턴스를 유지할 수 없다는 공통점이 있습니다.

차이점은 메모리에서 인스턴스 해제 후에 weak는 자동으로 nil이 할당됩니다. 따라서 weak는 항상 optional type으로 선언해야 합니다.

unowned 존재하지 않는 객체의 메모리를 참조하게 되는 dangling pointer가 됩니다. (Swift 5 이후, unowned도 optional 타입을 사용할 수 있게 됨)

weakunowned
공통점reference count가 되지 않는다.reference count가 되지 않는다.
차이점메모리가 해제될 때 자동으로 nil로 초기화 된다.메모리가 해제되면 dangling pointer를 갖는다.

이러한 차이점이 생기는 이유는 무엇일까요?

그 이유는 바로 객체를 참조하는 방식이 다르기 때문입니다.

unowned는 객체를 직접 참조하는 반면 weak는 Side Table을 참조합니다.

포스팅 핵심 참고 자료

https://github.com/swiftlang/swift/blob/main/stdlib/public/SwiftShims/swift/shims/RefCount.h

Swift 공식 레포에 있는 RefCount.h 파일에서 로우 레벨에서 reference count가 strong, weak, unowned 별로 어떤 방식으로 이루어지는지 알아보려고 합니다.

Preface.

An object conceptually has three refcounts. These refcounts are stored either "inline" in the field following the isa or in a "side table entry" pointed to by the field following the isa.

객체는 개념적으로 3가지 refcount를 가지고 있다. 이 refcount는 객체 안에서 inline으로 저장되거나 side table entry로 저장된다.

Reference Count

The strong RC counts strong references to the object. When the strong RC reaches zero the object is deinited, unowned reference reads become errors, and weak reference reads become nil.
The strong RC is stored as an extra count: when the physical field is 0 the logical value is 1.

The unowned RC counts unowned references to the object. The unowned RC also has an extra +1 on behalf of the strong references; this +1 is decremented after deinit completes. When the unowned RC reaches zero the object's allocation is freed.

The weak RC counts weak references to the object. The weak RC also has an extra +1 on behalf of the unowned references; this +1 is decremented after the object's allocation is freed. When the weak RC reaches zero the object's side table entry is freed.

  1. Strong RC
    • 객체에 대한 strong references를 계산한 값이다.
    • 동작: Strong RC가 0이 되면 객체가 deinit된다 , unowned reference는 error가 발생하고 weak reference는 nil이 된다.
    • Extra Count: 실제 저장된 값이 0일 때 논리적 값은 1로 간주된다.
  2. Unowned RC
    • 역할: 객체에 대한 unowned references를 계산한 값입니다.
    • 동작: Unowned RC도 Strong Reference에 대한 추가 +1을 가진다. 이 +1은 객체가 deinit된 이후 감소된다.
    • 추가 카운트: Unowned RC가 0이 될 때, 객체의 allocation이 freed된다.
  3. Weak RC (약한 참조 카운트)
    • 역할: 객체에 대한 를 weak references를 계산한다.
    • 동작: Weak RC는 unowned reference에 대한 추가 +1을 포함한다. 객체의 allocation이 freed된 이후 +1이 감소된다.
    • 추가 카운트: Weak RC가 0이 되면 객체의 사이드 테이블 엔트리가 freed된다.

Weak RC에서 side table 내용이 나옵니다. 여기까지는 그냥 weak와 side table이 연관되어 있구나 생각하면 되겠다.

또 기억해야 할 건 객체에 대해서 denit, allocation freed 등 여러 상태가 존재한다는 것이다. 우리가 생각한 것보다 객체가 생성되고 할당 해제되기 까지 다양한 상태를 갖구나 예상할 수 있을 것이다.

참조 변수

이미지 출처

  • Strong 및 Unowned 변수: 객체를 직접 가리킵니다.
  • Weak 변수: 객체의 사이드 테이블을 가리킵니다.

Side Table (사이드 테이블)

사이드 테이블은 Swift에서 weak reference를 구현하기 위한 메커니즘이다.

  • 메모리 절약: weak RC를 위해 공간을 예약하는 것이 아니라 필요한 경우에만 side table를 할당
  • race condition 방지: weak reference가 직접 객체를 가리키는 것이 아니기 때문에 객체가 해제될 때 weak reference를 불러올 때 안전하게 nil로 설정할 수 있다.

Objects initially start with no side table. They can gain a side table when:

  • a weak reference is formed and pending future implementation:
  • strong RC or unowned RC overflows (inline RCs will be small on 32-bit)
  • associated object storage is needed on an object
  • etc
    Gaining a side table entry is a one-way operation; an object with a side table entry never loses it. This prevents some thread races. Strong and unowned variables point at the object.
    Weak variables point at the object's side table.

객체는 처음에는 사이드 테이블을 가지지 않는다. 사이드 테이블이 생성되는 경우는 다음과 같다.

  • 생성 시점
    • Weak reference가 생성될 때
    • Strong RC 또는 unowned RC가 오버플로우할 때 (특히 32-bit에서는 inline RC 크기가 작다.)
    • 객체에 대한 연관된 객체 저장소(associated object storage)가 필요할 때
    • 그 외 (etc)

사이드 테이블 엔트리가 생성되면, 객체는 사이드 테이블을 절대 잃지 않는다.
이는 thread의 race condition를 막는다고 한다 .

Storage layout

우리는 Refcount가 객체 안에서 inline으로 저장되거나 side table entry로 저장된다는 걸 알았다. 각각의 방식이 어떻게 저장되는지 알아보자.

HeapObject

HeapObjec 는 Heap에 저장되는 동적으로 할당되는 객체의 구조를 나타낸 것이다.

다음과 같은 데이터를 포함한다.

  • isa: 타입에 대한 metadata
  • InlineRefCounts: 인라인 참조 카운트 strong RC 와 unowned RC, Flag을 합한 값을 저장 or side table entry의 참조값을 저장
    • weak reference가 발생하면 side table을 생성하므로 weak RC를 제외한 RC들을 합한 값을 저장
    • 참조 카운트가 인라인으로 처리될 수 없는 경우, 이 포인터는 HeapObjectSideTableEntry를 가리키게 된다.
  HeapObject {
    isa
    InlineRefCounts {
      atomic<InlineRefCountBits> {
        strong RC + unowned RC + flags
        OR
        HeapObjectSideTableEntry*
      }
    }
 }

HeapObjectSideTableEntry

객체의 Side Table 구조로, 객체가 인라인 참조 카운트를 넘어서 참조를 관리해야 할 때 사용된다.

다음과 같은 데이터를 포함한다.

  • object pointer: 객체의 포인터
  • atomic<SideTableRefCountBits>: strong RC와 unowned RC, weak RC, flag를 합한 값
HeapObjectSideTableEntry {
    SideTableRefCounts {
      object pointer
      atomic<SideTableRefCountBits> {
        strong RC + unowned RC + weak RC + flags
      }
    }
  }

Swift Object Life Cycle

그럼 이제 객체가 저장되는 방식에 대해서 알아봤으니 객체가 어떤 과정으로 메모리에세 해제되는지 살펴보자.

객체의 life cycle은 아래 그림과 같다. object는 약한 참조가 있느냐 즉 side table 존재 여부에 따라 다른 life cycle을 갖는다. 이에 대해 자세히 알아보자.

이미지 출처: https://shubhamchawla00.medium.com/memory-management-on-ios-ad2244b49b20

1. Live

LIVE without side table (사이드 테이블이 없는 객체)

The object is alive.
Object's refcounts are initialized as 1 strong, 1 unowned, 1 weak.
No side table. No weak RC storage.
Strong variable operations work normally.
Unowned variable operations work normally.
Weak variable load can't happen.
Weak variable store adds the side table, becoming LIVE with side table.
When the strong RC reaches zero deinit() is called and the object
becomes DEINITING.

객체가 side table 없이 존재하는 경우이다. 객체의 RC는 strong 1, unowned 1, weak 1로 초기화된다.

  • 객체를 생성할 때, strong RC가 1로 증가한다.
  • Unowned RC는 Strong Reference에 대해 extra count를 가지므로 0에서 1로 증가한다.
  • Weak RC 또한 Unowned Reference에 대해 extra count를 가지므로 0에서 1로 증가한다.

side table이 존재하지 않으므로 weak RC 저장소가 없다. → side table에서만 weak RC를 저장했었음

Strong과 Unowned 변수 연산이 정상적으로 동작된다.

  • 객체가 side table을 가지지 않은 상태에서 weak reference 변수를 통해 객체를 로드할 수 없다. (접근 불가능)

약한 참조가 발생하면 객체의 상태가 Live with side table로 변합니다. 이후에는 weak reference 변수를 통해 객체에 접근할 수 있다.

strong RC가 0이 되면 deinit이 호출되고 객체는 DEINITING 된다.

LIVE with side table

Weak Reference 변수 작업이 정상적으로 작동하는 것을 제외하고는 LIVE without side table과 동일하다.

2. DEINITING

DEINITING without side table

deinit() is in progress on the object.
Strong variable operations have no effect.
Unowned variable load halts in swift_abortRetainUnowned().
Unowned variable store works normally.
Weak variable load can't happen.
Weak variable store stores nil.
When deinit() completes, the generated code calls swift_deallocObject.
swift_deallocObject calls canBeFreedNow() checking for the fast path
of no weak or unowned references.
If canBeFreedNow() the object is freed and it becomes DEAD.
Otherwise, it decrements the unowned RC and the object becomes DEINITED.

객체에 대해 deinit()가 진행 중인 DEINITING 상태일 때 동작이다.

Strong Reference 변수는 아무런 영향을 받지 않는다.

Unowned 변수 로드는 swift_abortRetainUnowned 때문에 중단되지만 저장 작업은 정상적으로 동작된다.

weak 변수를 통해 객체를 불러올 수 없다 → 당연하다. side table이 없으니

weak 변수에 nil이 저장된다.

deinit()이 완료되면 생성된 코드는 swift_deallocObject를 호출한다.

  • swift_deallocObjectcanBeFreedNow()를 호출하여 weak나 unowned 참조가 있는지 확인한다.
    • weak, unowned가 없다면 canBeFreedNow()가 호출되면 객체는 freed 되고 DEAD 상태가 된다.
  • 그렇지 않으면 unowned RC를 하나 줄이고 객체는 DEINITED 상태가 됩니다.
    • unowned RC는 객체가 deinit()된 이후 1이 감소된다.( unowned RC 참고)

DEINITING with side table

Weak variable load returns nil.
Weak variable store stores nil.
canBeFreedNow() is always false, so it never transitions directly to DEAD.
Everything else is the same as DEINITING.

side table을 가지고 있는 경우 deinit() 진행 중일 때이다. weak 변수에 객체를 저장하거나 불러올 때 모두 nil을 받는다.

side table을 가지고 있다는 것은 추가적인 weak RC가 존재한다는 걸 의미한다. 따라서 canBeFreedNow()는 항상 false 값을 갖으므로 바로 DEAD 상태로 변할 수 없다.

3. DEINITED

DEINITED without side table

deinit() has completed but there are unowned references outstanding.
Strong variable operations can't happen.
Unowned variable store can't happen.
Unowned variable load halts in swift_abortRetainUnowned().
Weak variable operations can't happen.
When the unowned RC reaches zero, the object is freed and it becomes DEAD.

side table이 없는 DEINITED 상태일 때 동작이다.

deinit()가 완료되었지만 unowned reference가 남아 있는 경우이다. (남아있지 않았으면 DEINITING에서 DEAD 상태가 되었을 것)

이미 DEINITING에서 ****Strong RC가 0이 되었으므로 강한 참조에 대한 연산이 일어나지 않는다.

Unowned RC가 비로소 0이 될 때, 객체가 free(완전히 해제)되고 상태는 DEAD가 됩니다.

DEINITED with side table

Weak variable load returns nil.
Weak variable store can't happen.
When the unowned RC reaches zero, the object is freed, the weak RC is
decremented, and the object becomes FREED.
Everything else is the same as DEINITED.

side table이 있는 DEINITED 상태일 때 동작이다.

weak 변수를 통해 객체를 불러올 때 nil을 리턴하며 객체를 저장할 수 없다.

unowned RC가 0이 될 때, 객체는 free(완전히 해제)되고 weak RC도 감소하고 object가 FREED 상태가 된다.

  • weak RC는 객체의 allocation이 freed된 이후 1이 감소된다. ( weak RC 참고)

4. FREED

FREED without side table

This state never happens.

side table이 없는 경우 약한 참조가 존재하지 않으므로 바로 DEAD 상태가 된다.

따라서 절대 일어나지 않는다.

FREED with side table

The object is freed but there are weak refs to the side table outstanding.
Strong variable operations can't happen.
Unowned variable operations can't happen.
Weak variable load returns nil.
Weak variable store can't happen.
When the weak RC reaches zero, the side table entry is freed and
the object becomes DEAD.

side table이 있는 FREED 상태일 때 동작이다.

FREED 상태는 객체는 해제되었는데 여전히 side table에 대한 weak RC가 남아있는 경우이다.

  • Strong, unowned 변수에 대해서 어떠한 연산도 발생할 수 없다.

이때 객체를 weak로 참조하게 되면 nil을 리턴하고 객체를 저장할 수 없다.

weak RC가 0이 되면(모든 RC가 0) side table이 freed 되고 객체는 마침내 DEAD 상태가 된다.

5. DEAD

The object and its side table are gone.

객체와 side table 모두 해제된 상태이다.

마무리

high-level에서 Swift에서 일어나는 Reference Counting 방식을 배울 때는 strong reference의 경우에만 count를 적용한다고 배웠다. 하지만 low-level을 살펴보니 전혀 그렇지 않았다.

Reference가 strong, unowned, weak에 상관없이 count를 저장하고 있었고 만약 weak 참조가 존재한다면 side table를 할당하여 reference count를 관리하고 있었다.

strong RC가 0이 되면 객체는 denit 되지만 unowned reference가 존재한다면 여전히 메모리에 남아있었다. (DENITED 상태)

unowned RC가 비로소 0이 될 때는 객체는 완전 해제되지만 weak RC가 남아있다면 side table이 아직 메모리에 남아있게 된다. (FREED 상태)

Ref.

Discover Side Tables - Weak Reference Management Concept in Swift

Memory management on iOS

https://github.com/swiftlang/swift/blob/main/stdlib/public/SwiftShims/swift/shims/RefCount.h

0개의 댓글