일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 백준
- 부르트포스
- 그리디
- 프로그래머스
- sigmoid
- 문제풀이
- 캡스톤정리
- Node.js
- 실버쥐
- Swift
- C++
- 백트래킹
- 탐색
- Greedy
- ios
- Algorithm
- Docker
- dp
- 플로이드와샬
- BFS
- DeepLearning
- NeuralNetwork
- dfs
- 알고리즘
- ReLU
- 그래프
- 풀이
- Stack
- Blockchain
- mysql
- Today
- Total
개발아 담하자
[iOS] Memory Leak (1) : 강한 순환 참조 본문
deinit
메모리 릭에 대해 알아보기에 앞서, deinit 함수에 대해 알아보겠습니다.
deinit 은 클래스의 인스턴스가 메모리에 해제될 때 즉시 호출되는 함수입니다. (클래스 타입에서만 작성 가능)
이 클래스 인스턴스가 메모리에서 해제되는 시점은 ARC 의 규칙에 따라 결정되는데, ARC는 클래스 인스턴스가 더 이상 필요없을 때 자동으로 메모리 해제합니다.
즉, deinit 이 호출되지 않는 경우 = ARC에서 메모리가 해제되지 않음 = 메모리 누수 (Memory Leak, Retain Cycle) 이 발생하고 있다는 것을 의미합니다.
ARC
그렇다면 ARC 는 정확히 무엇이고, 어떤 규칙으로 인스턴스를 메모리에서 해제시킬까요?
ARC 란 Automatic Referencing Counting의 약자입니다. 이름 그대로 각 인스턴스가 얼마나 참조되고 있는지를 추적하고 저장함으로써 메모리를 관리합니다.
만약 참조 counting 이 1 이상이면 인스턴스는 메모리에 계속 남아있고, 0이 되었을 때 메모리에서 해제됩니다. 참조 counting은 강한 참조가 될 때 +1 만큼 더해집니다.
Swift 는 이 ARC 를 활용해서 자동으로 앱의 메모리 사용을 추적하고 관리합니다.
그렇다면 언제 메모리 누수가 일어나는지(언제 deinit이 호출되지 않는지) 살펴봅시당
예제는 swift 공식 문서를 참조했습니다.
Automatic Reference Counting - The Swift Programming Language (Swift 5.5)
1. 서로 강한 참조를 할 때
두 인스턴스의 강한 참조로 순환이 이루어질 때 메모리 릭이 발생합니다.
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
Person, Apartment 클래스가 있다고 가정합시다.
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
위와 같이 할당하는 경우 john은 Person에, unit4A는 Apartment 에 강한참조를 하고 있습니다. (Person reference count = 1, Apartment reference Count = 1)
john = nil, unit4A = nil 을 할당하면 reference Count 가 각각 0이 되므로 deinit 함수가 무사히 호출됩니다. 즉, 메모리 릭이 일어나지 않습니다.
john!.apartment = unit4A
unit4A!.tenant = john
이 경우는 강한 순환 참조가 일어나는 경우입니다.
Person 인스턴스는 Apartment를 강한 참조 하고 있고, Apartment 인스턴스는 다시 Person을 강한 참조하고 있습니다. (Person reference count = 2, Apartment reference count = 2)
그래서 john = nil, unit4A = nil 을 해도 둘 다 reference count = 1 이기 때문에 이 두 개의 인스턴스는 메모리에서 해제되지 않습니다. → 이는 메모리 누수로 이어집니다.
이런 경우 약한 참조(weak) 와 미소유 참조(unowned) 를 사용해야 합니다.
약한 참조
약한 참조를 사용하면 Reference Count를 올리지 않습니다.
약한 참조는 변수 앞에 weak 를 선언함으로써 사용 가능합니다.
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person? // 🔥 약한 참조 weak 사용!! 🔥
deinit { print("Apartment \(unit) is being deinitialized") }
}
Person 은 Apartment를 여전히 강한 참조를 하고 있지만, Apartment는 Person에 대해 약한 참조를 하고 있습니다. (Person Reference Count: 1, Apartment Reference Count: 2)
john = nil
// Prints "John Appleseed is being deinitialized"
이 경우 john = nil 을 할당하면 Person 인스턴스의 Reference Count 값이 0이 되므로 무사히 메모리가 해제 됨을 확인할 수 있습니다. (Person Reference Count: 0, Apartment Reference Count: 1)
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
다시 unit4A = nil 까지 할당하면 Apartment 인스턴스의 Reference Count 값 까지 모두 0이 됩니다. 즉 두 인스턴스 모두 메모리에서 무사히 해제 됩니다.
미소유 참조 (Unowned Reference)
미소유 참조 역시 참조를 강하게 유지하지않음으로써 강한 참조 순환이 발생하는 것을 방지합니다. 다만 미소유참조는 다른 인스턴스의 수명이 같거나 수명이 더 길 때 사용됩니다. 또 약한 참조와 다르게 미소유 참조는 항상 값을 가질 것으로 예상되어야 합니다. (nil이 될 수 없기 때문 → Optional 로 선언되어서는 안 됨)
감이 잘 안 잡히므로 예제를 살펴봅시당
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer // 🔥 미소유 참조 unowned 사용!!
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
Customer 의 경우 Credition Card 를 소지할 수도 안 할 수도 있지만 (옵셔널로 설정),
카드의 경우 무조건 고객이 있어야만 합니다. 그리고 카드는 결코 고객의 수명보다 오래 지속될 수 없습니다.
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
Customer 인스턴스는 CreditCard 를 강한참조 하고 있고, CreditCard 인스턴스는 Customer 를 미소유 참조를 하고 있습니다. 그래서 Customer 가 메모리에서 해제될 때 무조건 CreditCard 역시 메모에서 해제됩니다.
여기까지 Memory Leak 이 일어나는 경우인 강한 순환 참조와 해결 방법(weak, unowned) 를 알아봤는데용
다음엔 메모리 릭이 일어나는 또 다른 경우인 클로저 캡처, delegate 에 대해 작성하도록 하겠습니다.
너무 길어서 끝!
'📱 iOS' 카테고리의 다른 글
[iOS] Tuist 를 활용한 프로젝트 모듈화 (2) (1) | 2022.08.25 |
---|---|
[iOS] Tuist 를 활용한 프로젝트 모듈화 (1) (1) | 2022.08.19 |
[iOS] MLKit 를 사용한 사진 속 텍스트 인식하기 (0) | 2021.11.05 |
[iOS] IDFA 적용하기 (0) | 2021.10.29 |
[iOS] Fridump3 를 사용한 메모리 덤프 (0) | 2021.10.01 |