메모리 참조 방식을 살피기 전에 ARC를 짚고 넘어가보려고 합니다.
ARC ❓
정의 ▾
- 컴파일 시 코드를 분석해서 자동으로 retain, release 코드를 생성해주는 것
- retain
- retain count(= reference count) 증가를 통해 현재 Scope에서 객체가 유지되는 것을 보장합니다.
- release
- retain count(= reference count) 감소시킵니다.
- retain 후에 필요 없을 때 release 합니다.
- retain
- 참조된 횟수를 추적해 더 이상 참조되지 않는 인스턴스를 메모리에서 해제해 주는 것
- 참조 카운트가 0이 되면 자동으로 메모리 해제된다고 보면 됩니다.
- 0이 될 때 deinit을 호출해서 메모리 해제를 시킵니다.
- 자동으로 RC를 관리해주기 때문에 메모리 해제에 대한 개발자의 부담을 덜어준다고 합니다.
- Objective-C에서는 MRC, 수동으로 메모리 관리를 했다고 합니다.
- Swift Runtime이라는 library에 구현되어 있습니다.
왜 존재하는가
- ARC는 Heap 영역과 관련이 있는데 이 Heap은 class, closure 등의 참조형 자료들이 머무는 공간이며, 개발자가 동적으로 할당하는 메모리 공간이기 때문에 관리가 필요합니다.
- 관리는 Heap 영역에 참조형 자료가 얼마나 참조되고 있는지 카운팅하고 이에 따른 메모리를 할당 및 제거를 하면 됩니다.
이를 자동으로 해주는 것이 ARC 입니다.
Strong 🍎
정보 ▾
- 해당 인스턴스의 소유권을 가집니다.
- 자신이 참조하는 인스턴스의 retain count를 증가시킵니다.
- 값 지정 시점에 retain이 되고, 참조가 종료되는 시점에 release가 됩니다.
- 선언할 때 아무것도 적어주지 않으면 default로 strong이 됩니다.
- 강한(strong) 참조의 규칙을 모르고 사용하게 되면 메모리 누수(Memory Leak)가 발생할 수 있습니다.
- 위 문제를 발생시키는 원인이 "순환 참조" 입니다.
- 위 해결책이 바로 "약한참조(weak, unowned)" 입니다.
아래 순환 참조를 통해 Strong의 예시를 살펴보겠습니다.
순환 참조 → 메모리 누수 ❓
일단 아래 예시를 살펴보겠습니다.
class Man {
var name: String
var girlfriend: Woman?
init(name: String) {
self.name = name
}
deinit { print("Man deinit..") }
}
class Woman {
var name: String
var boyfriend: Man?
init(name: String) {
self.name = name
}
deinit { print("Woman deinit..") }
}
- Man에는 Woman 타입의 프로퍼티(girlfriend)가 있습니다.
- Woman에는 Man 타입의 프로퍼티(boyfriend)가 있습니다.
- 인스턴스 생성
var cheolsu: Man? = .init(name: "철수")
var yeonghee: Woman? = .init(name: "영희")
- 메모리는 다음과 같이 표현이 됩니다.
- 만약 철수와 영희가 사귀면 어떻게 될까?
chelsu?.girlfriend = yeonghee
yeonghee?.boyfriend = cheolsu
- 메모리는 아래와 같이 표현이 됩니다.
- Man 인스턴스와 Woman 인스턴스의 RC가 1씩 증가했습니다.
- boyfriend 프로퍼티는 Man 인스턴스의 주소값이 할당
- girlfriend 프로퍼티는 Woman 인스턴스의 주소값이 할당
- 두 프로퍼티는 default strong이기 때문에 RC 값이 1씩 증가해버립니다.
- 위 예시처럼 두 개의 객체가 서로가 서로를 참조하고 있는 형태 → 순환 참조
여기서 문제 ❗️
- 만약 철수와 영희가 헤어지게 되면 어떻게 될까?
cheolsu = nil
yeonghee = nil
- 인스턴스를 가리키던 변수에 nil을 대입했으니 RC가 1만큼 감소하고, 0일 경우 메모리에서 해제 되어야 합니다.
- 하지만 위 코드 결과로는 deinit이 호출되지 않습니다.
- 철수와 영희가 가리키던 인스턴스가 힙에서 사라지지 않고, 계속 메모리를 차지 않고 있다는 뜻!
- 아까 순환참조로 인해 서로의 RC가 1씩 증가했기 때문
- 철수와 영희가 가리키던 인스턴스가 힙에서 사라지지 않고, 계속 메모리를 차지 않고 있다는 뜻!
weak 🥝
정보 ▾
- 해당 인스턴스의 소유권을 가지지 않고, 주소값만 가지고 있는 포인터 개념입니다.
- 자신이 참조하는 인스턴스의 retain count를 증가시키지 않습니다.
- release도 발생하지 않게 됩니다.
- 자신이 참조는 하지만 weak 메모리를 해제시킬 수 있는 권한은 다른 클래스에 있습니다.
- 메모리가 해제될 경우 자동으로 레퍼런스가 nil로 초기화를 해줍니다.
- weak 속성을 사용하는 객체는 항상 optional 타입이어야 합니다.
- 해당 객체가 nil일 수 있기 때문입니다.
아래 예시를 통시 weak를 살펴보겠습니다.
순환참조 해결 예시 ▾
class Man {
var name: String
weak var girlfriend: Woman?
init(name: String) {
self.name = name
}
deinit { print("Man deinit..") }
}
class Woman {
var name: String
var boyfriend: Man?
init(name: String) {
self.name = name
}
deinit { print("Woman deinit..") }
}
- 순환 참조를 일으키는 프로퍼티 앞에 weak를 붙여줍니다.
- 인스턴스 생성
var cheolsu: Man? = .init(name: "철수")
var yeonghee: Woman? = .init(name: "영희")
cheolsu?.girlfriend = yeonghee
yeonghee?.boyfriend = cheolsu
- 위 처럼 인스턴스를 생성하면 아래와 같이 메모리를 표현할 수 있습니다.
- girlfriend의 프로퍼티가 weak라서 Woman 인스턴스의 주소값을 할당받지만, Woman 인스턴스의 RC 값을 건들지 않습니다.
- 메모리 해제
cheolsu = nil
yeonghee = nil
메모리 해제 순서 📝
- 철수, 영희 변수는 nil이 할당된 순간 각각의 인스턴스에 대한 RC 값이 1씩 감소합니다.
- RC가 0이 된 Woman 인스턴스가 메모리에서 해제됩니다.
- weak로 선언된 girlfriend가 참조하던 인스턴스가 메모리에서 해제 되었으니, girlfriend의 값이 nil로 할당됩니다.
- weak 특징 → 가리키던 인스턴스가 메모리에서 해제될 경우 nil이 할당됩니다.
- Man 인스턴스의 RC 값도 0이 되었으니 메모리에서 해제됩니다.
- deinit 함수가 작동 합니다.
Unowned 🍊
정보 ▾
- 강한 순환 참조를 해결할 수 있습니다. (weak와 동일)
- RC 값을 증가시키지 않습니다. (weak와 동일)
- 인스턴스를 참조하는 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신하는 것이 핵심입니다. (weak와 차이)
- 참조하던 인스턴스가 메모리에서 해제되도, nil을 할당받지 못하고 해제된 메모리 주소값을 계속 갖고 있습니다.
- 주로 객체의 라이프사이클이 명확하고 개발자에 의해 제어 가능이 명확한 경우 weak Optional 타입 대신 사용하여 더 간결한 코딩을 가능하게 합니다.
참고 사이트 ▾
https://babbab2.tistory.com/27
'iOS_Swift.zip' 카테고리의 다른 글
[Swift]SnapKit + Then CollectionView Cell Size (0) | 2022.09.10 |
---|---|
[Swift]Status Bar 색상 변겅 (0) | 2022.04.21 |
[iOS]Protocol (0) | 2022.02.20 |
[iOS]NotificationCenter (0) | 2022.02.18 |
[iOS]App Thinning (0) | 2022.02.16 |