-
Closure 의 Capture 와 Self개발/Swift 2022. 11. 21. 15:58
var a = 0 var b = 0 let closure = { print(a,b) } a = 5 closure()
위와같은 상황에 클로져가 호출될때
a가 직전에 5로 변경되었으므로
5 0 이 호출 된다.
이게 클로져의 기본동작이다.
원본값이(a) 변하면 클로져 내부값도 변하는 “Reference capture”를 한다.
var a = 0 var b = 0 let closure = { [a] in print(a,b) } a = 5 closure()
위 클로져에서는 [a] in 을 넣어줬다.
이때 결과값은 0 0 이 된다.
클로져를 "생성" 할때 변수 a 를
a라는 상수로 capture한다. (따라서 변경 불가)
위와 같은 [a] 이런것을 Capture List라 한다.
따라서 클로져 내부에서 Capture List 안에 넣어주면 이것들이 상수가 되므로
상수값 let a = 0 값을 가지고 있는것이다.
그래서 아래와 같이 수정할수 없다.
Class 에서 Capture List
class TestClass{ var value = 0 } let testA = TestClass() let closure = {[testA] in print(testA.value) } testA.value = 10 closure()
위 클로져 에서는 testA 를 capture했다.
그러면 클로져 생성시의 testA.value 값인
0이 호출되기를 바란다.
하지만 결과값은 호출직전에 넣어줬던 10이 호출된다.
class 는 참조타입이며 참조타입은 값타입과 다르다
참조타입에서는 클로져의 기본 동작인 reference capture를 한다.
그러면 class 에서 캡쳐리스트를 왜 사용할까?
왜냐하면 참조타입은 메모리에서 해제될수도 있기 때문이다.
위의 상황에서 클로저를 호출하기 이전에
testA라는 변수가 nil이 되었다면
nil.value에 접근했으므로 크래쉬가 발생할것이다.
그래서 캡쳐리스트를 써서 참조 카운팅을 증가시켜
해제되지 못하도록 한다.
struct Actor { //or class var name: String init(name: String) { self.name = name } func printName() { print("Name \(self.name)") } } var actor = Actor(name: "민수") actor.name = "철수" let closure = actor.printName actor.name = "규진" closure()
위 closure가 호출되면 무엇이 호출될까?
Actor가 struct가 아닌 class면 어떤게 호출될까?
printName()이 self.name에 접근하고있고
상수 closure에 할당될때 self가 캡쳐될 것이다.
따라서 struct의 경우 철수
class의경우 규진을 호출한다.
CaptureList 와 RC
클로져에서 참조타입은 기본적으로
강한 참조를 한다.
let closure = { print(self.value) }
클로져에서 self 를 강한참조 하여
self의 RC가 증가하게 되면 메모리릭 위험에 (서로 강한참조 할경우)
노출 될 수가 있다.
우리가 흔히 클로져에 [weak self] in 을 써야하는 이유이다.
정리하자면 클로져의 Reference Capture는
기본적으로 강한 참조를 하게 되어
순환참조를 피하기 위한 약한 참조를 위해서는
캡쳐리스트에 weak 을 사용해야한다.
Self
그런데 escaping closure에서는 왜 굳이 self를 써야할까
그냥 value에 접근하면 컴파일 에러가 나고
self.value에 접근하고있다.
Reference to property 'value' in closure requires explicit use of 'self' to make capture semantics explicit
self는 이미 자동으로 capture list에 들어가있다.
이를 통해 클로져가 메모리에서 해제되기 전까지
self가 해제되지 않도록 한다.
self가 참조 타입인경우 클로져 실행시 reference capture를 사용하기 때문에
self가 메모리에서 해제될수도 있다.
클로져가 실행되면서 value에 접근했을시 만약 self가 해제된 상태라면?
self.value가 nil이므로 앱이 크래쉬 날것이다.
이런 상황을 방지하기 위해
클로져에서는 객체 내부에 직접 접근할수 없고
self를 통해서만 가능하다.
Weak Self
escaping 클로져는 해당 클로져가 메소드 종료 이후에도
쓰이므로 메모리에 남아있는다.
escaping 클로져 에서 순환 참조를 일으킬수있는 경우는 이렇다.
- 클로저가 객체 프로퍼티에 저장됨
- 클로저가 다른 클로저로 전달됨
- 클로저 안 객체가 해당 클로저를 강한 참조함
- 클로저 안 객체가 전달된 클로저를 강한 참조함
즉 다른 프로퍼티에 저장되거나 다른 클로저에 전달 될 경우가 없으므로
순환참조가 일어나지 않는다.
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.test() // 이런경우 weak self 필요 x }
반대로 클래스 내부 프로퍼티에 클로저가 저장되고
클로저 내부에서 클래스를 self 참조할 경우 문제가 발생한다
class ViewModel { func format(_ value: Int) -> String { return "\\(value)!!" } var handler: ((Int) -> Void)! func code() { handler = { value in let formatted = self.format(value) print(formatted) } } } var vm:ViewModel? = ViewModel() print(CFGetRetainCount(vm)) //2 vm?.code() print(CFGetRetainCount(vm)) //3
'개발 > Swift' 카테고리의 다른 글
[Swift] Compositional Layout - 헤더, 다양한 layout적용 (0) 2022.11.23 [Swift] Compositional Layout - 레이아웃 그려보기 (0) 2022.11.22 DispatchQueue 에서 주의할 데드락 현상 (0) 2022.11.21 Rxswift - ObserveOn , SubscribeOn (0) 2022.11.18 Rxswift 에러 핸들링 (complete, dispose 대응) (0) 2022.11.18