일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
29 | 30 | 31 |
- Apple # HIG #iOS15 #iOS14 #Human #Interface #Guidelines #Apple developer # Apple human interface guidelines
- 다형성
- 행동 호환성
- 객체지향
- 명령-쿼리 분리
- OOP
- 책임주도설계
- 유연한 설계
- 알고리즘
- 메서드를 통한 해결
- 유여난 설계
- OCP
- 추상화
- 합성
- iSP
- 설계 재사용
- 객체 생성 사용 분리
- 일관성 있는 협력
- Swift#flatMap#map#Monad#함수형 프로그래밍#Optional
- 하향식 접근
- 믹스인
- 의존성
- dip
- 컴파일 타임 의존성
- '기존 설계 재사용
- 서브 타이핑
- 상속 조합 폭발적 증가
- 오브젝트
- 런타임 의존성
- 상속
- Today
- Total
도니의 iOS 프로그래밍 세상
[Swift] ARC weak self를 하는 이유(delayed deinitialization, memory leak) - ARC 응용편 5탄 본문
[Swift] ARC weak self를 하는 이유(delayed deinitialization, memory leak) - ARC 응용편 5탄
Donee 2023. 1. 15. 23:40지난 포스팅글에서는 ARC의 정의, weak unowned, strong reference cycle, closure와 clousure 순환참조에 대해서 알아봤습니다.
이번 시간에는 UIkit에서 흔하게 발생할 수 있는 delayed deinitialization, memory leak에 대해서 알아보겠습니다.
Escaping Closure와 Non-escaping closure 두가지 사례로 나뉩니다.
Escaping Closure에서의 Memory leak
Escaping Closure는 함수가 반환된 후 호출되는 클로저를 의미합니다.
해당 클로저는 함수가 실행이 완료된 이후에도 별도로 동작하기 때문에 클로저 실행이 완료되어야 메모리에서 해제가 가능합니다.
이러한 성질로 인해 메모리 leak을 유발시키는 케이스를 확인하겠습니다.
Closure와 Memory leak
이전 시간에 배웠던 대로, 객체가 클로저를 가지고, 클로자가 다시 객체를 가진다면 이는 메모리 leak의 원인이 됩니다.
class DestinationViewController: UIViewController {
private var vcValue: String = ""
override func viewDidLoad() {
super.viewDidLoad()
exampleFunction { str in
self.vcValue = str
print(self.vcValue)
}
}
func exampleFunction(completion: @escaping (String) -> Void) {
Task {
try await Task.sleep(for: .seconds(5))
completion("Delayed Deinitialization Test")
}
}
deinit {
print("DestinationVC deinit")
}
}
vcValue라는 멤버 변수가 존재하고, 이 값은 exampleFunction의 클로저로부터 받은 값을 대입시킵니다.
exampleFunction은, closure를 통해서 값을 5초 뒤에 전달하게 됩니다.
현재 ViewController는 클로저와 순환참조 관계입니다.
ViewController는 클로저를 가르키고 있고, 클로저는 다시 ViewController를 가르키고 있는 상황입니다.
그림으로 본다면 다음과 같습니다.
이전 포스팅에서 배웠듯 memory leak의 상황이죠.
completion handler가 값을 리턴하기 전, 해당 ViewController를 pop하거나 dismiss하게 된다면 해당 뷰 컨트롤러는 메모리에서 바로 사라지지 않습니다.
정확히 말하자면 메모리에서 사라지지만, 늦게 사라지는 delayed deinitialization이 발생합니다.
클로저 실행이 종료되고, 순환참조가 해결되어야 만 ViewController는 메모리에서 해제되기 때문입니다.
delayed deinitialization
delayed deinitialization이란 메모리로부터 “해제”는 되지만, “지연돼서 해제”되는 현상을 가르킵니다.
위의 코드의 예시를 조금더 자세히 동영상으로 보도록 하겠습니다!
Navigation Button을 누르게 되면 위의 ViewController로 이동하게 됩니다.
그리고나서 backButton을 누르면 우리는 어떻게 생각할까요?
생성된 ViewController가 메모리에서 즉시 해제 된다고 생각합니다.
하지만, ViewController는 아직 reference가 존재하기 때문에 해제될 수 없습니다.
ViewController와 클로저가 서로 순환참조하고 있는 형태이기 때문입니다.
그리하여 클로저가 ViewController의 참조를 해제해야, 정상적으로 메모리에서 해제됩니다.
위험성
광고 Banner가 존재하고, 해당 배너는 1초에 한번 새로운 ViewController를 만들고, 사라집니다.
이때, 배너들은 광고 SDK를 통해서 서버에서 데이터를 요청합니다.
URLSession을 사용하여 Escaping handler를 통해 데이터를 요청 하고, 서버 요청 만료시간을 3분으로 상황을 가정해봅시다.
이렇다면, 3분동안 180개의 ViewController가 메모리에 존재하게 됩니다.
이럴경우 심각한 메모리 leak을 유발해 App crash로 프로그램 종료를 야기시킵니다.
따라서 delayed deinitialization을 방지하기 위해선 순환 참조의 고리를 끊어주어야 합니다.
해결법
이전 포스팅에서 우리는 unowned와 weak를 capturing list에 넣는다면 순환참조 고리를 해결할 수 있다고 설명하였습니다.
그렇다면 해당 해결법을 위 코드에 적용시켜보겠습니다.
override func viewDidLoad() {
super.viewDidLoad()
exampleFunction { [weak self] str in
self?.vcValue = str
print(self?.vcValue)
}
}
우리는, weak self를 적용시킴으로써 순환 참조 문제를 해결했습니다.
그렇다면 결과는 어떨까요?
위와 같이, ViewController가 dismiss될때 메모리 해제가 정상적으로 되는 걸 확인할 수 있었습니다.
이처럼, weak self를 사용한다면 ViewController와 클로저 간 순환참조 문제를 아주 쉽게 해결할 수 있습니다.
unowned self
unowned는 weak와 동일하게 참조를 해결할 수 있는 좋은 방법입니다.
하지만 unowned는 “값이 존재”한다라는 가정을 가지고 사용하는 참조방법입니다.
값이 없을때 nil을 할당하는 weak와 달리, 강제 참조를 하게 됩니다.
만약 값이 존재하지 않았을때, 해당 값을 참조하면 app crash라는 side effect를 가지고 있습니다.
따라서 ViewCoontroller life cycle이 끝난 다음 값을 “호출”하지 않는다는 확신이 들때만 사용하는 것이 좋습니다.
예시를 통해 다시한번 설명해드리겠습니다
class DestinationViewController: UIViewController {
var start = 0
override func viewDidLoad() {
super.viewDidLoad()
timerOn()
}
func timerOn() {
Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { [unowned self] timer in
if self.start < 10 {
print("\(self.start)")
self.start += 1
} else {
print("timer end")
timer.invalidate()
}
}
}
deinit {
print("Destination View Controller dismissed")
}
}
뷰가 로드되고, 3초뒤에 타이머가 종료됩니다. 이때 타이머 내부 클로저는 “Escaping”입니다.
unowned를 사용해서 ViewController를 참조하고 있기에 Reference Count를 증가시키지 않습니다.
따라서, ViewController가 dismiss된다면, 뷰는 메모리에서 해제가 됩니다.
위 코드를 실행한 결과 App Crash가 발생했습니다.
ViewController는 unowned를 사용해서 메모리에서 해제됐을지라도, escaping closure는 실행이 완료될때까지 메모리에 남아있는 상황입니다.
타이머 시작 3초뒤, 클로저는 이미 메모리에서 해제된 ViewController를 호출합니다.
unowned는 weak와 달리 메모리 해제될 때 값을 자동으로 nil처리 해주지 않습니다.
따라서 존재하지 않는 값을 참조하게 되고 이는 memory crash를 발생시켜 앱을 종료시킵니다.
결국 unowned를 사용할땐 ViewController해제보다 클로저 해제가 먼저된다는 것이 보장되어야합니다.
Non-Escaping Closure
non Escaping closure는 함수가 종료되기 전 실행되는 closure이다.
그리하여, Escaping처럼 함수 종료후 클로저가 별도로 실행되면서 겪는 Delayed Deinitialization과는 조금 다른 문제가 발생합니다.
Delayed Deinitialization
Non-escaping 함수의 경우, 메모리 지연 해제 현상은 Escaping Handler과 다른 현상으로 발생합니다.
하지만, 이는 순환참조의 문제라기 보단, 클로저 동작 방식으로 인해서 발생합니다.
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.popViewController(animated: true)
nonEscaping { str in
for i in 1...10000000 {
print("x")
}
print(self.number)
}
}
위의 경우, escaping closure와 다르게 viewDidLoad함수는 non Escaping closure가 끝날때까지 serially하게 기다려야 합니다.
따라서 for문 연산이 끝날때까지 View는 어떠한 작업도 할 수 없기 때문에 메모리 해제또한 되지 않습니다.
하지만 이는 앞서말한 memory leak과는 다르게 연산 과정에서의 과도한 연산으로 인한 문제라고 보는편이 맞습니다.
그렇다면 memory 지연해제를 non-escaping에서는 어떻게 재연해야 할까요?
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.popViewController(animated: true)
DispatchQueue.global(qos: .default).async {
nonEscaping { str in
for i in 1...10000000 {
print("x")
}
print(self.number)
}
}
}
func nonEscaping(completion: (String) -> Void) {
completion("complete")
}
이렇게 할 경우, non escaping함수는 concurrent하게 작동하기 때문에 ViewController가 pop되고 나서도 연산을 계속해서 진행합니다.
하지만 이는, DispatchQueue의 async의 클로저가 escaping이기 때문에 가능한것입니다.
따라서 non-escaping으로 인한 Delayed deinitialization이 나타나는 것은 아닙니다.
Memory Leak
Delayed deinitialization는 발생하지 않았지만, non-Escaping에서는 Memory leak은 발생합니다. 밑의 케이스를 보도록 하겠습니다~!
class DestinationViewController: UIViewController {
var customView: CustomView = CustomView()
var buttonClicked = false
var onTap:(() -> Void)?
func setupCustomView(){
var timesTapped = 0
onTap = {
timesTapped += 1
print("button tapped \\(timesTapped) times")
self.buttonClicked = true
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupCustomView()
}
deinit {
print("DestinationVC deinit")
}
}
위의 사례에서, Destination View Controller가 NavigationController에서 pop되었을 경우 Memory leak이 발생합니다.
현재 참조상태는 다음과 같습니다.
서로가 서로를 참조하는 순환 참조가 발생하고 있습니다. 이를 해결하기 위해선, 두개의 참조중 최소 하나를 weak로 만드는 작업이 필요합니다.
하지만 onTap 클로저이기에 weak로 하는것은 불가능하여 weak self를 사용해야 합니다.
weak self
func setupCustomView(){
var timesTapped = 0
onTap = { [weak self] in
timesTapped += 1
print("button tapped \\(timesTapped) times")
self?.buttonClicked = true
}
}
결론적으로, 우리는 weak self를 사용하는 이유는 “delayed deinitialization”을 방지하기 위함이다.
weak self대신, unowned self를 사용할 수도 있지만, app crash의 이유로 life cycle이 분명한 경우가 아니면 사용하지 않는다.
결론
Escaping closure의 경우
결론적으로, 우리는 weak self를 사용하는 이유는 “delayed deinitialization”을 방지하기 위함이다.
그렇지 않으면, ViewController는 메모리에서 지속적으로 쌓이게 되고 이는 App Crash를 발생시킨다.
weak self대신, unowned self를 사용할 수도 있지만, app crash의 이유로 life cycle이 분명한 경우가 아니면 사용하지 않는다.
Non-Escaping closure
non-escaping closure에서는 delayed deinitialization보단, serial 동작으로 인해 병목현상이 발생할 수 있다.
하지만, 순환참조는 여전히 발생하기 때문에 Memory leak을 해결하기 위해 weak 처리를 반드시 해주어야 한다.
그리하여 Escaping closure에서는 weak self를 필수적으로 사용하고, non-Escaping closure의 역시 대부분의 경우에서 weak self가 필요하다.
여러개의 글, 스스로의 이론 및 테스트를 통해서 얻은 결론이기 때문에 틀릴수 있습니다.
혹시 아시는 게 있다면 꼭! 알려주시면 감사하겠습니다!
Non-escaping Closure의 delayed deinitialization상황과, Escaping Closure에서 delayed deinitialization이 아닌 Memory leak상황을 아시는 분이 있다면 댓글을 달아주시면 감사하겠습니다.
참조
https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html
https://docs.swift.org/swift-book/LanguageGuide/Closures.html
https://medium.com/@almalehdev/you-dont-always-need-weak-self-a778bec505ef
https://stremsdoerfer.medium.com/understanding-memory-leaks-in-closures-48207214cba
'Swift' 카테고리의 다른 글
Swift Error handling(try, catch) - Swift Concurrency(2) (0) | 2023.02.09 |
---|---|
Swift Concurrency(1) - 기본개념 및 작동원리 (0) | 2023.02.08 |
[Swift] ARC와 클로저, 클로저 memory leak - ARC 기초편 4탄 (0) | 2023.01.11 |
[Swift] weak, unowned 차이 - ARC 기초편 3탄 (0) | 2023.01.10 |
[Swift] Strong Reference Cycle - ARC 기초 2탄 (0) | 2023.01.08 |