Swift의 정말 정말 중요한 메모리 관리 기법입니다. Automatic Reference Counting 자동으로 레퍼런스 카운팅을 관리하며, 이를 명확히 이해해서 ARC를 고려한 메모리 설계가 iOS 프로젝트를 진행하며 필수입니다. (ARC를 고려하여, 메모리 누수 방지를 위해 노력한점 또한 추후 포스팅 예정 빠밤.)
공식문서 ARC 핵심 요약
- Swift는 ARC(Automatic Reference Counting)를 사용하여 앱의 메모리 사용량을 추적하고 관리
- 클래스의 새 인스턴스를 생성할 때마다 ARC는 해당 인스턴스에 대한 정보를 저장하기 위해 메모리 청크를 할당합니다. 이 메모리는 해당 인스턴스와 관련된 저장된 속성 값과 함께 인스턴스 유형에 대한 정보를 보유
- Class 마다 참조에 관한 Couting 공간이 있다고 생각하고 넘어갑니다. 시리즈 2에서 이 부분만 추가 설명 !
- 인스턴스가 더 이상 필요하지 않은 경우 ARC는 해당 인스턴스에서 사용하는 메모리를 해제하여 메모리를 대신 다른 용도로 사용하게함
ARC 키워드 설명
- Strong - 강한 참조시 레퍼런스 카운트 1 증가, 클래스가 서로가 서로를 강하게 참조하면 Retatin Cycle 발생하므로 주의하여 선언. 기본선언이 Strong
- Weak - 약한 참조 레퍼런스 카운팅 증가하지 않음 (Swift 4이후로 Sidetable 개념이 생겨남) 참조한 인스턴스가 메모리 해제시 nil 할당되어 메모리 해제
- Unowned - 레퍼런스 카운팅을 증가하지 않지만, 카운터가 0인 소유되지 않은 참조에 액세스하면 런타임 오류가 발생합니다. 다른 인스턴스의 수명이 같거나 더 길 때 사용합니다. (절대 해당 참조 인스턴스가 먼저 해제되면 안됩니다. + Swift 5 부터 옵셔널 가능
순환참조?
class Test1 {
var name: String?
var test2Ins: Test2?
init(name: String?) {
self.name = name
}
deinit{
print("test1 deinit")
}
}
class Test2 {
var address: String?
var test1Ins: Test1?
init(_ ads: String?) {
self.address = ads
}
deinit {
print("test2 deinit")
}
}
var a: Test1? = Test1(name: "asdf") // Test1 레퍼런스 카운트 +1
var b: Test2? = Test2("gy") // Test2 레퍼런스 카운트 +1
a!.test2Ins = b // Test2 레퍼런스 카운트 +1
b!.test1Ins = a // Test1 레퍼런스 카운트 +1
a = nil
b = nil
//test1과 test2가 서로가 서로를 강하게 참조하고 있어서 메모리 해제 X
Test1 과 Test2는 각각 2번의 강한 참조가 이루어졌고, 메모리가 해제되기 위해선 Reference Counting이 0 이 되어야 합니다. 하지만 Test1의 멤머 인스턴스 변수로 서로를 참조하기 때문에 각각 레퍼런스 카운팅이 1씩 남아있게 되어 메모리 해제가 되지 않습니다.
이러한 RetainCycle은 없어져야할 ViewController 및 인스턴스들이 사라지지 않아서 심각한 메모리 문제 및 성능저하를 일으킵니다.
a!.test2Ins = nil
a = nil
b = nil
//결과출력:
//test2 deinit
//test1 deinit
위의 코드처럼 a 인스턴스의 멤버 인스턴스 변수의 메모리를 먼저 해제해주면 메모리가 해제됩니다.
하지만 이것은 근본적인 해결방법이 아닙니다. 이 때 바로 weak 키워드를 이용해 해결해야합니다 !
또한, "어짜피 내가 짠 코드니까 언제 nil이 되어야하는지 명시해줄 수 있어 ~ 만능 ARC에 원할 때 nil로 메모리를 해제 시킬거야 ~"
매우 안좋은 생각입니다.
ARC는 컴파일 시점에 언제 메모리가 해제되는지 결정되고, 런타임에 실행됩니다. 따라서 런타임에 추가 리소스가 발생하지 않고, 컴파일 시점에 메모리 해제 시점을 알기에 성능적인 이점이 있습니다.
위의 코드 구조를 따른다면, 불필요하게 런타임에서 추가 리소스가 발생하여 언제 해제 될지 모르는 메모리 해제 계산을 진행해야합니다.
따라서, 정확하게 레퍼런스 카운팅을 관리할 줄 알아야 합니다.
class Test1 {
var name: String?
weak var test2Ins: Test2?
init(name: String?) {
self.name = name
}
deinit{
print("test1 deinit")
}
}
class Test2 {
var address: String?
var test1Ins: Test1?
init(_ ads: String?) {
self.address = ads
}
deinit {
print("test2 deinit")
}
}
var a: Test1? = Test1(name: "asdf")
var b: Test2? = Test2("gy")
a?.test2Ins = b
b?.test1Ins = a
// 여기까지, Test1 레퍼런스 카운팅 2 , Test2 레퍼런스 카운팅 1 (Test1 클래스 프로퍼티에서 약한참조이므로)
a = nil
b = nil
//출력결과:
//test2 deinit
//test1 deinit
Test1에서 weak 선언으로 Test2를 참조하게 되면, Test2 클래스의 레퍼런스 카운팅이 증가하지 않습니다.
순서대로 설명해보겠습니다.
1. a = nil , b = nil이 실행되면 Test1 과 Test2의 레퍼런스 카운팅은 1씩 감소하게 됩니다.
(a 와 b를 ViewController에 있는 지역변수라고 생각하겠습니다. Stack 에서 heap을 가리키는 참조가 해제)
2. Test2 클래스가 먼저 메모리가 해제됩니다. test2 deinit 출력
3. Test1의 레퍼런스 카운팅 1, Test1을 참조하고 있는 Test2가 메모리 해제되었기 때문에 Test1을 참조하는 레퍼런스 카운팅 1 또한 감소
(Test2 -> Test1 참조 해제)
4. Test1 클래스 메모리 해제 =>test1 deinit 출력
이렇게 RetainCycle의 핵심 문제점과 해결법을 살펴보았습니다. 다음 시리즈에선, 어떻게 이러한 메모리 카운팅 과정이 이루어지는지 설명하도록 하겠습니다 ~ !
'Swift information' 카테고리의 다른 글
우아한 Model Data 처리 (1) (0) | 2022.06.11 |
---|---|
클로저 - Capturing Values와 ARC (0) | 2021.12.03 |
ARC 시리즈 2 - SideTable (0) | 2021.11.04 |
Struct와 Class에서의 let 과 var의 차이에 대하여 (0) | 2021.11.02 |