iOS - ARC(Automatic Reference Count)

longlivedrgn·2023년 1월 3일
0

iOS

목록 보기
4/10
post-thumbnail

Automatic Reference Counting

  • ARC는 클래스 인스턴스가 더 이상 필요하지 않을 때 클래스 인스턴스에 할당된 메모리를 자동으로 해제한다.
  • 참조 타입인 클래스 인스턴스에만 적용이된다. → 즉, 영역을 관리한다.

예를 통해서 ARC가 필요한 상황을 이야기해보자, 아래와 같이 Human Class를 정의하고, miro라는 클래스 인스턴스를 생성했다고 하자. 그리고 miro는 지역변수라고 가정!

class Human {
    var name: String?
    var age: Int?
    
    init(name: String?, age: Int?) {
        self.name = name
    }
}
 
let miro = Human(name: "Miro")
  • 그러면 아래와 같이 miro는 지역변수 이므로 스택에 할당이되고, Human 인스턴스는 힙에 할당이된다.

  • 그리고 아래와 같이 클론을 만들어서 주소값을 복사해보자.

  • 그리고 만약 함수 종료 시점에 miro와 clone이 사라지게 된다면 아래와 같은 그림과 같이 될 것이다.

그렇다면 힙에 있는 인스턴스는 누가 메모리 해제를 해줄까? 바로 ARC가 해준다!


ARC in Action

어떠한 방식으로 ARC가 작동하는 알아보자

  • 아래와 같이 Person Class를 정의해보자
class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}
  • 그리고 Person 클래스를 할당한 3개의 변수를 선언해보자. 세개의 변수 다 옵셔널로 지정하였기에 초기값은 전부 다 nil이다.
var reference1: Person?
var reference2: Person?
var reference3: Person?
  • 아래와 같이 Person 인스턴스와 reference1은 강한 참조를 하게 되고, reference2와 reference3에도 할당이되면서 2개의 강한 참조가 추가로 카운팅이 됩니다. 따라서 최종적으로 reference counting은 3입니다.
reference1 = Person(name: "John Appleseed") // reference count : 1
// Prints "John Appleseed is being initialized"
reference2 = reference1 // reference count : 2
reference3 = reference1 // reference count : 3
  • 그리고 이제 할당 해제를 해보겠습니다. reference1과 reference2에 nil을 할당하여 강한 참조 두개를 제거할 수 있습니다. 그러나 아직 한개의 강한 참조가 남아있기에 deinit이 실행되지는 않습니다.
reference1 = nil // reference count : 2
reference2 = nil // reference count : 1
  • 최종적으로 할당 해제를 진행해주면 참조 카운트가 0이 되면서 메모리에서 해제가된다.
reference3 = nil // reference count : 0
// Prints "John Appleseed is being deinitialized"

이를 통해서 ARC는 인스턴스를 생성하면 참조 카운트를 추적하고 더 이상 필요하지 않을 때(참조 카운트 = 0)일 때에 인스턴스를 메모리에서 해제한다는 것을 알 수 있었습니다. → By ARC


Strong Reference Cycles Between Class Instances(강한 순한 참조)

위와 같은 강한 참조 시 참조를 한 인스턴스가 해제되었음에도 계속해서 참조를 유지하는 문제가 발생하는데, 아래의 코드를 통해서 해당 문제를 알아보자.

  • 아래와 같은 class 두 개를 정의해주자.
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment? // 옵셔널로 초깃값은 nil
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person? // 옵셔널로 초깃값은 nil
    deinit { print("Apartment \(unit) is being deinitialized") }
}
  • 아래와 같이 클래스 인스턴스를 만들어보자.
var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed") // Person reference count : 1
unit4A = Apartment(unit: "4A") // Apartment reference count : 1
  • 그러면 아래의 그림과 같은 강한 참조 그림을 그릴 수 있습니다.
  • 그러면 각자의 프로퍼티에 클래스 인스턴스를 한번 할당 해보자. 그러면 Apartment의 RC는 2가 되고, Person의 RC는 2가됩니다.
john!.apartment = unit4A // Apartment reference count : 2
unit4A!.tenant = john // Person reference count : 2
  • 그러면 아래와 같은 그림으로 강한 참조를 나타낼 수 있습니다.

  • 그리고 난 후, 변수에 데이터를 할당 해제하게 되면, 강한 참조가 사라지게 되면서 참조 카운트는 1이 됩니다.

john = nil // Person reference count : 1
unit4A = nil // Apartment reference count : 1

→ 즉, Person instance와 Apartment 인스턴스 사이의 강한 참조가 유지되면서 불필요한 데이터가 메모리 상에서 해제되지 않고 유지가 되는 문제가 발생합니다. 즉 서로 instance끼리 참조를 하고 있어 RC가 0이 되지 않은 것이다!

‼️ Person 객체와 Apartment 객체가 몇번이나 할당이 되었는 지를 통하여 RC를 계산한다!


Resolving Strong Reference Cycles Between Class Instances

weak(약한 참조)

  • 인스턴스를 참조할 시에 RC를 증가시키지 않는다.
  • 인스턴스가 메모리에서 해제가 된 경우에 자동으로 nil을 할당하여 메모리가 해제가된다.
  • 무조건 옵셔널 타입의 변수여야한다.

→ 아래와 같이 순한 참조를 일으키는 프로퍼티 앞에 weak를 붙혀주면 된다.

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment? // 옵셔널로 초깃값은 nil
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person? // 옵셔널로 초깃값은 nil
    deinit { print("Apartment \(unit) is being deinitialized") }
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed") // Person reference count : 1
unit4A = Apartment(unit: "4A") // Apartment reference count : 1
  • 그리고, 아래와 같이 할당을 해주면 weak의 경우 약한 참조를 하므로 RC가 증가하지 않는다.
john!.apartment = unit4A // Apartment reference count : 2
unit4A!.tenant = john // Person reference count : 1

  • 따라서 만약 아래와 같이 nil을 할당할 경우 Person의 RC는 0이 되어 바로 메모리를 해제해버리고, Apartment의 경우 RC가 1이지만 참조하고 있는 Person 메모리 해제되므로 최종적으로 RC가 0이된다.
john = nil // Person reference count : 0, Apartment 4A is being deinitialized
unit4A = nil // Apartment reference count : 0, John Appleseed is being deinitialized

unowned(미소유 참조)

  • 인스턴스를 참조할 시에 RC를 증가시키지 않는다.
  • 인스턴스를 참조하는 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신 → 참조하던 인스턴스가 메모리에서 해제된 경우, nil을 할당받지 못하고 해제된 메모리 주소값을 계속해서 들고 있는다.

→ unowned로 선언된 변수가 가르키던 인스턴스가 메모리에서 먼저 해제가 된 경우, 해당 변수에 접근을 하려고 하면 에러가 발생하게 된다.

  • 그리고, weak는 런타임에 nil이 될 수 있기에 optional로 선언되어야하고, var로 선언되어야한다.

  • 그러나, unowned의 경우 위에서 설명한 바와 같이 참조 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신하기에 옵셔널로 선언하지 않는다!

  • 아래와 같이 weak의 경우 인스턴스를 nil로 할당할 수 있다.

  • 그러나, unowned의 경우 weak와는 다르게 unowned로 설정된 값을 nil로 설정하지 않는다!(계속 메모리 값을 가지고 있어서 접근하면 error가 난다!)

  • 이 또한, 예를 통해서 알아보자.

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") }
}

var john: Customer?

john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
  • 아래와 같이 Customer instance의 RC는 1, CreditCard instance의 RC은 1이다.

  • 그리고 아래와 같이 nil을 할당을 해주자.

john = nil
  • 그러면 아래와 같이 Customer instance의 RC는 0이 되고(메모리에서 해제가 된다), CreditCard instance의 RC도 0이 된다.

  • 만약 위의 코드 말고 weak와 동일한 코드에서 weak와 unowned를 가정하고 john에게 nil을 할당하고 unit4A.tenant에 접근한다고 가정해보자.


// **weak**
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment? // 옵셔널로 초깃값은 nil
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person? // 옵셔널로 초깃값은 nil
    deinit { print("Apartment \(unit) is being deinitialized") }
}

// **unowned**
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment? // 옵셔널로 초깃값은 nil
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    unowned let 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

john = nil
  • 그리고 john에게 nil을 할당한다. 그리고 unit4A의 tenant에 접근해보면?
unit4A.tenant
  1. weak일 경우 -> nil
  2. unowned일 경우 -> error

참고자료

https://babbab2.tistory.com/27
https://zeddios.tistory.com/1213

0개의 댓글