본문 바로가기

iOS

ARC(Automatic Reference Counting)

728x90
반응형
SMALL

ARC(Automatic Reference Counting)

ARC(Automatic Reference Counting)는 Objective-C와 Swift에서 사용되는 자동 메모리 관리 기법입니다.

이전에는 수동으로 메모리를 관리해야 했으나, ARC를 사용하면 컴파일러가 코드에서 객체에 대한 참조를 추적하고, 객체를 참조하는 변수나 속성, 컬렉션 등의 객체에 대한 참조가 없을 때 자동으로 객체를 해제합니다. 따라서 개발자는 메모리 관리를 위한 코드를 작성하지 않아도 됩니다.

 

ARC는 앱의 성능을 향상시키고, 메모리 관리를 단순화시키는 등의 장점이 있습니다. 하지만, 객체의 참조 관계가 복잡하거나 순환 참조(circular reference)가 발생하는 경우에는 여전히 메모리 누수(memory leak)가 발생할 수 있습니다. 따라서 개발자는 객체의 수명 주기와 참조 관계를 잘 이해하고, 약한 참조(weak reference)나 비소유 참조(unowned reference)를 사용하여 이러한 문제를 해결해야 합니다.

 

ARC 예제코드

import Foundation

class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

var person1: Person? = Person(name: "John", age: 30)
var person2: Person? = person1 // person1의 참조를 person2에 할당
person1 = nil // person1 참조 해제
print(person2?.name)
if let name = person2?.name {
    print("Hello, \(name)!")
} else {
    print("Hello, world!")
}

person2 = nil // person2 참조 해제

옵셔널... 불편...

 

 

순환참조의 예시 코드와 메모리 누수 확인

class Person {
    var name: String
    var car: Car?
    
    init(name: String) {
        self.name = name
        print("\(name)이(가) 생성되었습니다.")
    }
    
    deinit {
        print("\(name)이(가) 제거되었습니다.")
    }
}

class Car {
    var model: String
    var owner: Person?
    
    init(model: String) {
        self.model = model
        print("\(model)이(가) 생성되었습니다.")
    }
    
    deinit {
        print("\(model)이(가) 제거되었습니다.")
    }
}

var person: Person? = Person(name: "John")
var car: Car? = Car(model: "Tesla")

person?.car = car
car?.owner = person

person = nil
car = nil

이 코드는 Person과 Car 클래스 간의 순환 참조를 보여줍니다. Person 클래스의 인스턴스와 Car 클래스의 인스턴스가 서로를 강한 참조하고 있습니다. deinit 되지 않음.

 

해결1. 

타입선언시 onwer 를 약한 참조로 선언

class Person {
    var name: String
    var car: Car?
    
    init(name: String) {
        self.name = name
        print("\(name)이(가) 생성되었습니다.")
    }
    
    deinit {
        print("\(name)이(가) 제거되었습니다.")
    }
}

class Car {
    var model: String
    weak var owner: Person?
    
    init(model: String) {
        self.model = model
        print("\(model)이(가) 생성되었습니다.")
    }
    
    deinit {
        print("\(model)이(가) 제거되었습니다.")
    }
}

var person: Person? = Person(name: "John")
var car: Car? = Car(model: "Tesla")

person?.car = car
car?.owner = person

person = nil
car = nil

 

**String이 Weak 선언이 안되는 이유

Swift의 String 클래스는 참조 타입이 아닌 값 타입(Value Type)이므로 weak 키워드로 선언될 수 없습니다.

값 타입은 값 그 자체를 복사하여 전달하므로, 복사본과 원본이 독립적으로 존재합니다. 이는 참조 타입과는 다른 메모리 관리 방식입니다. 따라서, 값 타입은 강한 참조를 필요로 하지 않으므로 weak 키워드로 선언될 필요가 없습니다.

반면에 참조 타입은 하나의 인스턴스를 여러 변수나 상수에서 참조할 수 있으므로, 각각의 참조가 인스턴스를 메모리에서 제거하는데 영향을 미칩니다. 이때 강한 참조가 사용되고, 때로는 weak 참조가 필요할 수 있습니다.

따라서, String 클래스는 값 타입(Value Type)으로 구현되어 있으므로 weak 키워드로 선언될 수 없습니다.

 

해결 2

Capture List를 사용해 약한 참조로 변환

class Person {
    var name: String
    var car: Car?
    
    init(name: String) {
        self.name = name
        print("\(name)이(가) 생성되었습니다.")
    }
    
    deinit {
        print("\(name)이(가) 제거되었습니다.")
    }
}

class Car {
    var model: String
    var ownerClosure: (() -> Person?)?
    
    init(model: String) {
        self.model = model
        print("\(model)이(가) 생성되었습니다.")
    }
    
    deinit {
        print("\(model)이(가) 제거되었습니다.")
    }
}

var person: Person? = Person(name: "John")
var car: Car? = Car(model: "Tesla")

person?.car = car
car?.ownerClosure = { [weak person] in person }

person = nil
car = nil

여기에서 Car 클래스는 ownerClosure 속성을 가지고 있으며, 클로저 내에서 Person 클래스의 인스턴스를 강한 참조 대신 약한 참조로 참조하도록 Capture List를 사용했습니다. 이렇게 함으로써, Person과 Car 클래스 간의 순환 참조를 해결할 수 있습니다.

 

 

해결 3

Capture List를 사용해 미소유 참조로 변환

class Person {
    var name: String
    var car: Car?
    
    init(name: String) {
        self.name = name
        print("\(name)이(가) 생성되었습니다.")
    }
    
    deinit {
        print("\(name)이(가) 제거되었습니다.")
    }
}

class Car {
    var model: String
    var ownerClosure: (() -> Person?)?
    
    init(model: String) {
        self.model = model
        print("\(model)이(가) 생성되었습니다.")
    }
    
    deinit {
        print("\(model)이(가) 제거되었습니다.")
    }
}

var person: Person? = Person(name: "John")
var car: Car? = Car(model: "Tesla")

person?.car = car
car?.ownerClosure = { [unowned person] in person }

person = nil
car = nil

 

약한 참조(Weak Reference)

해당 참조가 참조하는 인스턴스가 메모리에서 해제되면 자동으로 nil값으로 초기화됩니다. 약한 참조는 강한 참조와 달리 참조 카운트를 증가시키지 않습니다.

약한 참조는 보통 순환 참조(Strong Reference Cycle)를 막기 위해 사용됩니다. 예를 들어, A 객체가 B 객체를 강한 참조하고, B 객체가 다시 A 객체를 강한 참조하는 경우, 이 둘 사이에 순환 참조가 발생합니다. 이 경우, A 객체나 B 객체 중 하나가 메모리에서 해제되지 않고 계속 남아 있게 되는데, 이 문제를 해결하기 위해 약한 참조를 사용할 수 있습니다.

 

미소유 참조(Unowned Reference)

미소유 참조(Unowned Reference)는 비슷한 개념으로, 참조하는 인스턴스가 메모리에서 해제되면 더 이상 유효하지 않은 포인터가 됩니다. 하지만 약한 참조와 달리 nil값으로 초기화되지 않으며, 사용시 nil 체크를 직접 해주어야 합니다. 또한, 미소유 참조는 참조 카운트를 증가시키지 않으며, 참조 대상이 nil이 아님을 보장하지 않습니다.

미소유 참조는 약한 참조보다는 더 성능이 좋고 간결한 코드를 작성할 수 있으나, 사용에 주의해야 합니다. 참조 대상이 nil일 경우에도 미소유 참조를 사용하면 런타임 오류가 발생할 수 있습니다. 따라서, 미소유 참조를 사용할 때는 주의해서 사용해야 합니다.

 

 

 

 

 

**추가**

도구를 이용한 메모리 누수 확인 방법

Swift에서 순환 참조로 인한 메모리 누수를 확인하는 방법 중 하나는 "Instruments"를 사용하는 것입니다. "Instruments"는 Xcode에 포함된 프로파일링 도구로, 앱 실행 중 메모리 사용량 등의 정보를 제공합니다.

다음은 "Instruments"를 사용하여 순환 참조로 인한 메모리 누수를 확인하는 방법입니다.

  1. Xcode를 열고 "Product" 메뉴에서 "Profile"을 선택합니다.

  • "Instruments" 창이 열리면 "Leaks"를 선택합니다.

  • "Instruments"가 앱을 실행시키고, 메모리 사용량 등의 정보를 수집합니다.

  • 앱 실행 중 메모리 누수가 발생하면 "Leaks" 창에서 해당 객체를 확인할 수 있습니다.

또 다른 방법은 "Debug Memory Graph" 기능을 사용하는 것입니다. 이 기능은 Xcode의 디버거를 사용하여 객체 간의 참조 관계를 시각화하여 보여줍니다. 이를 통해 순환 참조로 인한 메모리 누수를 쉽게 확인할 수 있습니다.

다음은 "Debug Memory Graph" 기능을 사용하여 순환 참조로 인한 메모리 누수를 확인하는 방법입니다.

  1. Xcode를 열고 디버그 모드로 앱을 실행합니다.

  • 디버거 창에서 "Debug Memory Graph" 버튼을 클릭합니다.
  • "Debug Memory Graph" 창에서 객체 간의 참조 관계를 시각화하여 볼 수 있습니다.
  • 순환 참조가 있는 경우 해당 객체들을 클릭하여 확인할 수 있습니다.
728x90
반응형
LIST

'iOS' 카테고리의 다른 글

iOS APP 화면 구성  (0) 2023.04.11
NSObject  (0) 2023.04.11
NSLog, CFShow의 차이  (0) 2023.04.11
가비지 컬렉터(Garbage Collector)  (0) 2023.04.11
KVO - key-value-observing  (0) 2023.03.08