-
[Swift] ARC🍏Swift 2023. 2. 26. 13:43
안녕하세요!
오늘은 Swift의 메모리 관리 모델인 ARC에 대해서 한번 알아보겠습니다!
메모리 관리 모델
- 메모리 구조
힙 영역에 할당되는 데이터는 관리를 해야지만 메모리에서 해제가 됨 할당이 해제되지 않으면 메모리 누수 현상이 발생
- 메모리 관리 모델
스위프트에서는 RC(참조 숫자)를 세는 것을 통해 메모리 관리 / 컴파일시에 메모리 해제시점을 결정함
//오래된 프로그래밍 언어인 Objective-C, C와 같은 언어들은 MRC라는 수동으로 세야하는 관리 모델을 사용했었음 //개발자들은 실제 로직 구현 포함, 메모리 관리에 대한 부담이 있었음 //따라서 현대적 언어들은 대부분 자동 메모리 관리 모델을 사용 //스위프트의 경우 컴파일러가 자동으로 retain()할당(+1), release()해제(-1) 코드를 삽입한다고 보면됨 //컴파일러가 메모리 관리코드를 자동으로 추가해 줌으로써 프로그램의 메모리 관리에 대한 안정성 증가 //쉽게 말하면 인스턴스를 가르키고 있는 RC가 1이상이면 메모리에 유지되고, 0이되면 메모리에서 제거됨 // MRC(수동 RC관리)의 예시 //원래는 주석처리한 것들을 실제로 입력해줬어야했음 class Dog { var name: String var weight: Double init(name: String, weight: Double) { self.name = name self.weight = weight } deinit { print("\\(name) 메모리 해제") } } var choco: Dog? = Dog(name: "초코", weight: 15.0) //retain(choco) RC: 1 var bori: Dog? = Dog(name: "보리", weight: 10.0) //retain(bori) RC: 1 //RC가 0이 되었으니 아래 인스턴스들은 메모리에서 제거됨 choco = nil // RC: 0 //release(choco) bori = nil // RC: 0 //release(bori) //ARC(자동 RC) var dog1: Dog? var dog2: Dog? var dog3: Dog? dog1 = Dog(name: "댕댕이", weight: 7.0) // RC + 1 RC == 1 dog2 = dog1 // RC + 1 RC == 2 dog3 = dog1 // RC + 1 RC == 3 dog3?.name = "깜둥이" dog2?.name dog1?.name dog3 = nil // RC - 1 RC == 2 dog2 = nil // RC - 1 RC == 1 dog1 = nil // RC - 1 RC == 0 // 메모리 해제강한 참조 사이클과 메모리 누수
- 개념
강한 참조 사이클이란 객체가 서로를 참조하는 것으로 인해 변수의 참조에 nil을 할당해도 메모리 해제가 되지 않는 메모리 누수의 상황이 발생하는 것
class Dog { var name: String var owner: Person? init(name: String) { self.name = name } deinit { print("\\(name) 메모리 해제") } } class Person { var name: String var pet: Dog? init(name: String) { self.name = name } deinit { print("\\(name) 메모리 해제") } } var bori: Dog? = Dog(name: "보리") var gildong: Person? = Person(name: "홍길동") //여기서 강한 참조 사이클이 일어남 bori?.owner = gildong gildong?.pet = bori //nil을 할당해도 메모리가 해제되지 않는 메모리 누수가 발생 bori = nil gildong = nil- 해결 방안(약한 참조(weak 키워드))
가르키는 인스턴스의 RC의 숫자를 올라가지 않게 해주는 weak 키워드를 사용하면 가능함
class Dog { var name: String weak var owner: Person? // weak 키워드 -> 약한 참조 init(name: String) { self.name = name } deinit { print("\\(name) 메모리 해제") } } class Person { var name: String weak var pet: Dog? // weak 키워드 -> 약한 참조 init(name: String) { self.name = name } deinit { print("\\(name) 메모리 해제") } } var bori: Dog? = Dog(name: "보리") var gildong: Person? = Person(name: "홍길동") // 강한 참조 사이클이 일어나지 않음 bori?.owner = gildong gildong?.pet = bori // 메모리 해제가 잘됨(사실 이 경우 한쪽만 weak으로 선언해도 상관없음) bori = nil gildong = nil💡 weak키워드는 참조하고 있던 인스턴스가 사라지면 nil로 초기화되기 때문에 사용시에 weak var로만 선언이 가능하고 옵셔널 타입만 가능함
- 해결방안(비소유 참조(unowned 키워드))
가르키는 인스턴스의 RC의 숫자를 올라가지 않게 해주는 unowned 키워드를 사용하면 가능함
class Dog1 { var name: String unowned var owner: Person1? // Swift 5.3 이전버전에서는 비소유참조의 경우, 옵셔널 타입 선언이 안되었음 init(name: String) { self.name = name } deinit { print("\\(name) 메모리 해제") } } class Person1 { var name: String unowned var pet: Dog1? init(name: String) { self.name = name } deinit { print("\\(name) 메모리 해제") } } var bori1: Dog1? = Dog1(name: "보리1") var gildong1: Person1? = Person1(name: "홍길동1") // 강한 참조 사이클이 일어나지 않음 bori1?.owner = gildong1 gildong1?.pet = bori1 // 메모리 해제가 잘됨(사실 이 경우 한쪽만 unowned로 선언해도 상관없음) bori1 = nil gildong1 = nil💡 unowned 키워드의 경우 참조하고 있던 인스턴스가 사라지면 nil로 초기화 되지 않음
클로저와 메모리 관리
캡처리스트
- 형태
클로저는 힙의 영역에 존재해야하고 클로저 내부에서 외부에 존재하는 변수를 계속 사용해야 하기때문에 캡처 현상이 발생함
//파라미터가 없는 경우 { [캡처리스트] in print("프린트") } //파라미터가 있는 경우 { [캡처리스트](파라미터) -> 리턴형 in print("프린트") }- 값 타입 캡처 / 캡처리스트
캡처: 클로저 외부에 존재하는 값 타입의 참조(변수 주소)를 캡처함(외부요인에 의해 해당 값이 변했을때도 계속 참조)
캡처리스트: 클로저 외부에 존재하는 값 타입의 값을 복사해서 사용(외부요인에 의해 해당 값의 변경을 방지할때 사용)//값 타입 캡처 //클로저는 자신이 사용할 외부의 변수를 캡처함 var num = 1 let valueCaptureClosure = { print("밸류값 출력(캡처): \\(num)") } num = 7 valueCaptureClosure() //7 출력 //밸류타입의 참조(메모리주소)를 캡처함 //값 자체를 복사해서 가지고 있는 것이 아니고 //num의 주소를 캡처해서 계속 사용하기 때문에 값이 변경될때마다 같이 변경됨 num = 1 valueCaptureClosure() //1 출력 //값 타입 캡처리스트 //선언시에 num = 1 let valueCaptureListClosure = { [num] in // 캡처리스트에서 밸류(value) 타입 캡처 print("밸류값 출력(캡처리스트): \\(num)") } num = 7 valueCaptureListClosure() //1 출력 //1을 출력하는 이유는 캡처리스트를 통해 클로저 선언할 때에 num의 값이 1이 였기 때문 //값 타입 캡처리스트에서는 캡처와 다르게 선언할때의 값을 저장해두고 계속 사용함 //값 변경 방지에 좋음 // 밸류타입의 값을 캡처함 // (즉, 값 자체를 복사해서 가지고 계속 사용) // 즉, 값 타입에서는 참조하는 값의 변경을 방지(외부적인 요인에 의한)하고 사용하고자 할때, 사용- 참조 타입 캡처 / 캡처리스트
캡처: 클로저 외부에 존재하는 참조타입의 참조(변수 주소)를 캡처함(외부요인에 의해 해당 값이 변했을때도 계속 참조)
캡처리스트: 클로저 외부에 존재하는 참조타입의 주소값을 복사해서 사용(외부요인에 의해 해당 인스턴스의 해제를 방지할때 사용 → 가르키는 인스턴스의 RC를 올라가게 해서 메모리에서 해제될 가능성을 방지해줌)//참조 타입 캡처 / 캡처리스트 //참조 타입 캡처는 값 타입과 같음 class SomeClass { var num = 0 } var x = SomeClass() var y = SomeClass() print("참조 초기값(시작값):", x.num, y.num) // 0 0 //x - (참조타입) 주소값 캡처, x를 직접참조로 가르킴 //y - 변수를 캡처해서, y변수를 가르킴(간접적으로 y도 동일) let refTypeCapture = { [x] in print("참조 출력값(캡처리스트):", x.num, y.num) } x.num = 1 y.num = 1 //x = SomeClass() //y = SomeClass() print("참조 초기값(숫자변경후):", x.num, y.num) // 1, 1 //참조 타입 캡처와 캡처리스트 모두 주소를 복사하는 것이므로 변경된 값 출력함 refTypeCapture() // 1, 1 print("참조 초기값(클로저실행후):", x.num, y.num) // 1, 1- 참조 타입 캡처리스트에서의 weak / unowned 키워드
강한 참조 사이클 문제를 해결할 수 있는 저 키워드들은 참조 타입 캡처리스트에서만 사용 가능함
var z = SomeClass() //가르키는 인스턴스의 RC를 올라가지 않게 함 //weak키워드 변수의 값은 옵셔널 타입을 가짐 let refTypeCapture1 = { [weak z] in print("참조 출력값(캡처리스트):", z?.num) } refTypeCapture1() // Optional(0) //가르키는 인스턴스의 RC를 올라가지 않게 함 let refTypeCapture2 = { [unowned z] in print("참조 출력값(캡처리스트):", z.num) } refTypeCapture2() // 0 //캡처리스트에서 바인딩도 가능 var s = SomeClass() let captureBinding = { [r = s] in // 내부에서 변수명 바꿔서 사용가능 (외부변수와 헷갈리는 것을 방지) print("바인딩의 경우:", r.num) } let captureWeakBinding = { [weak r = s] in print("Weak 바인딩 경우:", r?.num) //weak 키워드는 옵셔널타입이므로 접근연산자에서 ?표시 해야함 } captureBinding() captureWeakBinding()클로저의 사용
- 객체 내에서의 사용
클로저 내에서 객체의 속성 및 메서드에 접근 시에는 self키워드를 반드시 사용해야함(강한 참조를 하고 있다는 것을 표시하기위한 목적) → 여기서는 Dog의 RC를 올리는 역할 +1
1) self.name
2) [self]
구조체의 경우, self를 생략하는 것도 가능class Dog { var name = "초코" func doSomething() { // 비동기적으로 실행하는 클로저 // 해당 클로저는 오래동안 저장할 필요가 있음 -> 새로운 스택을 만들어서 실행하기 때문 DispatchQueue.global().async { //새로운 스택을 만들어서 해당 클로저를 실행함 print("나의 이름은 \\(self.name)입니다.") } } } var choco = Dog() choco.doSomething() // 클로저가 choco인스턴스에 대해 강한 참조는 하지만 (RC + 1) // 다른 스택에서 클로저의 실행이 완료되고 나면 힙에 있는 클로저도 제거되기 때문에 // 실제 강한 참조 사이클을 일으키진 않음- 캡처리스트의 실제 사용 예시
// 강한 참조의 경우 // 강한 참조 사이클은 일어나지 않지만(인스턴스가 서로를 가르키는) // 클로저가 name 변수를 강한 참조를 하기 때문에 뷰컨트롤러를 가르키는 변수가 해제되었음에도 // 강한 참조로 아직 RC가 1이 남아있기 때문에 3초 뒤에 출력하고 나서 // 뷰컨의 메모리가 해제됨 class ViewController: UIViewController { var name: String = "뷰컨" func doSomething() { DispatchQueue.global().async { sleep(3) print("글로벌큐에서 출력하기: \\(self.name)") } } deinit { print("\\(name) 메모리 해제") } } func localScopeFunction() { let vc = ViewController() vc.doSomething() } // 이 함수는 이미 종료 ==> vc변수 없음 //localScopeFunction() // (3초후) // 글로벌큐에서 출력하기: 뷰컨 // 뷰컨 메모리 해제 // 약한 참조의 경우 // 뷰컨트롤러를 오래동안 잡아두지 않음 // 뷰컨트롤러가 사라지면 출력하는 일을 계속하지 않도록 할 수 있음 // (if let 바인딩 또는 guard let 바인딩까지 더해서 return 가능하도록) class ViewController1: UIViewController { var name: String = "뷰컨" func doSomething() { // 강한 참조 사이클이 일어나지 않지만, 굳이 뷰컨트롤러를 길게 잡아둘 필요가 없다면 // weak self로 선언 DispatchQueue.global().async { [weak self] in //guard let weakSelf = self else { return } sleep(3) print("글로벌큐에서 출력하기: \\(self?.name)") } } deinit { print("\\(name) 메모리 해제") } } func localScopeFunction1() { let vc = ViewController1() vc.doSomething() } localScopeFunction1() // 뷰컨 메모리 해제 // (3초후) // 글로벌큐에서 출력하기: nil혹시 잘못된 내용이 있다면 피드백 주시면 정말 감사합니다! 😊
'🍏Swift' 카테고리의 다른 글
[Swift] Classes And Structures (0) 2023.03.12 [Swift] self vs Self (0) 2023.03.05 [Swift] Optional (0) 2023.02.19 [Swift] Enumeration (0) 2023.02.09 [Swift] Access Control (0) 2023.02.06