Swift는 ARC(Automatic Reference Counting)를 통해서 메모리 관리가 이루어진다. ARC는 기존 개발자가 직접 코드를 작성하여 메모리를 관리해야하는 부분을 분석하여 적절하게 레퍼런스 감소 코드를 삽입해 주어, 실행 중에 별도의 메모리 관리를 하지 않도록 도와준다. 하지만, 메모리 참조 순환에 대한 것은 개발자가 직접 관리해 주어야 한다. iOS 개발을 하다보면 흔히 볼 수 있는 [weak self]
가 무엇인지 알아보자.
특정 주소값을 참조하고 있는 인스턴스의 카운트를 세는 역할을 한다.
참조 카운트는 참조 타입에만 해당된다. 구조체나, 열거형과 같은 Value Type에는 해당하지 않는다. 클래스의 인스턴스가 생성되면 Heap에 메모리가 할당된다. 초기 인스턴스의 RC값은 1(자기 자신)이된다. 해당 인스턴스를 참조 하고 있는 다른 객체를 선언하는 경우 RC 카운트가 증가하게 된다.
RC 값이 0이 되는 경우, 즉, 더이상 해당 인스턴스를 참조하지 않는 경우에만 메모리에서 해제가 가능하다. 자동으로 메모리에서 해제시켜주지 않으므로, 이처럼 RC값이 0이 되지 않는 경우 메모리에서 계속 남아있게 되고, 메모리 누수를 발생시킨다.
이러한 메모리 누수를 방지하기 위하여 Weak, Unowned 와 같은 타입을 사용할 수 있다.
Strong 타입의 변수는 인스턴스 생성시 참조 카운트를 증가시킨다.
Swift에서는 대부분 강한 참조를 사용하고 있다. Property를 선언시 default 값이 Strong인 것만 봐도 알 수 있다. 일반적인 linear reference flow를 따를 경우에는 문제가 되지 않는다. 부모 객체가 메모리에서 해제 되고 retain count를 감소시키면 모든 자식 객체들도 또한 사라진다.
예를 들면 다음과 같다. 클래스 Car는 brand이름을 가지고 있다.
class Car {
var brand: String
init(brand: String) {
self.brand = brand
print("Car of the brand \(brand) allocated")
}
deinit {
print("Car of the brand \(brand) is being deallocated")
}
}
do {
let tesla = Car(brand: "tesla")
}
car 객체의 생성은 do 스코프안에서 이루어진다. 스코프 안에서 객체가 생성되고 스코프가 종료되는 순간 객체는 메모리에서 해제된다.
//output
Car of the brand tesla allocated
Car of the brand tesla is being deallocated
하지만, 이렇게 Strong 타입인 경우, 클래스 인스턴스 사이에 강한 참조 순환이 발생할 수 있다. Car는 Owner를 소유하고, Owner는 Car를 소유하는 경우이다.
class Car {
var brand: String
var owner: Owner?
init(brand: String) {
self.brand = brand
print("Car of the brand \(brand) allocated")
}
deinit {
print("Car of the brand \(brand) is being deallocated")
}
}
class Owner {
var name: String
var car: Car?
init(name: String) {
self.name = name
print("Owner \(name) allocated")
}
deinit {
print("Owner \(name) deallocated")
}
}
do {
let tesla = Car(brand: "tesla")
let misterX = Owner(name: "Mister X")
tesla.owner = misterX
misterX.car = tesla
}
Car와 Owner는 서로 강한 참조를 하고 있다. 때문에, do 스코프가 종료되더라도 두 인스턴스는 메모리에서 해제되지 못한다. 서로를 참조하고있기 때문에 메모리에서 해제되지 못하는 경우를 강한 참조 순환이라고 한다. 이러한 문제를 해결하기 위한 방법이 Weak references이다.
Strong References와 다르게 참조 카운트(RC)를 증가시키지 안는다. ARC에서 메모리가 해제되면 같이 사라지게 된다. 자동으로 nil값을 가지게 되는데, weak reference가 optional 타입이라는 것을 유추할 수 있다.
앞서 들었던 예시에서 프로퍼티를 weak 타입으로 변경하면, 정상적으로 메모리에서 해제되는 것을 확인할 수 있다.
class Car {
weak var owner: Owner?
...
}
class Owner {
weak var car: Car?
...
}
...
//output
Car of the brand tesla allocated
Owner Mister X allocated
Owner Mister X deallocated
Car of the brand tesla is being deallocated
Unowned 참조는 weak과 거의 유사하다. Unowned 역시 참조 카운트(RC)를 증가시키지 않는다. 한가지 Weak과 다른점은 Unowned는 Optional 타입이 아니라는 것이다. Optional 타임이 아니므로 메모리 해제시 자동으로 nil값을 가질 수 없다. 때문에, nil이 아님을 보장 할 수 있는 경우에만 Unowned 타입을 사용해야한다. 애플에 따르면, 참조 코드가 동시에 메모리에서 해제되는 경우 Unowned를 사용하기 가장 좋은 경우라고 한다.
Strong, Weak 상황에 따른 메모리 상태를 그림으로 나타내 보려고한다.
localScopeFunction()
함수를 하나 선언하고, 내부에서 뷰컨트롤러를 생성한다.
func localScopeFunction(){
let vc = ViewController()
vc.doSomething()
}
localScopeFunction
함수를 호출하게되면 Stack 영역에 쌓이게 된다.localScopeFunction
함수는 ViewController를 생성하므로 RC 카운트가 하나 증가한다.doSomething()
에서는 글로벌 큐와 메인 큐에서 ViewController의 name을 출력하는 일을 수행한다.class ViewController : UIViewController {
var name : String = "I'm a View Controller"
func doSomething(){
DispatchQueue.global().async {
sleep(3)
print("in Global Queue : \(self.name)")
DispatchQueue.main.async {
print("in main Queue : \(self.name)")
}
}
}
deinit{
print("메모리 해제")
}
}
5.localScopeFunction
가 종료된다.
글로벌 큐 내부 클로저 동작을 수행한다.
메인 큐 내부의 클로저 동작을 수행한다.
모든 클로저의 수행이 끝이 난다. 이제서야 ViewController의 RC가 0이 되어, 메모리에서 해제가 가능해진다.
결과
//output
in Global Queue : I'm a View Controller
in main Queue : I'm a View Controller
메모리 해제
4-7의 과정은 비동기적으로 일어나므로 순서를 보장할 수 없다. 하지만 여기서 중요한 것은 ViewController의 메모리가 해제되는 시점이다. localScopeFunction()
의 스코프가 종료 되었음에도 ViewController는 메모리에서 해제되지 못한다.
doSomething()
의 클로저에 [weak self]
를 추가한다.
func doSomething(){
DispatchQueue.global().async { [weak self] in
sleep(3)
print("in Global Queue : \(self.name)")
DispatchQueue.main.async {
print("in main Queue : \(self.name)")
}
}
}
5.localScopeFunction
가 종료된다.
ViewController를 참조하고 있던 함수가 스택에서 제거되므로, RC 카운트가 감소한다.
RC 카운트가 0이 되었으므로 ViewController는 메모리에서 해제된다.
결과
//output
메모리 해제
in Global Queue : nil
in main Queue : nil
localScopeFunction()
의 스코프가 종료 됨과 동시에 ViewController는 메모리에서 해제되었다. 때문에 글로벌 큐, 메인 큐에서 ViewController의 name
이 nil
인 것을 확인 할 수 있다.
https://slacktime.org/strong-weak-unowned-reference-counting-in-swift-5813fa454f30
https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html