Swift)ARC

Havi·2021년 2월 4일
0

Swift기초

목록 보기
16/19

Swift에서는 앱의 메모리 사용을 관리하기 위해서 ARC(Automatic Reference Counting)을 사용한다.

자동으로 참조 횟수를 관리하기 때문에 대부분의 경우, 개발자가 메모리 관리에 신경 쓸 필요가 없고, ARC가 알아서 더이상 사용하지 않는 인스턴스 메모리를 해지한다.

하지만 몇몇의 경우 ARC에서 메모리 관리를 위해 코드의 특정 부분에 대한 관계에 대한 정보를 필요로 한다.

참조 횟수는 클래스 타입의 인스턴스에만 적용되고, 값 타입인 구조체, 열거형 등에는 적용되지 않는다.

ARC의 동작

클래스의 새 인스턴스를 만들 때마다, ARC는 인스턴스의 정보를 담는데 필요한 적정의 크기의 메모리를 할당한다.

이 메모리는 그 인스턴스에 대한 정보와 관련된 저장 프로퍼티 값도 가지고 있는다. 추가적으로 인스턴스가 더 이상 사용되지 않으면, ARC는 그 인스턴스가 차지하고 있는 메모리를 해지해서 다른 용도로 사용할 수 있고록 공간을 확보한다.

하지만 만약 ARC가 아직 사용중인 인스턴스를 메모리에서 내렸는데, 인스턴스의 프로퍼티에 접근한다면 앱은 크래시가 발생한다.

ARC에서는 아직 사용중인 인스턴스를 해지하지 않기 위해 얼마나 많은 프로퍼티, 상수, 변수가 그 인스턴스에 대한 참조를 가지고 있는지를 추적한다.

그래서 ARC가 최소 하나라도 그 인스턴스에 대한 참조가 있는 경우 그 인스턴스를 메모리에서 해지하지 않는다.

ARC의 사용

다음 코드는 ARC가 실제로 어떻게 동작하는지 확인하는 코드이다.

// MARK: ARC

class Person {
    let name: String

    init(name: String) {
        self.name = name
        print("\(name) init")
    }

    deinit {
        print("\(name) deinit")
    }
}

var ref1: Person?
var ref2: Person?
var ref3: Person?

ref1 = Person(name: "스리니") // Person's ARC = 1
// Prints 스리니 init

ref2 = ref1 // Person's ARC = 2
ref3 = ref1 // Person's ARC = 3

ref1 = nil //  Person's ARC = 2
ref2 = nil //  Person's ARC = 1

ref3 = nil // Person's ARC = 0
// Prints 스리니 deinit

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

ARC에서 기본적으로 참조 횟수에 대해 추적하고 있기 때문에 더이상 사용하지 인스턴스는 자동으로 메모리에서 해제된다.

하지만 절대로 메모리에서 해제 되지 않는 경우도 있다.

예를 들어 클래스의 인스턴스간 강하게 상호 참조를 하고 있는 경우이다.

이를 강한 참조 순환이라고 한다.

다음은 예제 코드이다.

class Person {
    let name: String
    var apart: Apartment?

    init(name: String) {
        self.name = name
        print("\(name) init")
    }

    deinit {
        print("\(name) deinit")
    }
}

class Apartment {
    let unit: String
    var tenant: Person?

    init(unit: String) {
        self.unit = unit
        print("\(unit) init")
    }

    deinit {
        print("\(unit) deinit")
    }
}

var frog: Person?
var helioCity: Apartment?

frog = Person(name: "개굴")
helioCity = Apartment(unit: "송파")

변수 frog는 Person을 강하게 참조하고, helioCity는 Apartment를 강하게 참조한다.

frog?.apart = helioCity
helioCity?.tenant = frog

Person instance의 apart가 helioCity를 참조하고, Apartment의 tenant가 Person을 참조해서 서로 refernce count 가 2가 된다.

frog = nil
helioCity = nil

각 변수를 nil 로 할당해 참조를 해지한다. 하지만 인스턴스는 해지되지 않는다.

그림처럼 강한 순환 참조가 발생해 메모리 누수가 일어난다.

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

강한 순환 참조를 해결하기 위해서는 weak 참조, 하나는 unowned참조를 사용하는 방법이 있다.

두 참조 모두 ARC에서 reference count를 증가시키지 않고 인스턴스를 참조하기 때문에 싸이클을 해결할 수 있다.

따라서 위 예제에서

weak var tenant: Person?

로 바꿔주게되면 그림이 다음과 같이 바뀌면서 싸이클이 해제된다.

미소유 참조

Unowned References는 weak Reference와 다르게 참조 대상이 되는 인스턴스가 현재 참조하고 있는 것과 같은 생애주기를 갖거나 더 긴 생애 주기를 갖기 때문에 항상 참조에 그 값이 있다고 기대된다.

따라서 unowned 참조는 절대 Nil을 할당하지 않는다.

미소유 참조는 참조 대상 인스턴스가 항상 존재한다고 생각하기 때문에 인스턴스가 해제됐을 때 접근하게 되면 런타임 에러가 발생한다.

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

클로저에서 강한 참조 순환

강한 참조 순환은 클로저와의 관계돼서 발생할 수도 있다.

왜냐하면 클로저에서는 self를 캡쳐하기 때문이다.

이 문제를 해결하기 위해 클로저 캡쳐리스트를 사용한다.

아래 클래스에서는 클로저에서 self.text와 self.name을 캡쳐한다.

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의 클로저는 아래와 같이 다른 클로저로 변경 될 수도 있습니다.

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

asHTML은 lazy property로 선언됐습니다. 왜냐하면 HTML을 렌더링하기 위해 태그와 텍스트가 준비된 뒤에 사용해야하기 때문입니다.

이 코드를 실행하면 다음의 결과가 나온다.

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

그러면 강한 순환 참조에 빠지게 된다.

클로저에서 강한 참조 순환 문제의 해결

클로저에서 강한 참조 순환 문제의 해결하기 위해 캡쳐 참조에 strong 대신 weak 혹은 unowned참조를 지정 가능

weak 일지 unowned일지는 코드에서 상호관계에 달려있다.

클로저에서는 self를 명시해줘야 한다.

캡쳐리스트 정의

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

약한 참조와 미소유 참조

참조가 먼저 해제되는 경우 약한참조,

같은 시점이나 나중 시점에 해제되는 경우에 미소유 참조 사용

만약 캡쳐리스트가 절대 nil이 될 수 없다면 그것은 반드시 unowned가 되어야한다.

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

profile
iOS Developer

0개의 댓글