이전에 학습할 때 순환참조에 의한 메모리 누수, 그리고 그것을 해결하기 위한 약한참조에 대해 알아보았다.
오늘은 살짝 다른 주제에 대해서 정리하려고 한다
강한 참조 순환 문제는 두 인스턴스끼리의 참조일 때만 발생하는 것 외에도 클로저가 인스턴스의 프로퍼티일 때나, 클로저의 값 획득 특성 때문에 발생한다.
예를들어, 클로저 내부에서 self.someProperty
처럼 인스턴스의 프로퍼티에 접근할 때나 클로저 내부에서 self.someMethod()
처럼 인스턴스의 메서드를 호출할 때 값 획득이 발생할 수 있는데, 두 경우 모두 클로저가 self를 획득하므로 강한참조 순환이 발생한다.
import Foundation
class Person {
let name: String
let hobby: String?
lazy var introduce: () -> String = {
var introduction: String = "My name is \(self.name)"
guard let hobby = self.hobby else {
return introduction
}
introduction += " "
introduction += "My hobby is \(hobby)"
return introduction
}
init(name: String, hobby: String? = nil) {
self.name = name
self.hobby = hobby
}
deinit {
print("\(name) is being deinitialized")
}
}
var kane: Person? = Person(name: "kane", hobby: "soccer")
print(kane?.introduce() as! String)
kane = nil
//My name is kane My hobby is soccer
코드 마지막에 결과를 보면 kane
변수에 nil을 할당했지만 deinit이 호출되지 않은 것을 볼 수 있다... 메모리 누수가 발생한 것!!
print(kane?.introduce() as! String)
kane = nil
이렇게 한다면???
//kane is being deinitialized
그렇다.. 해제가 된다
자기소개를 하려고 introduce 프로퍼티를 통해 클로저를 호출하면 그 때 클로저는 자신의 내부에 있는 참조 타입 변수등을 획득한다. 문제는 여기부터인데...
클로저는 자신이 호출되면 언제든지 자신 내부의 참조들을 사용할 수 있도록 참조 횟수를 증가시켜 메모리에서 해제되는 것을 방지하는데, 이때 자신을 프로퍼티로 갖는 인스턴스의 참조 횟수도 증가시킨다.
이렇게 강한참조 순환이 발생하면 자신을 강한참조 프로퍼티로 갖는 인스턴스가 메모리에서 해제될 수 없습니다.
우리는 이러한 문제를 획득목록을 통해 해결할 수 있다. 획득목록은 클로저 내부에서 참조 타입을 획득하는 규칙을 제시할 수 있는 기능이다.
그러니깐 앞의 코드에서 self
를 약한 참조로 지정할 수 있다!!!! 라는 것이다
var a = 0
var b = 0
let closure = { [a] in
print(a, b)
b = 20
}
a = 10
b = 10
closure() // 0 10
print(b) // 20
위 코드를 보면 변수 a의 경우 획득목록을 통해 클로저가 생성될 때 값 0을 획득했지만, b의 경우는 값 획득이 이뤄지지 않았다
나중에 a, b 값이 할당되었지만 closure가 실행될 때 a의 경우만 이전의 0
값을 출력한다.
B.U.T 이것은 a, b가 값 타입이기 때문이다
그럼 참조타입의 경우는?
class SimpleClass {
var value: Int = 0
}
var x = SimpleClass()
var y = SimpleClass()
let closure = { [x] in
print(x.value, y.value)
}
x.value = 10
y.value = 10
closure() // 10 10
변수 x의 경우는 획득목록을 통해 값이 지정되었지만, y의 경우는 그렇지 않다.
But, 출력되는 것을 보면 똑같이 참조된다.
class SimpleClass {
var value: Int = 0
deinit {
print("deinitialized \(value)")
}
}
var x: SimpleClass? = SimpleClass()
var y: SimpleClass? = SimpleClass()
let closure = { [x, y] in
print(x?.value, y?.value)
}
x?.value = 10
y?.value = 10
closure()
x = nil
y = nil
//Optional(10) Optional(10)
그리고 캡처리스트에 x, y값을 넣은 뒤에 nil 값을 할당해주면
deinit
이 출력되지 않는 것을 볼 수 있다
여기서 그럼 약한 참조로 해줘볼까???
약한획득 (weak capture list)의 경우
획득목록에서 획득하는 상수가 옵셔널 상수로 지정된다. 그 이유는 차후에 클로저 내부에서 약한획득한 상수를 사용하려고 할 때 이미 메모리에서 해제된 상태일 수 있기 때문이다. 해제된 후에 접근하려 하면 잘못된 접근으로 오류가 발생하므로 안전을 위해 약한획득은 기본적으로 타입을 옵셔널으로 사용하는 것이다.
class SimpleClass {
var value: Int = 0
deinit {
print("deinitialized \(value)")
}
}
var x: SimpleClass? = SimpleClass()
var y = SimpleClass()
let closure = { [weak x, unowned y] in
print(x?.value, y.value)
}
x = nil
//deinitialized 0
y.value = 10
closure() // nil 10
획득목록에서 x를 약한참조로
, y를 미소유참조
하도록 지정 했다
의도한 대로 클로저 내부에서 사용하더라도 클로저는 x가 참조하는 인스턴스의 참조횟수를 증가시키지 않았다. 그렇게 되면 변수 x가 참조하는 인스턴스가 메모리에서 해제되어 클로저 내부에서도 더 이상 참조가 불가능한 것을 볼 수 있다.
y의 경우 미소유 참조를 했기 때문에 클로저가 참조 횟수를 증가시키지 않았지만, 만약 메모리에서 해제된 상태에서 사용하려 한다면 실행 중에 오류로 애플리케이션이 강제로 종료될 가능성이 있다!!
reference) 야곰의 스위프트 프로그래밍