3-3 ARC

STONE·2024년 11월 6일

Swift_Ios

목록 보기
12/44

주제

ARC ( Automatic Reference Counting)

Swift는 앱의 메모리 사용량을 추적하고 관리하기 위해 자동 참조 카운팅(ARC)를 사용한다.
대부분의 경우에는 Swift에서는 메모리 관리는 "그냥 수행하라"를 의미하고 메모리 관리에 대해서 생각할 필요가 없다. ARC는 인스턴스가 더이상 필요하지 않을 때 자동으로 클래스 인스턴스에 의해 사용된 메모리를 할당 해제 한다.

ARC의 작동 원리

클래스의 새로운 인스턴스가 생성될 때마다 ARC는 인스턴스에 대한 정보를 저장하기 위해 메모리의 청크에 할당한다. 또한 더이상 필요치 않을 때 ARC는 메모리가 다른 목적으로 사용될 수 있도록 인스턴스에 의해 사용된 메모리를 할당 해제 한다. 그러나 ARC가 아직 사용중인 인스턴스에 할당을 해제하면 더이상 인스턴스의 프로퍼티에 접근할 수 없거나 인스턴스의 메서드를 호출할 수 없다. 인스턴스가 여전히 필요한 동안 사라지지 않도록 추적한다. 이를 가능하게 하려면 강한 참조를 만든다

ARC 동작

ARC 예제

class Person{
	let name: String
    init(name: String) {
    	self.name = name
        print("\(name) is beingg initialized")
    }
    deinit {
    	print("\(name) is being deinitialized")
    }
}

Person클래스는 인스턴스의 name프로퍼티에 설정하고 초기화가 진행 중임을 나타내는 메세지를 출력하는 초기화 구문을 가집니다. Person클래스는 인스턴스가 할당 해제될 때 메시지를 출력하는 초기화 해제 구문도 가지고 있습니다.

다음 코드는 Person인스턴스에 여러개 참조를 설정하기 위해 Person? 타입의 3개의 변수를 정의하고 옵셔널 타입 이므로 nil의 값으로 자동으로 초기화 되고 현재는 Person 인스턴스를 참조하지 않음

var reference1: Person?
var reference2: Person?
var reference3: Person?

새로운 Person인스턴스를 생성하고 3개의 변수중 하나에 할당

reference1 = Person(name: " Kim " )
// 출력 
// Kim Applessed is being initialized Person클래스의 초기화 구문 실행할 떄 출력 -> 초기화 발생

여기서 새로운 Person 인스턴스는 reference1 변수에 할당되기 때문에 reference1에서 강한 참조가 발생하고 ARC는 Person을 메모리에 유지하고 할당 해제하지 않음

//강한 참조가 설정된 reference1을 할당하면 해당 인스턴스에 대한 2개 이상의 강한 참조 설정
// 총 3 개의 강한 참조 
reference2 = reference1
reference3 = reference1

// 2개를 중단해도 하나의 강한 참조가 남아있으므로 Person인스턴스 해제되지 않음
reference1 = nil
reference2 = nil

// 남은 하나의 강한 참조를 중단할 때까지 Person인스턴스를 할당 해제 하지않음 -> 이때부터Person인스턴스를 더이상 사용하지 않음
reference3 = nil

클래스 인스턴스 사이의 강한 참조 사이클

위의 설명에서 ARC는 새로운 Person 인스턴스를 생성하고 더이상 필요치 않을 때 할당 해제하기 윟해서 참조의 수를 추적할 수 있다.
그러나 클래스의 인스턴스가 강한 참조가 없는 지점에 도달하지 않는 코드를 작성할 수 있습니다. 이는 두 클래스 인스턴스가 서로에 대한 강한 참조를 유지하여 각 인스턴스가 다른 인스턴스를 유지하는 경우 발생할 수 있습니다. 이것은 강한 참조 사이클 (strong reference cycle) 이라고 합니다.
해결방법

  • 약한 참조
  • 미소유 참조
    사이클 예제
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 프로퍼티와 초기값이 nil 인 옵셔널 apartment 프로퍼티를 가지고 있습니다. 사람이 항상 아파트를 가지고 있지 않기 때문에 apartment 프로퍼티는 옵셔널입니다.

유사하게 모든 Apartment 인스턴스는 String 타입의 unit 프로퍼티와 초기값이 nil 인 옵셔널 tenant 프로퍼티를 가지고 있습니다. 아파트는 항상 세입자를 보유하는 것이 아니므로 tenant 프로퍼티는 옵셔널입니다.

이 클래스 모두 클래스의 인스턴스가 초기화 해제 됨을 출력하는 초기화 해제 구문을 정의합니다. 이것을 사용하면 Person 과 Apartment 의 인스턴스가 예상대로 초기화 해제되었는지 확인할 수 있습니다.

다음 코드는 아래에서 특정 Apartment 와 Person 인스턴스를 설정할 john 과 unit4A 라는 옵셔널 타입의 2개의 변수를 정의합니다. 이 변수 모두 옵셔널 이므로 초기값으로 nil 을 가집니다

var john: Person?
var unit4A: Apartment?

// 이제 특정 Person 인스턴스와 Apartment인스턴스를 생성할 수 있고 이 새로운 인스턴스를 변수에 할당 가능
john = Person(name: "John Applessed")
unit4A = Apartment(unit: "4A")

강한 참조 할당하는 이미지

// 이제 사람 <-> 아파트 두 인스턴스를 연결할 수 있다
// !는 두 옵셔널 변수 내에 저장된 인스턴스를 언래핑하고 접근하기 위해 사용됨
john!.apartment = unit4A
unit4A!.tenant = john

강한 참조 사이클 이미지

//이제 두 변수를 nil로 설정해도 초기화 되지 않는다
john = nil
unit4A = nil
// 두 인스턴스 간의 강한 참조는 남아있고 중단될 수 없다

클래스 인스턴스 간의 강한 참조 사이클 해결

week reference(약한 참조)

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? // week 약한 참조 설정
    deinit { print("Apartment \(unit) is being deinitialized") }
}

// nil 설정하여 강한 참조를 끊으면 더이상 Person 인스턴스에 대해 강한 참조를 가지지 않기 때문에 할당 해제되고 tenant은 nil로 설정
john = nil

// 두 변수 모두 할당 해제됨
unit4A = nil

Unowned References (미소유 참조)

미소유 참조는 약한 참조와 마찬가지로 인스턴스를 강하게 유지하지 않지만, 다른 인스턴스의 수명이 같거나 더 긴 경우에 사용한다.

// 미소유 참조 예제
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") }
}

// 특정 고객에 대한 참조를 저장하는데 사용되는 john이라는 옵셔널 변수 정의 초기값은 nil
var john: Customer?

// Customer라는 인스턴스 생성 가능, card프로퍼티로 새로운 CreditCard인스턴스 초기화하고 할당 가능
john = Customer(name: "John Applessed")
john!.card = CreditCard(number: 1234_1234, customer: john!)

// john을 nil로 설정한 후에 deinitialized 초기화 해제 구문 출력됨
john = nil      

두 인스턴스 참조 이미지

미소유 옵셔널 참조

클래스에 옵셔널 참조를 미소유로 표기할 수 있다

// Department는 각 과정에 강한 참조를 유지
class Department {
    var name: String
    var courses: [Course]
    init(name: String) {
        self.name = name
        self.courses = []
    }
}
// Course는 2개의 미소유 참조
class Course {
    var name: String
    unowned var department: Department
    unowned var nextCourse: Course?
    init(name: String, in department: Department) {
        self.name = name
        self.department = department  // 옵셔널 x
        self.nextCourse = nil // 옵셔널 o
    }
}

let department = Department(name: "Horticulture")

let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)

intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]


미소유 옵셔널 참조는 래핑하는 클래스의 인스턴스에 강하게 유지하지 않으므로 ARC가 인스턴스를 할당 해제하는 것을 방지하지 않습니다. 미소유 옵셔널 참조가 nil 이 될 수 있다는 점을 제외하고 미소유 참조는 ARC에서 수행하는 것과 동일하게 동작합니다.

옵셔널이 아닌 미소유 참조와 같이 nextCourse 가 항상 할당 해제되지 않은 과정을 참조하도록 해야합니다. 예를 들어 department.courses 에서 과정을 삭제할 때 다른 과정에 있을 수 있는 모든 참조를 삭제해야 합니다.

미소유 참조와 암묵적 언래핑된 옵셔널 프로퍼티

이 시나리오에서는 한 클래스의 미소유 프로퍼티를 다른 클래스에 암시적으로 언래핑된 옵셔널 프로퍼티와 결합하는 것이 유용합니다.

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

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"

클로저에 대한 강한 참조 사이클

강한 참조 사이클은 클래스 인스턴스의 프로퍼티에 클로저를 할당하고 해당 클로저의 본문에 인스턴스를 캡처하는 경우에도 발생할 수 있습니다.

//아래의 예제는 self 를 참조하는 클로저를 사용할 때 강한 참조 사이클이 어떻게 생성될 수 있는지 보여줍니다. 이 예제는 HTML 문서 내에 개별 요소에 대한 간단한 모델을 제공하는 HTMLElement 라는 클래스를 정의합니다:
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")
    }

}

// asHTML 프로퍼티는 빈 HTML태그를 반환하는 표현을 방지하기위해 text 프로퍼티가 nil이면 일부 텍스트를 기본값으로 하는 클로저로 설정할 수 있다.
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>"

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

// paragraph를 nil로 설정하고 강한 참조를 끊으면 강한 참조 사이클은 HTMLElement 인스턴스와 해당 클로저를 할당 해제하지 않는다

paragraph = nil

클로저 간의 강한 참조 사이클

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

클로저의 정의의 부분으로 캡처 리스트 (capture list) 를 정의하여 클로저와 클래스 인스턴스 간의 강한 참조 사이클을 해결합니다. 캡처 리스트은 클로저의 본문 내에서 하나 이상의 참조 타입을 캡처할 때 사용할 규칙을 정의합니다. 두 클래스 간의 강한 참조 사이클과 마찬가지로 캡처된 각 참조를 강한 참조가 아닌 약한 참조 또는 미소유 참조로 선언합니다. 약한 참조 또는 미소유 참조의 적절한 선택은 코드의 다른 부분 간의 관계에 따라 다릅니다.

캡처 리스트 정의

캡처 리스트에서 각 항목은 self 처럼 클래스 인스턴스 또는 delegate = self.delegate 와 같은 어떤 값을 초기화된 변수에 대한 참조가 있는 weak 또는 unowned 키워드와 쌍을 이룹니다. 이 쌍은 콤마로 구분하여 대괄호 내에 작성됩니다.

캡처 리스트은 클로저의 파라미터 리스트 전에 위치하고 반환 타입이 있다면 반환 타입은 파라미터 리스트 다음에 위치합니다:

// 파라미터와 반환타입이 존재 in 키워드로 클로저 본문이 시작됨
lazy var someClosure = {
    [unowned self, weak delegate = self.delegate] // 클로저가 self, self.delegate에 강한 참조를 가지지 않도록 만듬
    (index: Int, stringToProcess: String) -> String in
    // closure body goes here
}
//클로저는 컨텍스트로 부터 유추할 수 있기 때문에 파라미터 리스트 또는 반환 타입을 지정하지 않으면 캡처 리스트는 클로저의 가장 처음에 위치하고 이어서 in 키워드가 옵니다

//파라미터와 반환타입이 없을 경우
lazy var someClosure = {
    [unowned self, weak delegate = self.delegate] in
    // closure body goes here
}

약한 참조와 미소유 참조

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

반대로 캡처된 참조가 향후에 nil 이 될 때 약한 참조로 캡처를 정의합니다. 약한 참조는 항상 옵셔널 타입이고 참조하는 인스턴스가 할당 해제되면 자동으로 nil 이 됩니다. 이를 사용하여 클로저의 본문 내에서 존재하는지 확인할 수 있습니다.

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

}

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

// paragrph = nil로 설정하면 HTMLElement인스턴스 할당 해제
paragraph = nil

캡처 리스트 참조 이미지

출처 - https://bbiguduk.gitbook.io/swift/language-guide-1/automatic-reference-counting

profile
흠...?

0개의 댓글