일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- UIKit
- iOS개발
- Clean Architecture
- 공부
- 알고리즘 공부
- Python
- Kotlin
- 백준온라인저지
- 정렬
- Swift공부
- greedy algorithm
- 프로그래머스
- swift
- Android
- BFS
- ios
- 파이썬 풀이
- 알고리즘
- Level 1
- 앱개발
- 그리디 알고리즘
- 백준 온라인 저지
- Autolayout
- 안드로이드 공부
- error
- 오토레이아웃
- 파이썬
- Algorithm
- dfs
- SwiftUI
- Today
- Total
Tori의 개발 공부
weak self 를 사용하지 않아도 되는 경우 본문
결론적으로 클로저에서 weak self를 사용하지 않아도 되는 경우는 다음과 같은 두 가지가 있다.
1. non-escaping 클로저인 경우
2. DispatchQueue.main.asyncAfter 함수를 사용할 경우
escaping 클로저와 non-escaping 클로저
함수 파라미터로 전달된 클로저를 함수 실행이 종료된 후 실행되는 것을 함수를 탈출(escape)한다고 한다.
즉 escaping클로저는 파라미터로 전달된 클로저가 탈출하여 함수 종료 후에 실행되는 클로저를 의미한다.
escaping | non-escaping | |
실행 시점 | 함수 종료 후 | 함수 내에서 코드 호출 즉시 |
외부 저장 여부 | 가능 | 불가능 |
두 함수의 차이는 외부 저장 여부에 대해서도 다르다.
non - escaping 클로저는 함수 외부 변수(or 상수)에 저장하려고 하면 컴파일 에러가 발생한다.
함수 내부에서만 사용되고 말기 때문에 외부 변수로 탈출시킬 수 없기 때문이다.
반대로 escaping 함수는 함수 밖으로 탈출 가능하기 때문에 함수 외부의 변수(or 상수)에 저장이 가능하다.
escaping과 weak self의 상관관계
그렇다면 escaping이 weak self와 무슨 관련일까?
non-escaping은 함수 밖으로 탈출하지 못한다는 특징을 가지고 있다.
그렇기 때문에 함수 호출이 끝나면 해당 클로저는 더 이상 사용되지 않음을 보장한다.
따라서 자동으로 클로저를 메모리에서 해제시킨다
다음과 같은 상황이 있다 하자.
인스턴스 내부에 있는 클로저에서 self를 참조하고 있다.
non-escaping 클로저는 클로저가 함수범위를 벗어날 일이 없음을 보장하기 때문에 클로저 실행 시 자동으로 메모리에서 해제되어 self의 strong reference를 증가시키지 않는다.
하지만 escaping클로저의 경우 함수 밖으로 벗어날 수 있기 때문에 reference count를 증가시킬 수 있다.
만약에 위의 그림처럼 서로의 RC를 증가시킨 경우
다음과 같은 RC 구조를 가지게 된다.
만약 인스턴스 변수에 nil을 할당하게 될 경우
- 인스턴스 변수가 참조하고 있던 RC 가 사라져 -1 된다 → 인스턴스의 RC = 1
- 클로저에서는 self의 강한 참조로 인해 self(인스턴스)가 메모리에서 해제될 때까지 기다림
- 인스턴스는 클로저에서 참조하는 RC가 남아있어 메모리에서 해제되지 못함
이처럼 순환참조를 발생시키게 된다.
예제 코드
// non - escaping 클로저 사용
class Class1 {
func format(_ value: Int) -> String { return String(value) }
func test() {
// 대표적인 고차함수들은 non-escaping
let formatted = [1,2,3,4,5].map { num in
self.format(num)
}
print(formatted)
}
deinit {
print("Class1 deinit")
}
}
var class1: Class1? = Class1()
class1?.test()
class1 = nil
// escaping 클로저 사용
class Class2 {
var handler: ((Int) -> Void)? = nil
func format(_ value: Int) -> String { return String(value) }
func test() {
handler = { value in
let formatted = self.format(value)
print(formatted as Any)
}
}
deinit {
print("Class2 deinit")
}
}
var class2: Class2? = Class2()
class2?.test()
class2 = nil
class2는 deinit 되지 않은 모습을 볼 수 있다.
handler = { [weak self] value in
let formatted = self?.format(value)
print(formatted as Any)
}
하지만 Class2에 weak self를 붙이게 되면 deinit 되는 걸 확인할 수 있다.
DispatchQueue.main.asyncAfter
해당 함수는 escaping 클로저로 전달해 강한 참조를 발생하지만
deadline이 존재하여 deadline동안만 메모리에서 유지함
즉 deadline이 끝나면 클로저가 메모리에서 해제됨으로 순환참조가 발생하지 않는다.
그런데 여기서 주의할 점이 있다.
순환참조가 발생하지 않아 weak self를 사용하지 않아도 되는 데 사용할 경우
생각한 동작과 다르게 동작할 수 있다.
class Class2 {
func format(_ value: Int) -> String { return String(value) }
func test() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let formatted = self.format(10)
print("formatted: \\(formatted as Any)")
}
}
deinit {
print("Class2 deinit")
}
}
class Class1 {
let class2: Class2
init() {
class2 = Class2()
}
func someEvent() {
class2.test()
}
deinit {
print("Class1 deinit")
}
}
var class1: Class1? = Class1()
class1?.someEvent()
class1 = nil
Weak self에 따른 실행 차이
- weak self 사용 : class1이 deinit 되고 1초 뒤 format함수가 실행된 뒤 자동으로 class2가 deinit 된다.
- weak self 사용 X: 바로 1,2 둘 다 deinit 되고 1초 뒤 test함수가 실행은 되지만 nil을 반환한다.
weak self를 쓰지 않으면 이와 같은 메모리 상황에서
class 1이 메모리에서 사라져도 class2의 rc = 1 이기 때문에 사라지지 않아
DispatchQueue.main.asyncAfter에서 self를 참조할 수 있다.
그리고 deadline 후 클로저는 자동으로 메모리에서 해제되면서 self 참조도 사라진다.
즉 클로저 참조가 사라지면서 class 2의 RC도 0이 되므로 class도 메모리에서 해제된다.
반대로 여기서 클로저의 참조가 weak가 될 경우 class2의 RC를 증가시키지 않는다.
즉 처음부터 RC = 1이고 class1이 nil이 되면 RC가 0이 되어 class2가 메모리에서 사라진다. (deinit 호출)
즉 DispatchQueue.main.asyncAfter 안의 내용이 실행될 때는 이미 self가 nil 이기 때문에 출력이 이상하게 나왔던 것이다.
func test() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
print(self)
let formatted = self?.format(10)
print("formatted: \\(formatted as Any)")
}
}
이와 같이 weak self를 쓰고 self를 프린트해 보면 nil이 찍히는 모습을 알 수 있다.
이번 공부로 인해 단순히 클로저를 쓴다고 해서 weak self를 사용하면 안 된다는 사실을 알 수 있었다.
특히 DispatchQueue.main.asyncAfter의 경우는 실행 결과가 다르게 나올 수 있다는 사실에 놀랐다.
클로저들의 특성 및 생명주기를 잘 이해하고 weak self를 써야 한다는 사실을 알 수 있었다.
'IOS App Programming > Swift' 카테고리의 다른 글
접근제한자 정리 및 성능 향상 이유 + final 제한자 (0) | 2024.02.23 |
---|---|
CollectionType 차이점 및 효율적인 사용 시점 (0) | 2024.02.22 |
ARC란? - 강한참조와 약한참조, [weak self] (1) | 2024.01.31 |
[Swift] mutating 키워드란? (0) | 2023.05.10 |
[Swift] 프로토콜 지향 프로그래밍 - 프로토콜 초기 구현 (0) | 2023.05.06 |