[ARC] Automatic Reference Counting 자동 참조 카운팅 (번역)

Posted by Sung Kyungmo on September 2, 2019

Swift는 ARC ( Automatic Reference Counting )를 사용하여 앱의 메모리 사용량을 추적하고 관리합니다. 대부분의 경우 이는 Swift에서 메모리 관리가“작동하는 것”이므로 메모리 관리에 대해 스스로 생각할 필요는 없습니다. ARC는 해당 인스턴스가 더 이상 필요하지 않을 때 클래스 인스턴스가 사용하는 메모리를 자동으로 해제합니다.

그러나 경우에 따라 메모리를 관리하기 위해 ARC에 코드 부분 간의 관계에 대한 자세한 정보가 필요합니다. 이 장에서는 이러한 상황에 대해 설명하고 ARC가 모든 앱 메모리를 관리하는 방법을 보여줍니다. Swift에서 ARC를 사용하는 것은 Objective-C와 함께 ARC를 사용하기 위한 Transitioning to ARC Release Notes에 기술된 접근방식과 매우 유사합니다.

참조 카운팅은 클래스 인스턴스에만 적용됩니다. 구조와 열거는 참조 유형이 아닌 값 유형이며 참조로 저장 및 전달되지 않습니다.

ARC 작동 방식

클래스의 새 인스턴스를 만들 때마다 ARC는 해당 인스턴스에 대한 정보를 저장하기 위해 메모리 덩어리를 할당합니다. 이 메모리에는 해당 인스턴스와 관련된 저장된 속성 값과 함께 인스턴스 유형에 대한 정보가 있습니다.

또한 인스턴스가 더 이상 필요하지 않은 경우 ARC는 해당 인스턴스가 사용하는 메모리를 비워서 다른 용도로 메모리를 사용할 수 있습니다. 이렇게하면 클래스 인스턴스가 더 이상 필요하지 않을 때 메모리에서 공간을 차지하지 않습니다.

그러나 ARC가 여전히 사용중인 인스턴스를 할당 해제하면 더 이상 해당 인스턴스의 속성에 액세스하거나 해당 인스턴스의 메서드를 호출 할 수 없습니다. 실제로 인스턴스에 액세스하려고하면 앱이 중단 될 가능성이 높습니다.

인스턴스가 여전히 필요한 동안 사라지지 않도록 ARC는 현재 각 클래스 인스턴스를 참조하는 속성, 상수 및 변수의 수를 추적합니다. ARC는 해당 인스턴스에 대한 하나 이상의 활성 참조가 여전히 존재하는 한 인스턴스 할당을 해제하지 않습니다.

이를 가능하게하기 위해 클래스 인스턴스를 속성, 상수 또는 변수에 할당 할 때마다 해당 속성, 상수 또는 변수가 인스턴스를 강력하게 참조 합니다. strong 참조라고하며, 이 인스턴스는 해당 인스턴스를 확실하게 유지하고 강한 참조가 남아있는 한 할당을 해제 할 수 없습니다.

ARC in Action

다음은 자동 참조 카운팅 작동 방식의 예입니다. 이 예는 name이라는 저장된 상수 속성을 정의하는 Person이라는 간단한 클래스로 시작합니다.

1
2
3
4
5
6
7
8
9
10
class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\\(name) is being initialized")
    }
    deinit {
        print("\\(name) is being deinitialized")
    }
}

Person클래스에는 인스턴스의 name속성 을 설정하고 초기화가 진행 중임을 나타내는 메시지를 출력하는 이니셜 라이저가 있습니다. 이 Person클래스에는 클래스의 인스턴스가 할당 해제 될 때 메시지를 출력하는 초기화 해제 기능도 있습니다.

다음 코드는 세 가지 변수를 정의하며 Person?,이 변수는 Person 다음 코드에서 새 인스턴스에 대한 다중 참조를 설정하는 데 사용됩니다 . 이러한 변수는 옵셔널 유형 ( Person?, not Person)이므로 변수들은 자동적으로 nil로 초기화가 되며, 지금은 Person 인스턴스를 참조하지 않습니다.

1
2
3
var reference1: Person?
var reference2: Person?
var reference3: Person?

이제 새 Person인스턴스를 만들어 다음 세 가지 변수 중 하나에 할당 할 수 있습니다 .

1
2
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"

메시지 는 클래스의 이니셜 라이저 를 호출 할 때 출력됩니다 . 초기화가 완료되었음을 확인합니다."John Appleseed is being initialized"Person

Person인스턴스가 reference1변수 에 지정되었으므로 이제 reference1Person인스턴스에 대한 강한 참조가 있습니다 . 최소한 하나의 강한 참조가 있기 때문에 ARC는 이것이 Person메모리에 유지되고 할당 해제되지 않도록합니다.

동일한 Person인스턴스를 두 개의 변수에 더 할당하면 해당 인스턴스에 대한 두 개의 강한 참조가 설정됩니다.

1
2
reference2 = reference1
reference3 = reference1

이제 단일 Person 인스턴스에 대한 세 가지 강한 참조가 돼있습니다.

두 개의 변수 에 nil을 할당하여 이러한 강한 참조 중 두 개 (원본 참조 포함)를 분리 하면 하나의 강한 참조가 유지되고 Person인스턴스가 할당 해제되지 않습니다.

1
2
reference1 = nil
reference2 = nil

ARC는 Person세 번째이자 강한 참조가 깨질 때까지 인스턴스 할당을 해제하지 않습니다 .이 시점에서 더 이상 Person인스턴스를 사용하지 않는 것이 분명 합니다.

1
2
reference3 = nil
// Prints "John Appleseed is being deinitialized"

클래스 인스턴스 간의 강한 순환 참조

위의 예에서 ARC는 Person생성 한 새 인스턴스 에 대한 참조 카운팅를 추적하고 Person더 이상 필요하지 않은 경우 해당 인스턴스 를 할당 해제 할 수 있습니다.

하지만 절대로 강한 참조의 갯수가 0으로 떨어지지 않게 코드를 작성하는 것이 가능합니다. 이는 두 클래스 인스턴스가 서로에 대한 강한 참조를 보유하여 각 인스턴스가 다른 인스턴스를 계속 유지하는 경우 발생할 수 있습니다. 이를 강한 순환 참조라고 합니다.

클래스 간의 관계 중 일부를 강한 참조가 아닌 약하거나 미소유 참조로 정의하여 강한 순환 참조를 해결합니다. 이 프로세스는 클래스 인스턴스 간의 강한 순환 참조 해결에 설명되어 있습니다. 그러나 강한 순환 참조를 해결하는 방법을 배우기 전에 이러한 현상이 어떻게 발생하는지 이해하는 것이 좋습니다.

다음은 의도치 않게 강한 순환 참조를 생성하게 되는 예입니다. 이 예제는 아파트 블록과 거기에 사는 사람을 모델링하는 PersonApartment 두개의 클래스를 정의합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("Apartment \\(unit) is being deinitialized") }
}

모든 Person인스턴스에는 String타입의 name 과 옵셔널 속성을 가진 apartment에 초기값으로 nil이 할당 되어있습니다. apartment에 사람이 살지 않을수도 있기 때문입니다.

마찬가지로 모든 Apartment인스턴스에는 unit유형의 속성이 있으며 초기에 String옵셔널 tenant속성에 nil이 할당 되어있습니다. 아파트에는 항상 임차인이있을 수 없으므로 임차인 속성은 선택 사항입니다.

이 두 클래스 모두 deinitializer를 정의하여 해당 클래스의 인스턴스가 초기화되지 않았다는 사실을 출력합니다. 이 인스턴스 여부를 확인 할 수 있습니다 PersonApartment예상한대로 할당 해제되고있습니다.

이 다음 코드 스 니펫은 johnunit4A 라는 옵셔널 유형의 두 변수를 정의 하며 아래 unit4A에서 특정 ApartmentPerson인스턴스 로 설정 가능합니다. 이 두 변수는 모두 옵셔널이기 때문에 초기값이 nil입니다.

1
2
var john: Person?
var unit4A: Apartment?

이제 특정 Person인스턴스와 Apartment인스턴스를 만들고 이러한 새 인스턴스를 johnunit4A변수에 할당 할 수 있습니다 .

1
2
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

다음은이 두 인스턴스를 생성하고 할당 한 후 강한 참조가 어떻게 보이는지 보여줍니다. john 변수는 새 Person 인스턴스에 강한 참조를 가지고 있으며 unit4A 변수는 Apartment 인스턴스에 강한 참조를 가지고 있습니다.

../_images/referenceCycle01_2x.png

이제 두 인스턴스를 서로 연결하여 사람이 아파트를 보유하고 아파트가 임차인을 갖도록 할 수 있습니다. 여기서 느낌표(!)는 johnunit4A 인스턴스 안에 저장된 옵셔널(optional) 변수를 언랩핑하여 접근할 수 있게 하는 것입니다. 그렇게 인스턴스의 속성은 다음과 같이 설정 될 수 있습니다.

1
2
john!.apartment = unit4A
unit4A!.tenant = john

다음은 두 인스턴스를 서로 연결 한 후 강한 참조가 어떻게 나타나는지 보여줍니다.

../_images/referenceCycle02_2x.png

불행히도이 두 인스턴스를 연결하면 두 인스턴스간에 강한 순환 참조가 생성됩니다. Person 인스턴스는 Apartment 인스턴스에 대한 강한 참조를 가지고 있고, Apartment 인스턴스는 Person 인스턴스에 대한 강한 참조를 가지게 됩니다. 따라서 johnunit4A변수가 보유한 강한 참조를 끊을 때 참조 횟수는 0으로 떨어지지 않으며 ARC에서 인스턴스를 할당 해제하지 않습니다.

1
2
john = nil
unit4A = nil

이 두 변수를에 nil을 할당하면 디이니셜라이저가 호출되지 않습니다 강한 순환 참조는 PersonApartment인스턴스가 할당 해제되지 않도록하여 앱에서 메모리 누수를 유발합니다.

johnunit4A변수에 nil이 할당된 후 강한 참조가 어떻게 되었는지는 다음과 같습니다 .

../_images/referenceCycle03_2x.png

Person인스턴스와 Apartment인스턴스 사이의 강한 참조는 유지되며 깨지지 않습니다.

클래스 인스턴스 간의 강한 순환 참조 해결

Swift는 클래스 유형의 속성으로 작업 할 때 강한 순환 참조를 해결하는 두 가지 방법, 약한 참조 및 미소유 참조를 제공합니다.

약한 참조와 미소유 참조는 순환 참조의 한 인스턴스가 다른 인스턴스 강력하게 유지 하지 않고 다른 인스턴스를 참조 할 있도록합니다. 그런 다음 인스턴스는 강한 순환 참조를 만들지 않고 서로를 참조 할 수 있습니다.

다른 인스턴스의 수명이 짧을 때, 즉 다른 인스턴스를 먼저 할당 해제 할 수있는 경우 약한 참조를 사용하십시오. 위의 Apartment예에서, 아파트가 일생 동안 어떤 시점에 임차인을 가질 수없는 것이 적절하므로 이 경우 약한참조를 정하는데 기준이 됩니다. 반대로, 다른 인스턴스의 수명이 동일하거나 수명이 길면 미소유 참조를 사용하십시오.

약한 참조

약한 참조는 참조하는 인스턴스를 강하게 유지하지 않는 참조이며, 따라서 ARC가 참조된 인스턴스를 할당해제하는 것을 중지하지 않습니다. 이로 인해 참조가 강한 참조 순환의 일부가 되는 것을 방지합니다. weak 키워드를 선언의 앞에 위치시키는 것으로 속성이나 변수 선언이 약한 참조를 나타냅니다 .

약한 참조는 참조하는 인스턴스를 강력하게 유지하지 않기 때문에 약한 참조가 여전히 참조하는 동안 해당 인스턴스의 할당이 해제 될 수 있습니다. 따라서 ARC는 참조하는 인스턴스가 할당 취소될 때 nil에 대한 약한 참조를 자동으로 설정합니다. 또한 약한 참조는 런타임에 값이 nil으로 변경되도록 허용해야 하기 때문에 항상 옵셔널 유형의 상수가 아닌 변수로 선언됩니다.

다른 옵셔널 값과 마찬가지로 약한 참조에서 값이 존재하는지 확인할 수 있으며 더 이상 존재하지 않는 잘못된 인스턴스에 대한 참조는 일어나지 않습니다.

NOTE
ARC가 nil에 대한 약한 참조를 설정할 때 속성 관찰자는 호출되지 않습니다.

아래 예제는 위의 Person, Apartment 예제와 동일하지만한 한가지 중요한 차이점이 있습니다. 이번에는 Apartment타입의 tenant속성이 약한 참조로 선언되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person?
    deinit { print("Apartment \\(unit) is being deinitialized") }
}

두 변수 ( johnunit4A) 의 강한 참조 와 두 인스턴스 간의 연결은 이전과 같습니다.

1
2
3
4
5
6
7
8
var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

다음은 두 인스턴스를 서로 연결 한 후 참조가 어떻게 보이는지 보여줍니다.

../_images/weakReference01_2x.png

Person인스턴스는 여전히 Apartment인스턴스를 강한 참조를 하고 있습니다. 하지만, Apartment인스턴스는 이제 Person인스턴스를 약한 참조를 하고 있습니다. 즉, john변수에 대한 강한 참조를 해제하면 더 이상 Person인스턴스에 대한 강한 참조가 없습니다.

1
2
john = nil
// Prints "John Appleseed is being deinitialized"

더 이상 Person인스턴스에 대한 강한 참조가 없으므로 할당이 해제되고 tenant속성이 다음 과 같이 nil로 설정됩니다.

../_images/weakReference02_2x.png

Apartment인스턴스에 대한 강한 참조는 unit4A에 유일하게 유지됩니다. 그 강한 참조를 사라지게 한다면 Apartment에 대한 강한 참조는 더이상 남아있지 않게 됩니다.

1
2
unit4A = nil
// Prints "Apartment 4A is being deinitialized"

더 이상 Apartment인스턴스에 대한 강한 참조가 없으므로 할당이 해제됩니다.

../_images/weakReference03_2x.png

NOTE
가비지 컬렉션을 사용하는 시스템에서는 강력한 참조가 없는 개체가 가비지 컬렉션을 트리거하는 경우에만 할당을 취소하기 때문에 간단한 캐싱 메커니즘을 구현하는 데 약한 포인터가 사용되는 경우가 있습니다. 그러나 ARC를 사용하면 마지막 강력한 참조가 제거되는 즉시 값이 할당 해제되므로 약한 참조는 이러한 목적에 적합하지 않습니다.

미소유 참조

약한 참조처럼, 미소유 참조참조 하는 인스턴스를 강력하게 유지하지 않습니다. 그러나 약한 참조와 달리 미소유 참조는 다른 인스턴스의 수명이 동일하거나 수명이 길면 사용됩니다. unowned속성 또는 변수 선언 앞에 키워드를 배치하여 미소유 참조를 나타냅니다 .

미소유 참조에는 항상 값이 있어야합니다. 결과적으로 ARC는 미소유 참조 값을 nil로 설정하지 않습니다. 즉, 미소유 참조는 논 옵셔널 유형을 사용하여 정의됩니다.

IMPORTANT
참조가 항상 할당 해제되지 않은 인스턴스를 참조하는 경우에만 미소유 참조를 사용하십시오 . 만약 해당 인스턴스 할당이 해제 된 후 미소유 참조의 값에 액세스하려고하면 런타임 오류가 발생합니다.

다음 예제에서는 두 개의 클래스 CustomerCreditCard를 정의하고 있습니다. 이 클래스는 은행 고객과 그 고객에게 가능한 신용카드를 모델링합니다. 이 두 클래스는 서로의 인스턴스를 속성으로 저장합니다. 이 관계는 강한 순환 참조를 만들 가능성이 있습니다.

CustomerCreditCard 관계는 위의 약한 참조 예의 관계와 약간 다릅니다 . 이 데이터 모델에서 고객은 신용 카드를 보유하거나 보유하지 않을 수 있지만 신용 카드는 항상 고객이 있어야 합니다.

이를 나타 내기 위해 Customer 클래스는 card 속성을 옵셔널 로 가지지만, CredicCard 클래스는 customer 를 논 옵셔널 속성으로 가집니다. 뿐만 아니라 새로운 CreditCard 인스턴스는 오직 number값과 customer 인스턴스를 CreditCard의 커스텀 이니셜라이저를 통해서만 생성될 수 있습니다. 이를 통해 CreditCard 인스턴스가 생성될 때는 언제나 credit 인스턴스와 연관되어 있습니다.

신용 카드에는 항상 고객이 있기 때문에 강한 순환 참조를 피하기 위해 해당 customer속성을 미소유 참조로 정의 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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") }
}

NOTE
CreditCard클래스 의 number속성은 Int가 아닌 UInt64으로 정의 됩니다. 32 비트 및 64비트 시스템 모두에 16 자리 카드 번호를 저장할 수 있도록 보장합니다.

다음 코드 스 니펫은 john이라는 옵셔널 Customer변수를 정의합니다. 이 변수는 특정 고객에 대한 참조합니다. 이 변수는 옵셔널이기 때문에 초기 값은 nil입니다.

1
var john: Customer?

이제 Customer인스턴스를 생성하여 인스턴스의 card 속성에 할당할 CreditCard 인스터스의 초기화에 이용할 수 있습다.

1
2
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234\_5678\_9012\_3456, customer: john!)

두 인스턴스를 연결 했으므로 참조 모양은 다음과 같습니다.

../_images/unownedReference01_2x.png

Customer 인스턴스는 CreditCard에 대해 강한 참조를 하고 있습니다. CreditCardCustomer 인스턴스에 대해 미소유 참조를 하고 있습니다. customer가 미소유 참조이기 때문에 john 변수에 한 강한 참조를 사라지게 한 순간, Customer에 대한 강한 참조는 더이상 존재않게 됩니다.

../_images/unownedReference02_2x.png

더 이상 Customer인스턴스에 대한 참조가 없으므로 할당이 해제됩니다. 그리고나면 더 이상 CreditCard인스턴스에 대한 참조가 없으며 할당이 해제됩니다.

1
2
3
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card \#1234567890123456 is being deinitialized"

위의 마지막 코드 스 니펫 은 john변수가 nil로 설정된 후 Customer인스턴스 및 CreditCard인스턴스가 “디이니셜라이즈” 메시지를 출력하는 것을 보여주고 있습니다.

NOTE
위의 예는 안전한 미소유 참조 를 사용하는 방법을 보여줍니다 . Swift는 또한 성능상의 이유로 런타임 안전 검사를 비활성화해야하는 경우 안전하지 않은 참조를 제공합니다 . 안전하지 않은 모든 작업과 마찬가지로 해당 코드의 안전 점검 책임은 귀하에게 있습니다.

unowned(unsafe)를 작성하여 안전하지 않은 미소유 참조를 나타냅니다. 안전하지 않은 미소유 참조에 액세스하려고 하는 경우 해당 인스턴스가 있었던 메모리 위치(안전하지 않은 작업)에 액세스하려고 합니다.

미소유 참조 및 잠재적으로 래핑되지 않은(Unwrapped) 옵셔널 속성

위의 약한 참조, 미소유 참조에 대한 예는 강한 순환 참조를 없앨 수 있는 일반적인 두 가지 시나리오를 다룹니다.

PersonApartment예제에서 두 가지 특성 모두 nil을 허용합니다, 이럴 경우 잠재적인 강한 순환 참조를 일으킬 가능성이있다. 이 시나리오는 약한 참조로 잘 해결됩니다.

CustomerCreditCard예는 하나는 nil이 허용되고 다른 하나는 nil을 허용하지 않습니다. 이럴 경우 잠재적인 강한 순환 참조를 유발한다. 이 시나리오는 미소유 참조로 해결하는 것이 가장 좋습니다.

초기화가 되면 속성은 nil이라도 값을 가진다고 한다면 미소유 속성을 가진 클래스와 암시적인 언래핑된 옵셔널 속성을 가진 다른 클래스를 결합해야 유용하다.

이를 통해 초기화가 완료되면 순환 참조를 피하면서 두 속성 모두에 직접 액세스 할 수 있습니다 (옵셔널 래핑 해제없이). 이 섹션에서는 그러한 관계를 설정하는 방법을 보여줍니다.

아래 예제는 두 개의 클래스를 정의 Country하고 City각각은 다른 클래스의 인스턴스를 속성으로 저장합니다. 이 데이터 모델에서 모든 국가에는 항상 수도가 있어야하며 모든 도시는 항상 국가에 속해야합니다. 이를 나타 내기 위해 Country클래스에는 capitalCity속성이 있으며 City클래스에는 country속성이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Country {
    let name: String
    var capitalCity: City!
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

두 클래스 사이의 상호 의존성을 설정하기 위해 초기화 프로그램 CityCountry인스턴스 를 가져 와서 이 인스턴스를 해당 country속성 에 저장 합니다.

City 클래스의 이니셜라이저는 Country에 이니셜라이저 안에서 호출된다. 그러나 Country 이니셜라이저는 selfCity 이니셜라이저에 새로운 Country 인스턴스가 완전히 초기화될때까지 넘길 수 없다.

CountrycapitalCity 속성은 암시적인 언래핑된 옵셔널 속성으로 선언하며 타입 끝에 느낌표로 나타낸다. 이는 capitalCity 속성이 기본 값으로 nil을 가짐을 의미하지만 언래핑 할 필요 없이 접근할 수 있다.

capitalCity가 기본 값 nil을 가지기 때문에, 새로운 Country 인스턴스는 이니셜라이저 안에서 name 속성이 설정되어 완전히 초기화 되었다고 간주한다. 이는 Country 이니셜라이저는 참조를 시작하고 name 속성이 설정된 후에 암시적인 self 속성을 넘겨줄 수 있다. Country 이니셜라이저는 Country 이니셜라이저가 자신의 capitalCity 속성을 설정할 때, City 이니셜라이저에 self를 하나의 인자로 넘겨줄 수 있다.

강력 참조 순환 없이 두 클래스간의 관계를 만들 수 있음을 의미한다.

1
2
3
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\\(country.name)'s capital city is called \\(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"

암시적인 언래핑된 옵셔널은 두 클래스 이니셜러이저 필요를 만족시킨다. capitalCity 속성은 초기화가 완료될 때 옵셔널 아닌 값처럼 접근할 수 있고 사용되며, 강력 참조 순환을 피할 수 있다.

클로저를 위한 강한 참조 순환

위에서 두 클래스 인스턴스 속성이 서로에 대한 강한 참조를 보유 할 때 강한 순환 참조를 만드는 방법을 살펴 보았습니다. 또한 약하고 미소유 참조를 사용하여 이러한 강한 순환 참조를 끊는 방법도 살펴 보았습니다.

클래스 인스턴스의 속성에 클로저를 할당하면 강력 참조 순환이 발생하고, 클로저 내에서 인스턴스를 획득한다. 클로저 내에서 self.someProperty 같이 인스턴스의 속성을 접근하거나 클로저가 인스턴스에 self.someMethod() 같은 메소드를 호출하기 때문에 획득이 발생한다. 클로저가 self를 획득하도록 접근하여 강력 참조 순환이 만들어진다.

강력 참조 순환은 클래스와 비슷하게 클로저가 참조 타입이기 때문에 발생한다. 클로저를 속성에 할당하면, 클로저에 참조를 할당한다. 따라서 두 개의 강력 참조가 각각 서로 살아있게 한다.

Swift는 클로저 획득 목록으로 이 문제를 해결한다.

다음은 강력 참조 순환이 발생하는 예제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -\> String = {
        if let text = self.text {
            return "\<\\(self.name)\>\\(text)\</\\(self.name)\>"
        } else {
            return "\<\\(self.name) /\>"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

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

}

HTMLElement 클래스는 name 속성을 정의합니다. 이것은 제목 요소 “h1“단락 요소의 “p“줄 바꿈 요소 “br“등 요소의 이름을 나타냅니다. HTMLElement 옵션의 text 속성을 정의합니다. 이 속성은 HTML 요소에서 렌더링 할 텍스트를 나타내는 문자열을 설정할 수 있습니다.

이러한 두 가지 간단한 속성 이외에 HTMLElement 클래스는 asHTML라는 지연 속성을 정의합니다. 이 속성은 이름과 텍스트를 HTML 문자열 조각에 결합하는 클로저를 참조하십시오. asHTML 속성은 유형 () -> 문자열 또는 “파라미터를 취하지 않고 문자열 값을 반환하는 함수」입니다.

기본적으로 asHTML 속성은 HTML 태그의 문자열 표현을 반환 클로저가 할당됩니다. 이 태그는 존재하는 경우는 옵션의 텍스트 값이 포함 된 텍스트가 존재하지 않는 경우 텍스트 콘텐츠가 포함되지 않습니다. 단락 요소의 경우 폐쇄 text 속성이 “some text“또는 nil이 동일한 지 여부에 따라 “<p> some text </ p>“또는 “<p />“를 반환합니다.

asHTML 속성에는 이름이 인스턴스 메소드처럼 사용됩니다. 그러나 asHTML는 인스턴스 메서드 대신 클로저 속성이기 때문에 특정 HTML 요소의 HTML 렌더링을 변경하려면 asHTML 속성의 기본값을 지정 클로저로 대체 할 수 있습니다.

예를 들어, 텍스트 속성이 빈 HTML 태그를 반환을 방지하기 위해 asHTML 속성을 text 속성이 nil의 경우 기본 텍스트로 설정되는 클로저 설정할 수 있습니다.

1
2
3
4
5
6
7
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "\<\\(heading.name)\>\\(heading.text ?? defaultText)\</\\(heading.name)\>"
}
print(heading.asHTML())
// Prints "\<h1\>some default text\</h1\>"

NOTE
asHTML 속성은 요소를 실제로 HTML 출력 대상의 문자열 값으로 렌더링 할 필요가있는 경우에만 필요하므로 지연 속성으로 선언됩니다. asHTML이 지연 속성이라는 사실은 초기화가 완료 self가 존재하는 것으로 될 때까지 지연 속성에 액세스하지 않기 때문에 기본 폐쇄에서 self를 볼 수 있다는 것을 의미합니다.

HTMLElement클래스는 단일 이니셜 라이저를 제공하며 name, text인수는 새로운 요소를 초기화하기 위해 인수 및 원하는 경우 인수 를 취합니다 . 이 클래스는 deinitializer를 정의하여 HTMLElement인스턴스 할당이 해제 될 때 표시되는 메시지를 출력합니다 .

HTMLElement클래스를 사용하여 새 인스턴스를 만들고 출력하는 방법은 다음과 같습니다 .

1
2
3
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "\<p\>hello, world\</p\>"

NOTE
paragraph변수는 옵셔널 으로 정의 HTMLElement되므로 nil강한 순환 참조가 있음을 보여주기 위해 아래로 설정할 수 있습니다 .

불행히도, HTMLElement위에서 언급 한 클래스는 HTMLElement인스턴스와 기본값으로 사용되는 클로저 사이에 강한 순환 참조를 만듭니다 asHTML. 주기는 다음과 같습니다.

../_images/closureReferenceCycle01_2x.png

인스턴스의 asHTML속성은 해당 클로저에 대한 강한 참조를 보유합니다. 그러나 클로저는 self( self.name및 참조 방법으로) 본문 내를 참조 self.text하므로 클로저 자체를 캡처 하므로 HTMLElement인스턴스에 대한 강한 참조를 보유 합니다. 둘 사이에 강한 순환 참조가 생성됩니다. 클로저에서 값을 캡처하는 방법에 대한 자세한 내용은 값 캡처를 참조하십시오 .

NOTE
클로저는 self여러 번 참조되지만 HTMLElement인스턴스에 대한 하나의 강한 참조 만 캡처 합니다.

paragraph변수를 nil로 설정하고 인스턴스에 HTMLElement 대한 강한 참조를 해제하면 강한 참조 주기로 인해 .HTMLElement 인스턴스와 해당 클로저의 할당이 해제되지 않습니다

1
paragraph = nil

HTMLElement초기화 해제 기 의 메시지는 출력 HTMLElement되지 않으며 인스턴스 할당이 해제되지 않았 음 을 나타냅니다 .

클로저에 대한 강한 참조 순환 사이클 해결

클로저 정의의 일부로 캡처 목록 을 정의하여 클로저와 클래스 인스턴스 간의 강한 순환 참조를 해결합니다 . 캡처 목록은 클로저 본문 내에서 하나 이상의 참조 유형을 캡처 할 때 사용할 규칙을 정의합니다. 두 클래스 인스턴스 간의 강한 순환 참조와 마찬가지로 캡처 된 각 참조를 강한 참조가 아닌 약하거나 미소유 참조로 선언합니다. 약하거나 미소유 적절한 선택은 코드의 다른 부분 사이의 관계에 따라 다릅니다.

NOTE
Swift 는 클로저 내에서 멤버변수를 언급 할 때마다 (someProperty 또는 someMethod() ) 대신 self.someProperty 또는 self.someMethod() 을 쓰도록 요구합니다 . 이를 통해 실수 로 캡처 할 수 있음을 기억할 수 있습니다

캡처 목록 정의

캡처 목록의 각 항목은 클래스 인스턴스 (예 : self) 또는 일부 값 (예 : delegate = self.delegate!)으로 초기화 된 변수에 대한 참조와 키워드 weak또는 unowned키워드 의 쌍입니다 . 이 쌍은 쉼표로 구분 된 한 쌍의 대괄호 안에 작성됩니다.

캡처 목록을 클로저의 매개 변수 목록 앞에 놓고 제공된 경우 반환 :

1
2
3
4
lazy var someClosure: (Int, String) -\> String = {
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -\> String in
    // closure body goes here
}

클로저가 컨텍스트에서 유추 될 수 있으므로 매개 변수 목록 또는 리턴 유형을 지정하지 않은 경우 캡처 목록을 클로저의 맨 처음에두고 in키워드를 입력하십시오.

1
2
3
4
lazy var someClosure: () -\> String = {
    [unowned self, weak delegate = self.delegate!] in
    // closure body goes here
}

약한 참조와 미소유 참조

클로저와 캡처하는 인스턴스가 항상 서로를 참조하고 항상 동시에 할당 해제 될 때 클로저의 캡처를 미소유 참조로 정의하십시오.

반대로 캡처 된 참조가 nil향후 어느 시점에 있을 때 캡처를 약한 참조로 정의하십시오 . 약한 참조는 항상 옵셔널 유형이며 nil참조하는 인스턴스가 할당 해제되면 자동으로됩니다 . 이를 통해 클로저 바디 내에 존재하는지 확인할 수 있습니다.

NOTE
캡처 된 참조가 절대로 nil 되지 않으면 항상 약한 참조가 아닌 미소유 참조로 캡처해야합니다.

미소유 참조는 위의 폐쇄에 대한 강한 순환 참조HTMLElement예에서 강한 순환 참조 를 해결하는 데 사용하는 적절한 캡처 방법 입니다. HTMLElement주기를 피하기 위해 클래스를 작성하는 방법은 다음과 같습니다 .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -\> String = {
        [unowned self] in
        if let text = self.text {
            return "\<\\(self.name)\>\\(text)\</\\(self.name)\>"
        } else {
            return "\<\\(self.name) /\>"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

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

}

이 구현은 클로저 HTMLElement내에 캡처 목록을 추가한다는 점을 제외하면 이전 구현과 동일합니다 asHTML. 이 경우 캡처 목록 [unowned self]은 “강한 참조가 아닌 미소유 참조로 자체 캡처”를 의미합니다.

HTMLElement이전과 같이 인스턴스를 만들고 출력 할 수 있습니다 .

1
2
3
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "\<p\>hello, world\</p\>"

캡처 목록을 사용하여 참조가 표시되는 방식은 다음과 같습니다.

../_images/closureReferenceCycle02_2x.png

이번에 self는 클로저 에 의한 캡처 는 미소유 참조이며 HTMLElement캡처 한 인스턴스를 강력하게 유지하지 않습니다 . 당신이 paragraph에 변수 nil에서 강한 참조 설정 한 경우는 HTMLElement아래의 예에서의 deinitializer 메시지의 출력에서 볼 수 있듯이 인스턴스는, 할당 해제됩니다 :

1
2
paragraph = nil
// Prints "p is being deinitialized"

캡처 목록에 대한 자세한 내용은 Capture Lists을 참조하세요

원문

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html