Swift

[Swift] ARC (1)

thoonk: 2021. 5. 12. 23:33
반응형

ARC에 관한 내용을 정리한 것을 기록합니다.

ARC(Automatic Reference Counting)

  • 참조 타입은 하나의 인스턴스가 참조를 통해 여러 곳에서 접근하기 때문에 언제 메모리에서 해제되는지가 중요하다.
    why ? 인스턴스가 적절한 시점에 해제되지 않으면 한정적인 메모리 자원을 낭비하게 되고 성능 저하로 이어지게 된다. → 그래서 스위프트는 프로그램의 메모리 사용을 관리하기 위해 메모리 사용을 추적하고 관리하는 ARC를 사용한다.
  • 참조 횟수 계산은 클래스 타입에만 적용된다.
  • 컴파일 시 인스턴스를 메모리에서 해제되는 시점을 예측할 수 있다.→ 인스턴스를 메모리에 유지시키려면 명분이 필요하다. → 인스턴스가 더 이상 필요없다고 판단하면 자동으로 인스턴스의 메모리를 해제한다. 그리고 인스턴스에 접근하려고 하면 앱 크래쉬가 발생한다.

강한 참조 (Strong Reference)

  • 인스턴스를 메모리에 유지시키는 명분을 만들어 주는 것이다.
  • 강한 참조를 사용하면 참조 횟수가 1이 증가함, 반대로 nil을 할당하면 참조 횟수가 1이 감소한다.
  • 기본적으로 프로퍼티, 변수, 상수 등을 선언할 때 별도의 식별자가 없으면 모두 강한 참조로 선언한 것이다.
  • 함수 내에 선언된 강한 참조한 지역 변수의 경우 함수가 종료되면 메모리가 해제되지만 전역 변수로 강한 참조한 경우 메모리가 해제되지 않는다.

강한 참조 순환 문제

  • 인스턴스끼리 서로를 강한참조 하고 있는 것 → 메모리 누수가 발생한다.
class Person {
    var name: String?
    var pet: Pet?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

class Pet {
    var name: String?
    var owner: Person?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) is  being deinitialized")
    }
}

// Person 인스턴스 참조 횟수: 1
var thoonk: Person? = Person(name: "thoonk")
// Pet 인스턴스 참조 횟수: 1
var rocky: Pet? = Pet(name: "rocky")
// Person 인스턴스 참조 횟수: 2
thoonk?.pet = rocky
// Pet 인스턴스 참조 횟수: 2
rocky?.owner = thoonk

// Person 인스턴스 참조 횟수: 1
thoonk = nil
// Pet 인스턴스 참조 횟수: 1
rocky = nil

위 코드에서 thoonk와 rocky가 초기화되면서 메모리에 강한참조로 할당된다.
그리고 Person의 pet 프로퍼티에 rocky가 할당되면서 강한 참조가 1 증가한다.
마찬가지로, Pet의 owner 프로퍼티에 thoonk가 할당되면서 강함 참조 횟수가 1 증가한다.

마지막에 nil로 각 인스턴스를 할당 해제하지만 서로의 프로퍼티가 강한참조하고 있어 메모리 해제가 되지 않은 상황이다.

thoonk?.pet = nil
rocky?.owner = nil 

위 코드처럼 직접 메모리를 해제시킬 수 있지만 번거롭다.

강한 참조 문제를 해결할 수 있는 방법에는 약한 참조와 미소유 참조 두 가지가 있다.

약한 참조 (Weak Reference)

  • 참조하는 인스턴스의 참조 횟수를 증가시키지 않는다.
  • weak 키워드를 프로퍼티나 변수의 선언 앞에 써주면 약한 참조를 나타낸다.
  • 약한 참조를 사용할 때 참조하는 인스턴스가 메모리에서 해제될 수도 있다는 것을 주의해야 한다.
  • 약한 참조는 상수가 아닌 변수에 사용하고 항상 옵셔널이어야 한다. why? 약한 참조하던 인스턴스가 메모리에서 해제되면 nil로 변경될 수 있다.
class Person {
    var name: String
    var pet: Pet?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

class Pet {
    var name: String
    weak var owner: Person?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) is  being deinitialized")
    }
}

// Person 인스턴스 참조 횟수: 1
var thoonk: Person? = Person(name: "thoonk")
// Pet 인스턴스 참조 횟수: 1
var rocky: Pet? = Pet(name: "rocky")

// Person 인스턴스 참조 횟수: 1
rocky?.owner = thoonk
// Pet 인스턴스 참조 횟수: 2
thoonk?.pet = rocky

// Person 인스턴스 참조 횟수: 0, Pet 인스턴스 참조 횟수: 1
thoonk = nil
// Pet 인스턴스 참조 횟수: 0
rocky = nil
// thoonk is being deinitialized
// rocky is  being deinitialized

위 코드는 Pet의 owner 프로퍼티에 weak를 붙여 강한 참조 순환을 끊어서 강한참조 순환 문제를 해결하는 예시를 보여준다.

rocky가 참조하는 인스턴스의 owner 프로퍼티가 약한 참조를 하므로 참조 횟수가 증가하지 않는다.

thoonk가 메모리에서 해제되면서 Person의 pet 프로퍼티가 강한 참조하던 인스턴스의 참조 횟수를 감소시키는 것을 알 수 있다.
why? 약한 참조를 통해 강한 참조 순환을 끊어서 횟수가 감소한다.

마지막으로 rocky = nil로 인스턴스를 메모리에서 해제하면서 더 이상 참조하는 곳이 없어 횟수가 0이 된다.

미소유 참조 (Unowned Reference)

  • 약한 참조와 마찬가지로 참조 횟수를 증가시키지 않는다.
  • unowned 키워드를 프로퍼티나 변수의 선언 앞에 써주면 미소유 참조를 나타낸다.
  • 약한 참조와 다르게 인스턴스가 항상 메모리에 존재함(값이 있음)을 전제한다. → 옵셔널이나 변수가 아니어도 된다.
  • 미소유 참조를 사용하면서도 메모리에서 해제된 인스턴스를 접근하려 하면 런타임 에러가 발생하여 앱 크래쉬가 발생한다. → 미소유 참조는 참조하는 인스턴스가 메모리에서 해제되지 않을 것을 확신할 때 사용해야 한다.
class Person {
    var name: String
    var passport: Passport?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

class Passport {
    let number: Int
    unowned let owner: Person

    init(number: Int, owner: Person) {
        self.number = number
        self.owner = owner
    }

    deinit {
        print("\(number) is  being deinitialized")
    }
}

// Person 인스턴스 참조 횟수: 1
var thoonk: Person? = Person(name: "thoonk")
// Passport 인스턴스 참조 횟수: 1
thoonk!.passport = Passport(number: 1234, owner: thoonk!)

// Person 인스턴스 참조 횟수: 0
// Passport 인스턴스 참조 횟수: 0
thoonk = nil
// thoonk is being deinitialized
// 1234 is  being deinitialized

위 코드에서 여권의 소유자는 항상 존재하기 때문에 unowned를 사용했다.

이는 Person 인스턴스가 메모리에 존재하는 한 Passport의 owner 또한 값이 존재한다는 의미이다.

thoonk 인스턴스의 passport 프로퍼티에 새로운 Passport를 할당하면 Passport 인스턴스 참조 횟수가 1 증가한다.하지만 owner 프로퍼티에 미소유 참조되는 Person 인스턴스의 참조 횟수는 증가하지 않는다.

이를 통해 강한 참조 순환 문제를 해결할 수 있는 것을 볼 수 있다.

그리고 thoonk를 nil로 할당하면 passport 프로퍼티에 강한 참조하던 passport 또한 메모리에서 해제된다.

 

내용이 길어져서 다음 글로 이어갑니다.
부족한점 피드백해주시면 감사합니다👍

 

참고: 

스위프트 프로그래밍(3판): 객체지향, 함수형, 프로토콜 지향 패러다임까지 한 번에!(Swift 5)

minsone.github.io/mac/ios/swift-automatic-reference-counting-summary

반응형