티스토리 뷰
[Swift/iOS] 자동 참조 카운트 ARC(Automatic Reference Counting) from 공식문서
마들브라더 2023. 4. 27. 01:02! 공식문서를 참고한 글입니다
보통의 경우에는 Swift에서 메모리 관리를 위해서 어떠한 행동을 해줄 필요가 없다
ARC라는 것이 알아서 사용하지 않는 인스턴스를 메모리에서 해지해주기 때문이다
ARC, Automatic Reference Counting
말그대로 참조의 수를 자동으로 카운팅한다는 것이다
참조가 되는 수를 세고 있다가 더이상 참조되지 않으면 메모리에서 해제한다
ARC가 동작하는 방법 How ARC Works
클래스의 인스턴스를 만들 때마다, ARC는 인스턴스에 대한 정보를 저장하기 위해서 메모리 조각을 할당한다
이 메모리는 인스턴스의 타입에 대한 정보를 갖고, 인스턴스와 관련된 저장된 속성의 값도 갖고 있다
추가적으로, 인스턴스가 더이상 필요하지 않을 때, ARC는 해당 인스턴스에 의해 사용된 메모리를 해제한다
그래서 메모리는 다른 목적을 위해서 사용될 수 있게 된다
이것은 클래스의 인스턴스가 더이상 필요하지 않을 때 메모리 공간을 차지하지 않게하는 것을 보장하게 한다
인스턴스가 필요한 동안에는 인스턴스를 사라지지 않게 하기 위해서, ARC는 얼마나 많은 프로퍼티와 상수 그리고 변수가 각 클래스의 인스턴스를 참조하고 있는지 트래킹한다
ARC는 적어도 하나의 활성 참조가 그 인스턴스에 대해 존재한다면, 인스턴스를 해제 하지 않을 것이다
이것을 가능하게 하기 위해, 클래스의 인스턴스를 프로퍼티, 상수, 변수에 할당할 때마다, 그 프로퍼티, 상수, 변수는 강력한 참조를 인스턴스에 만든다
이 참조를 강력한 참조라고 한다. 왜? 이 참조는 그 인스턴스를 확실하게 잡고 있고, 그 참조가 남아 있는 한 그 인스턴스를 해제하는 것을 허용하지 않을 것이다
->
1. 인스턴스를 만들때 메모리 할당
2. 인스턴스가 필요하지 않으면 메모리에서 해제
3. 인스턴스가 필요한 상황에서 해제 되지 않게 하기 위해서, 인스턴스가 강력하게 참조하고 있는 수를 카운팅
4. 인스턴스 카운팅이 0이 되면 메모리에서 해제
ARC의 사용 ARC in Action
class 사람 {
let name: String
init(name: String) {
self.name = name
print("\(name) 초기화")
}
deinit {
print("\(name) 메모리에서 해제")
}
}
var reference1: 사람? = 사람(name: "철수") // 철수 초기화
var reference2 = reference1
var reference3 = reference1
reference1 = nil
reference2 = nil
reference3 = nil // 철수 메모리에서 해제
1. 사람(name: 철수) 라는 인스턴스를 만든 후 reference1 변수가 이를 참조하도록 하고
2. reference2와 reference3이 같은 인스턴스를 참조하도록 함
3. reference1과 reference2를 참조에서 해제 했지만 인스턴스는 메모리에서 해제 되지 않음
4. reference3, 인스턴스를 참조하고 있는 마지막 변수를 해지할 때, ARC는 철수 인스턴스를 참조하고 있는 상수, 변수, 프로퍼티가 없다는 것을 확인하고 메모리에서 해제 한다
클래스 인스턴스간 강한 참조 순환 Strong Reference Cycle Between Class Instances
보통의 경우에서는 위와 같은 방법으로 인스턴스들은 메모리에서 자동으로 해제된다
하지만 그렇지 않은 경우가 존재하고 메모리 누수가 발생할 수 있다
각 인스턴스가 서로를 강한 참조로 유지하는 경우, 강한 참조 순환에 빠지게 되어서 인스턴스 참조의 카운팅이 0이 되지 않을 수 있음
이것을 해결하기 위해서는 강한 참조 대신 Weak 참조 또는 Unowned 참조를 사용해야함
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("\(name) 메모리에서 해제")
}
}
class Apartment {
let name: String
var tenant: Person?
init(name: String) {
self.name = name
}
deinit {
print("\(name) 메모리에서 해제")
}
}
var 철수: Person? = Person(name: "철수")
var 자이: Apartment? = Apartment(name: "자이")
1. name이 철수인 Person인스턴스 생성, 철수라는 변수에 할당
2. name이 자이인 Apartment인스턴스 생성, 자이라는 변수에 할당
철수?.apartment = 자이
자이?.tenant = 철수
1. 철수 인스턴스의 aprtment 프로퍼티가 자이 Apartment 인스턴스를 강하게 참조하게 됨
2. 자이 인스턴스의 tenent 프로퍼티가 철수 Person 인스턴스를 강하게 참조하게 됨
3. 강한 참조 순환 완성...
현재 철수 인스턴스를 참조하고 있는 수를 카운팅하면 2가 된다
1) 변수 철수
2) 자이 인스턴스의 tenent 프로퍼티
자이 인스턴스 역시 참조 횟수는 2
1) 변수 자이
2) 철수 인스턴스의 apartment 프로퍼티
철수 = nil
자이 = nil
여기서 변수 철수와 변수 자이의 값을 nil로 설정해도 인스턴스의 메모리 해제가 되지 않음
현재 철수 인스턴스를 참조하고 있는 수 = 1
1) 변수 철수
2) 자이 인스턴스의 tenent 프로퍼티
자이 인스턴스 역시 참조 횟수는 1
1) 변수 자이
2) 철수 인스턴스의 apartment 프로퍼티
인스턴스 참조 횟수가 0이 아니기 때문임
클래스 인스턴스간 강한 참조 순환 문제의 해결 Resolving Strong Reference Cycles Between Class Instances
강한 참조 순환문제는 weak, Unowned 참조를 사용해서 해결할 수 있음
약한 참조는 참조하고 있는 인스턴스가 해제된다면 자동으로 nil로 설정됨
약한 참조는 nil로 변경 될 수 있기 때문에 옵셔널로 선언해야함
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("\(name) 메모리에서 해제")
}
}
class Apartment {
let name: String
weak var tenant: Person?
init(name: String) {
self.name = name
}
deinit {
print("\(name) 메모리에서 해제")
}
}
// (1)
var 철수: Person? = Person(name: "철수")
var 자이: Apartment? = Apartment(name: "자이")
// (2)
철수?.apartment = 자이
자이?.tenant = 철수
// (3)
철수 = nil // 철수 메모리에서 해제
print(자이?.tenant) // nil
// (4)
자이 = nil // 자이 메모리에서 해제
(1)
1. 이름이 철수인 사람 인스턴스 생성, 변수 철수가 철수 인스턴스를 참조
2. 이름이 자이인 아파트 인스턴스 생성, 변수 자이가 자이 인스턴스를 참조
(2)
1. 철수 인스턴스의 아파트 프로퍼티가 자이 인스턴스를 강하게 참조
2. 자이 인스턴스의 세입자 프로퍼티가 철수를 약하게 참조
철수 인스턴스를 참조하는 횟수 = 1
1. 변수 철수
2. 자이 인스턴스의 세입자 프로퍼티 (약하게 참조하고 있음)
자이 인스턴스를 참조하는 횟수 = 2
1. 변수 자이
2. 철수 인스턴스의 아파트 프로퍼티
(3)
1. 변수 철수에 nil 할당
2. 철수 인스턴스를 참조하고 있는 유일한 변수 철수가 nil이 되었기 때문에 참조 횟수는 0이 되고, 메모리에서 해제
3. 약한 참조는 참조하고 있는 대상이 해지 되면 변수에 nil을 할당하기 때문에 자이의 세입자 프로퍼티를 Print해보면 nil이 출력
자이 인스턴스를 참조하는 횟수 = 1
1. 변수 자이
2. 철수 인스턴스의 아파트 프로퍼티, 철수 인스턴스가 해제되면서 삭제
(4) 변수 자이에 nil을 할당하면서 자이 인스턴스 역시 메모리에서 해제
미소유 참조 Unowned References (+ Unowned Optional References)
미소유 참조는 weak와 같이 약하게 참조하나, weak와는 다르게 항상 그 값이 있다고 가정함
즉 참조하고 인스턴스가 해제되더라도 nil을 할당하지 않음
위 코드에서 weak를 unowned로 바꾸고 playground에서 실행하면
에러가 발생하는 것을 알 수 있음
Use an unowned reference only when you are sure that the reference always refers to an instance that hasn’t been deallocated.
If you try to access the value of an unowned reference after that instance has been deallocated, you’ll get a runtime error.
참조하는 인스턴스가 해제되지 않는다는 것이 확실할 때에만 unowned 참조를 사용해야함
클로저에서의 강한 참조 순환 Strong Reference Cycles Closure
class Test {
let name: String
lazy var closure = {
print(self.name, "클로저 실행")
}
init(name: String) {
self.name = name
print(name, "초기화")
}
deinit {
print(name, "해제")
}
}
var test: Test? = Test(name: "테스트함수") // 테스트함수 초기화
test?.closure() // 테스트함수 클로저 실행
test = nil
1. 클로저는 내부에서 사용되는 값을 캡쳐한다
2. 클로저가 self.name을 사용하기 위해서 self를 캡쳐하게 됨, self를 참조함
3. 클로저는 참조 타입이다(클래스처럼)
4. Test함수 인스턴스의 closure프로퍼티가 클로저를 강하게 참조함
5. 클로저와 인스턴스가 서로 강한 참조 순환을 하게 됨
6. test를 nil로 할당해도 해제 되지 않음
클로저에서 강한 참조 순환 문제의 해결 Resolving Strong Reference Cycles for Closure
클로저는 클래스의 인스턴스와 강한 참조 순환을 해결하기 위해서 캡쳐 리스트를 정의한다
캡쳐리스트 정의 Defining a Capture List
lazy var closure = {
[weak self] in
print(self.name, "클로저 실행")
}
캡처리스트를 정의할 때는 파라미터 앞에 대괄호를 넣고 각 캡쳐 대상에 대한 참조 타입을 명시
약한 참조가 필요한 일반적인 상황에서는 weak 참조만을 사용하고 안전하게 옵셔널을 언래핑해서 사용하도록 알려져있는데,
스위프트 공식 문서에는
If the captured reference will never become nil, it should always be captured as an unowned reference, rather than a weak reference.
캡쳐된 참조가 절대 nil이 되지 않는다면, weak 대신 항상 unowned를 사용하라고도 되어있다
(그런데 그러다가 nil에 접근하게 된다면,,?)
class Test {
let name: String
lazy var closure = {
[weak self] in
print(self?.name, "클로저 실행")
}
init(name: String) {
self.name = name
print(name, "초기화")
}
deinit {
print(name, "해제")
}
}
var test: Test? = Test(name: "테스트함수") // 테스트함수 초기화
test?.closure() // 테스트함수 클로저 실행
test = nil // 테스트함수 해제
메모리에서 해제 되는 것을 확인할 수 있다
+
클로저가 클래스의 메서드 안에서 호출 된 경우, 그리고 프로퍼티에 할당되지 않는 경우에는 self를 캡쳐하더라도 메모리 누수가 발생하지 않는 것 같다
func test() {
closure {
print(self.name)
}
}
이유는 메서드가 호출할 때 메서드 안의 변수가 초기화 되고, 메서드가 종료될 때 메서드 안의 변수 역시 메모리에서 해제 되는 것처럼
메서드 안의 클로저 역시 메서드가 호출 될 때, 메모리에 올라가고 메서드가 종료될 때 메모리에서 내려가기 때문에 강한 참조가 발생하지 않는 것 같다
(틀릴 수 있음)
자세한 내용은
https://medium.com/@almalehdev/you-dont-always-need-weak-self-a778bec505ef
'programming > Swift' 카테고리의 다른 글
[Swift/iOS] In-Out Parameters From 공식문서 (0) | 2023.04.27 |
---|---|
[Swift/iOS] 캡쳐리스트 Capture List , 탈출 클로저 escaping Closure, 자동클로저 AutoClosure from 공식문서 (0) | 2023.04.27 |
[Swift/iOS] 클로저 (Closure) from 공식문서 (0) | 2023.04.25 |
[Swift/iOS] UIAlertController를 리팩토링하는 2가지 방법, Helper 클래스와 프로토콜 접근 (0) | 2023.03.11 |
[Swift/iOS] Swift 튜토리얼: MVVM 디자인 패턴의 소개(일부) (0) | 2022.10.26 |
- Total
- Today
- Yesterday
- 토큰저장
- core data
- CoreData
- inout 파라미터 메모리 충돌
- identity Token
- 클로저
- object
- 캡쳐리스트
- Persistent Container
- ios
- ASAuthorizationAppleIDCredential
- authorizationCode
- 강한 참조 순환
- 자동클로저
- 디자인패턴
- unowned
- SWIFT
- autoclosure
- 회원가입
- context
- 클로저 강한 참조
- Core Data Stack
- 메모리 안정성
- Delegate 패턴
- escaping closrue
- weak
- 강한참조순환
- 클로저표현
- Entity
- 클로저 축약
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |