[Swift] Automatic Reference Counting

Ben·2024년 6월 13일
0

iOS

목록 보기
15/23

How ARC works

ARC는 클래스 인스턴스가 생성될 때 메모리를 할당하고, 인스턴스가 더 이상 필요하지 않을 때 할당된 메모리를 해제한다. 인스턴스를 확고하게 유지하고 강력한 참조가 유지되는 한 ARC가 할당 해제를 하지 않기 때문에 이를 '강한 참조 (Strong Reference)' 라고 한다.


ARC in Action

reference1 변수에 Person 인스턴스를 생성하여 할당하면 Heap에 생성된 Person 인스턴스의 메모리 주소값을 갖게된다. (즉, 참조가 발생된다.)

그리고 reference2reference3reference1을 할당하면 똑같은 Person 인스턴스를 참조하게 됨으로써, 총 RC (reference count)가 3이 된다.

// There’s now a `strong reference` from reference1 to the new Person instance.
reference1 = Person(name: "John Appleseed")
// If you assign the same Person instance to two more variables, 
// `two more strong references` to that instance are established:
reference2 = reference1
reference3 = reference1
// MARK: - Address of <Person Instance> (Heap)
print("Address of <Person Instance>: \(Unmanaged.passUnretained(reference1!).toOpaque())")
print("Address of <Person Instance>: \(Unmanaged.passUnretained(reference2!).toOpaque())")
print("Address of <Person Instance>: \(Unmanaged.passUnretained(reference3!).toOpaque())")

reference1, reference2, reference3는 모두 Heap에 생성된 Person 인스턴스의 같은 메모리 주소값을 가지고 있다 (= '참조한다')

reference1 = nil
reference2 = nil
reference3 = nil

3개의 강한 참조를 모두 nil로 할당하면 Person Instance의 RC가 0이 되고 deinit()이 호출되어 ARC에 의해 인스턴스가 메모리에서 자동으로 할당 해제된다.


Strong Reference Cycles Between Class Instances

john!.apartamento = unit4A
unit4A!.tenant = john

두 인스턴스에 서로를 함께 연결 (참조)하면 서로간의 '강한 참조 사이클 (strong reference cycles)' 을 생성한다. 즉, 순환 참조 (circular references)가 발생한다.

print("Address of <Persona Instance>: \(Unmanaged.passUnretained(john!).toOpaque())")
print("Address of <Apartamento Instance>: \(Unmanaged.passUnretained(unit4A!).toOpaque()) \n")
print("Address of <Persona Instance in Apartamento Instance>: \(Unmanaged.passUnretained(john!.apartamento!).toOpaque())")
print("Address of <Apartamento Instance in Persona Instance>: \(Unmanaged.passUnretained(unit4A!.tenant!).toOpaque())")

//	Unfortunately, linking these two instances creates a strong reference cycle between them.
//	The Persona instance now has a strong reference to the Apartamento instance, 
//	and the Apartamento instance has a strong reference to the Persona instance.
//	Therefore, when you break the strong references held by the john and unit4A variables, 
//	the reference counts don’t drop to zero, 
//	and the instances aren’t deallocated by ARC:
john!.apartamento = unit4A
unit4A!.tenant = john

john!.apartamento!unit4A!.tenant!서로 강한 참조하고 있음을 알 수 있다.

john = nil
unit4A = nil

johnunit4A 변수를 nil로 설정하게 되면, 변수와 인스턴스간의 관계가 끊어진 것이지 아직 heap에서는 여전히 강한 참조가 일어나고 있어 각 클래스 인스턴스의 deinit()이 호출되지 않는다.

두 변수가 nil로 설정이 되었기에 접근 또한 불가능하며, heap에서 강한 순환 참조가 발생되고 있기에 '메모리 누수 (leaks)'가 발생한다.

// The strong reference cycle prevents the Persona and Apartamento instances from ever being deallocated, causing a memory leak in your app.
// The strong references between the Persona instance and the Apartamento instance remain and can’t be broken.
john = nil
unit4A = nil
john!.apartamento = nil
unit4A!.tenant = nil

john = nil
unit4A = nil

각 클래스 인스턴스의 RC가 0이 되어 완전한 인스턴스 할당 해제를 하기 위해서는 johnunit4A가 nil이 되기 전에 apartamentotenant를 수동으로 끊어줘야 한다.

더 이상 참조되는 것이 없기 때문에 RC가 0이 되고 deinit()이 호출되어 인스턴스 할당이 해제된다.


Resolving Strong Reference Cycles Between Class Instances

강한 참조 사이클 (= 순환참조)를 해결하기 위해 '약한 참조 (weak references)''미소유 참조 (unowned references)' 2가지 방법이 존재한다.

이 키워드를 사용하면 참조 사이클의 한 인스턴스가 강한 참조 없이 다른 인스턴스를 참조할 수 있다.

• 약한 참조 (weak references)

약한 참조는 참조하는 인스턴스를 강하게 유지하지 않는 참조로써, 약한 참조가 참조하는 동안 해당 인스턴스 (= 참조를 당하는 인스턴스)가 할당 해제될 수 있다.

Apartamento 클래스의 tenant 프로퍼티는 weak키워드를 사용함으로써, 약한 참조로 선언한다.
unit4Atenant가 어느 순간에 없을수도 있기에 즉, nil이 될 가능성이 있으므로 (tenant가 먼저 할당 해제 될 수 있음) tenantweak로 선언한다.

💡 약한 참조는 '다른 인스턴스의 수명이 더 짧은 경우 즉, 다른 인스턴스가 먼저 할당 해제될 수 있을 때 사용' 한다!

메모리 그래프를 통한 각 인스턴스의 관계는 다음과 같다.

Persona의 apartamento 프로퍼티는 굵은 실선으로 Apartamento를 Strong Ref하고 있고, Apartamento의 tenant 프로퍼티는 weak로 선언되었기에 Weak Ref함으로써 (굵은 실선으로) Persona를 가리키고 있지 않다.

  • Apartamento를 참조하는 객체

  • Apartamento에 의해 참조되는 객체

    💡 SwiftWeakRefStorage는 'Swift에서 약한 참조를 관리하기 위한 내부 구조체' 라고 한다.

  • Persona를 참조하는 객체

  • Persona에 의해 참조되는 객체

    _refcountsSwiftWeakRefStorage 타입인 것으로 보아,
    아마 Persona 인스턴스가 Apartamento 인스턴스를 프로퍼티로 가지고 있어 출력된 것 같다(?)

Persona 인스턴스를 참조하는 것은 john 변수뿐이고 Apartamento 인스턴스를 참조하는 것은 unit4A 변수와 john!.apartamento 이므로,

  • Reference Count of <Persona instance>: 1
  • Reference Count of <Apartamento instance>: 2

가 된다.

💡 즉, weakRC를 증가시키지 않음!

//	Because there are no more strong references to the Persona instance, 
//	it’s deallocated and the tenant property is set to nil:
john = nil

johnnil로 설정하면서 Persona 인스턴스의 RC가 0이 되면서 deinit()이 호출되고 인스턴스 할당 해제가 이루어진다.

💡 ARC는 (weak로 선언된 프로퍼티를) 참조하는 인스턴스가 할당 해제되면, nil로 약한 참조를 자동으로 설정한다

  • weak는 runtime에 값을 nil로 변경하는 것을 허락해야 하므로 '항상 옵셔널 타입의 변수로 선언' 한다!

Persona의 apartamento가 더 이상 Apartamento 인스턴스를 참조하지 않으며, 유일한 (강한)참조는 unit4A이다.

unit4A = nil

unit4A 변수마저 nil로 설정하면, RC가 0이 되면서 deinit()이 호출되고 Apartamento 인스턴스가 할당 해제된다.


• 미소유 참조 (unowned references)

미소유 참조는 약한 참조와 동일하게 RC를 증가시키지 않는다.

CreditCard 클래스의 customer 프로퍼티는 unowned 키워드를 사용함으로써, 미소유 참조로 선언한다.
Customer는 card가 없을수도 있지만, CreditCard는 customer가 없을수가 없다.

💡 weak와 반대로 unowned는 '다른 인스턴스의 수명이 동일하거나 더 긴 경우에 사용' 한다!

위와 같은 인스턴스의 이해관계로 미소유 참조는 항상 값을 갖도록 '예상' 한다.

//  항상 초기값을 가질 것으로 예상함, RefCnt를 증가시키지 않음!
unowned let customer: Customer

init(number: UInt64, customer: Customer) {
    self.number = number
    self.customer = customer
}

결과적으로 미소유로 만들어진 값은 '옵셔널로 만들어지지 않고, ARC는 미소유 참조 값을 nil로 설정하지 않음!'

💡 [IMPORTANT]

  • 항상 할당 해제되지 않는 인스턴스를 참조한다고 확신하는 경우에만 사용
  • 인스턴스가 할당 해제된 후에 미소유 참조값에 접근하려고 하면 runtime error가 발생!

  • Customer를 참조하는 객체

  • Customer에 의해 참조되는 객체

  • CreditCard를 참조하는 객체

  • CreditCard에 의해 참조되는 객체

    customerunowned 키워드로 선언되어 있어 RC가 0인 것을 알 수 있다.

Customer 인스턴스를 참조하는 것은 john 변수뿐이고 CreditCard 인스턴스를 참조하는 것은 john!.card 이므로,

  • Reference Count of <Customer instance>: 1
  • Reference Count of <CreditCard instance>: 1

이 된다.

💡 unowned도 마찬가지로 RC를 증가시키지 않음!

john = nil

johnnil로 설정하면, RC가 0이 되면서 deinit()이 호출되고 Customer 인스턴스가 할당 해제되며, CreditCard 인스턴스 역시 할당 해제가 된다.

위의 경우에는 CreditCard의 인스턴스가 john!.card로 종속적으로 생성되었지만, 만약 아래의 경우처럼 구현하게 되면 runtime error가 발생한다.

john 변수가 nil이 된 상태에서 독립적으로 생성된 card <CreditCard Instance>customer에 접근하려 하면 '이미 메모리에서 해제된 포인터 (dangling pointer) 값에 접근하려 해서 에러가 발생한다.'

💡 weak vs unowned

  • weak: 자동으로 'nil 처리'가 된다.
  • unowned: '항상 할당 해제되지 않는 인스턴스를 참조한다고 확신하는 경우' 에만 사용해야하기 때문에 'nil 처리가 되지 않는다'

• 미소유 옵셔널 참조 (Unowned Optional References)

weak와 unowned의 가장 큰 차이점 중 하나는 optional의 가능 여부였지만,
Swift 5.0부터 unowned가 optional을 지원함으로써 이제는 틀린 말이 되었다.

nextCourseunowned optional로 선언되어 있어 Coure의 다음 Course가 있을 수도 있고 없을 수도 있다.

즉, 참조 대상의 존재가 불확실 하기 때문이다.

Course가 1개 이상일 때, 논리적인 관점에서 nextCourse는 말 그대로 후행과목이기 때문에 수명이 더 길거나, advanced와 같이 다음 과목이 없어 nil로써 수명이 동일한 상황임을 보장하여 이를 '명시적' 으로 표현하기 위해 Optional로 선언한다.

즉, unowned로 참조하고 있는 인스턴스가 절대로 먼저 메모리에서 해제될 일이 없다는 것을 '의도' 로 나타낼 수 있다.

만약, department.course 중 하나가 삭제될 수 있는 경우, 다른 Course에 있을 수 있는 모든 참조를 삭제해야한다.

예를들어 courses에서 'intermediate가 삭제된다' 라고 가정했을 때,

strong하게 참조된 intermediatedepartment.courses[1]nil 처리 하게 된다면 intro.nextCourse에서 dangling pointer error가 발생한다.

반드시 intro.nextCourse 또한 nil로 설정해야한다.

unowned가 optional을 지원하면서 weak와 사용법적 측면에서는 nil을 명시해줄 수 있는가 없는가의 차이로 바뀐것 같지만, 성능적인 면에서 차이를 보인다.

참조: weak vs unowned


• <Deep Dive - Linked-List>

Course 인스턴스가 삭제될 경우, 참조 관계 정리를 위해 removeCourse(for:) 메서드를 추가했다.
(-> 즉, 삭제하려는 코스를 가리키고 있는 다른 코스들이 있을 경우 이들의 nextCoursenil로 설정하여 코스 간의 연결 관계를 정리한다.)

💡 Department - Course의 다이어그램을 통해 courses'단방향 연결리스트 (Singly Linked-List)' 임을 알 수 있다.

  • intro를 삭제할 경우

  • intermediate를 삭제할 경우

  • advanced를 삭제할 경우


• 미소유 참조와 암묵적 언래핑된 옵셔널 프로퍼티 (Unowned References and Implicitly Unwrapped Optional Properties)

  • Persona - Apartamento 예제: 둘 다 nil이 될 수 있는 프로퍼티가 강한 참조 사이클을 유발할 수 있는 가능성이 있는 상황이라 weak로 해결

  • Customer - CreditCard 예제: nil이 허용되는 프로퍼티와 non-nil인 프로퍼티가 강한 참조 사이클을 유발할 수 있는 가능성이 있는 상황이라 unowned로 해결

위의 두 케이스 이외에 두 프로퍼티 모두 값이 존재하고 초기화가 완료되면 non-nil인 케이스도 존재한다.

Country와 City는 서로 종속관계로, 모든 국가는 수도를 가지고 있고 모든 도시는 항상 국가에 종속되어야 한다.

self.capitalCity = City(name: capitalName, country: self)

Two-Phase Initialization (2단계 초기화) 에 의해 Country 인스턴스가 완벽히 초기화 될 때까지 City의 countryself를 전달할 수 없다.

이 요구사항을 처리하기 capitalCity암시적 언래핑된 옵셔널 (Implicitly Unwrapped Optionals) 프로퍼티로 선언한다.

암시적 언래핑된 옵셔널로 선언된 capitalCity는 기본적으로 nil 값을 가지므로 새로운 Country 인스턴스는 초기화 메서드 내에서 name을 설정하는 즉시 새로운 Country 인스턴스는 완벽히 초기화 된 것으로 '간주'한다.

💡 즉, Country 인스턴스의 초기화 메서드는 name이 설정되는 즉시 암시적 self를 참조하고 전달할 수 있다.

따라서 Country의 초기화 메서드는 capitalCity를 설정할 때 City의 초기화 메서드에 대한 하나의 파라미터로 self를 전달할 수 있다.

강한 참조 사이클을 만들지 않고 단일 구문을 Country와 City 인스턴스를 생성하고 capitalCity는 옵셔널 값을 언래핑 하기위해 !를 사용할 필요없이 직접 접근할 수 있다.

var country: Country = Country.init(name: "Canada", capitalName: "Ottawa")

print("\(country.name)'s capital city is called \(country.capitalCity.name)")

capitalCity는 초기화가 완료되면 Optional이 아닌 값처럼 사용되고 접근할 수 있지만 강한 참조 사이클은 피할수 있다.

주의해야 할 점은, 암시적 언래핑된 옵셔널이라고 하더라도, 값이 nil일 가능성이 있는 경우에 접근하면 runtime 에러가 발생한다.


Strong Reference Cycles for Closures

강한 참조 사이클은 클래스 인스턴스의 프로퍼티에 클로저를 할당하고 해당 클로저 내에서 self 키워드를 통해 인스턴스의 프로퍼티에 접근하는 경우에도 발생할 수 있다.

💡 Closures 또한 First-class citizen (1급 객체) 로 취급되어, heap에 저장되어지는 참조 타입으로써 "캡처 (capture)" 에 의한 순환 참조이다.

클로저를 가진 asHTML 지연 프로퍼티가 HTMLElement 인스턴스를 strong 참조하고 있다.

asHTML의 클로저 안에서 self를 통해 클래스 인스턴스의 프로퍼티에 접근했기에, paragraph.asHTML()가 실행될 때 RefCnt가 증가한다.

paragraph = nil

paragraphnil을 할당하여 인스턴스를 해제하면, 여전히 asHTML에 의해 강한 참조가 남게되고 HTMLElment의 deini() 메서드는 호출되지 않아 Memory Leaks가 발생한다.


Resolving Strong Reference Cycles for Closures

클로저 내에서 발생하는 순환 참조 문제를 클로저 정의부에 '캡처 리스트 (capture list)' 를 사용하여 해결할 수 있다.

💡 캡처 리스트는 하나 이상의 참조 타입을 캡처할 때 사용할 규칙을 정의하며, 각 참조를 강한 참조가 아닌 약한 참조 또는 미소유 참조로 선언한다.

• 약한 참조와 미소유 참조 (Weak and Unowned References)

클로저와 캡처한 인스턴스가 항상 서로를 참조하고 항상 같은 시간에 할당 해제될 때, 캡처를 미소유 참조로 정의한다.

반대로 캡처된 참조가 향후 nil이 될 때는 약한 참조로 캡처를 정의한다.

💡 캡처된 참조가 nil이 되지 않으면 약한 참조보다는 '항상' 미소유 참조로 캡처가 되어야한다.

paragraphasHTML()이 호출되고 난 뒤, 메모리 그래프는 아래와 같다.

  • HTMLElement가 참조하는 객체

  • HTMLElement에 의해 참조되는 객체

    동어 반복: Closures 또한 First-class citizen (1급 객체) 로 취급되어, heap에 저장되어지는 참조 타입이다.

  • Closure의 scope 내에서 참조되는 객체

    [unowned self] 로 캡처를 미소유 참조로 선언했기에, Owning references가 0이다.

LLDB에서 참조된 객체의 정보를 확인

paragraph = nil

paragraphnil을 할당하면 deinit()이 호출되고 HTMLElement 인스턴스는 할당 해제된다.

profile
 iOS Developer

0개의 댓글