🐦 Swift

[Swift] ARC(Automatic Reference Counting)

dev_zoe 2023. 6. 29. 19:44
반응형

본 포스팅은 '스위프트 프로그래밍 (3판)' 도서 앨런 Swift 문법 마스터스쿨 강의를 참고하여

Swift 프로그래밍에 대해 정리하는 글입니다.

혹시 틀린 부분이 있거나 질문이 있으시다면 언제든지 댓글 달아주시면 정말 감사하겠습니다 :)


ARC(Automatic Reference Counting)

Swift에서 메모리를 자동으로 관리해주는 방식으로, 클래스/클로저에만 해당되는 메모리 관리 방식

ARC는 참조 횟수(=Reference Count, Retain Count)를 통해 더이상 필요하지 않은 클래스의 인스턴스를 메모리에서 자동으로 해제하는 방식으로 동작

 

컴파일 시 이미 값 타입의 메모리 해제 시점(reference count가 0이 되는 시점)이 정해져있어서 해제되어야 할 시점에 메모리가 해제됨

 

어떤 인스턴스를 가리킬 때마다 해당 인스턴스의 reference count(=retain count)는 증가하며,

nil을 할당하면 Reference Count가 감소되면서 이 값이 0이 되면 힙에서 메모리를 해제함 (더이상 참조할 여지가 남아있지 않으므로)

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 = nil                                // RC - 1   RC == 2
dog2 = nil                                // RC - 1   RC == 1
dog1 = nil	                              // RC - 1   RC == 0: 메모리 해제

 

강한 참조 사이클(강한 참조 순환 문제)

참조 타입끼리 서로가 서로를 가리키면서 발생하는 메모리 누수 문제

class Dog {
    var name: String
    var owner: Person?
    
    init(name: String) {
        self.name = name
    }
}


class Person {
    var name: String
    var pet: Dog?
    
    init(name: String) {
        self.name = name
    }
}


var bori: Dog? = Dog(name: "보리") // RC - 1
var gildong: Person? = Person(name: "홍길동") // RC - 1


bori?.owner = gildong // RC - 2
gildong?.pet = bori   // RC - 2


// 강한 참조 사이클(Strong Reference Cycle) = 강한 순환 참조 일어남

bori = nil  // RC - 1
gildong = nil // RC - 1

여기서 문제점은 bori와 gildong이는 이미 해제되었는데 bori?.owner, gildong?.pet은 해제되지 않아서
여전히 Reference count가 각각 1이지만, 더이상 인스턴스 접근이 불가하여 해제할 수 있는 방법이 없으므로

좀비처럼 메모리에 남아있음(=메모리 누수)

 

이처럼 인스턴스 끼리 서로 가리키는 것을 강한 참조 사이클 라고 하고, 이를 통해 메모리 누수라는 문제점이 발생한다.

해결하기 위한 방안이 약한 참조(weak reference), 비소유 참조(unowned reference) 이다.

 

약한 참조, 비소유 참조

- 두가지 방식 모두 가리키는 인스턴스의 Reference count를 증가시키지 않게 함

 

1. weak (약한 참조)

- 참조하고 있던 인스턴스가 사라지면 자동으로 nil이 할당됨 (따라서 weak var 옵셔널 만 가능 --> 자동으로 nil을 나중에 할당해야하니까)

2. unowned (비소유 참조)

- 참조하고 있던 인스턴스가 사라지면 자동으로 nil이 할당되지 않음 -> 따라서 참조하는 인스턴스가 참조하는 동안 메모리에서 절대 해제되지 않을 것이라는 확신이 들었을 때에만 사용 (보통 이렇게 조건 따져줘야하는것 때문에 weak를 많이 사용함)

 

클로저의 캡쳐리스트, 캡쳐현상

클로저 캡쳐현상

외부 변수의 값을 계속 가지고 있어야하므로 해당 변수의 참조 값을 계속 가지고 있는 현상 (외부 변수의 값이 바뀌면 클로저 안의 변수 값도 바뀜)

 

 ✅값 타입 캡쳐

- 값 타입을 가지고 있는 변수의 주소를 캡쳐함 (closure --> num 주소값)

var num = 1

let valueCaptureClosure = {
    print("\(num)")
}

num = 7
valueCaptureClosure()   //7

num = 1
valueCaptureClosure()  //1

// 값 타입의 경우, 값을 가지고 있는 변수의 주소를 캡쳐하므로 해당 변수의 값이 변경되면 closer도 바뀐 num의 값을 출력함

 

 ✅참조 타입 캡쳐

- 참조 타입을 가지고 있는 변수의 주소를 캡쳐함 (closure --> y변수 주소값 --> y 인스턴스)

class SomeClass {
    var num = 0
}

var y = SomeClass()

let closure = { y: 캡쳐현상
	print("\(y.num)")
}

y.num = 1

print("참조 초기값(숫자변경후):", y.num)      // 1

 

클로저 캡쳐리스트

참조 대상을 대괄호([])로 묶는 방식으로, 값 타입은 값을 복사하며 참조타입은 메모리 주소 직접 참조함

+ 캡쳐 리스트의 경우에만 weak/unowned 선언 가능 --> 강한 참조를 방지하기 위해 캡쳐 리스트 필수

 

✅값 타입 캡쳐리스트

- 캡쳐 현상과는 달리, 변수를 참조하는 것이 아닌 값 자체를 복사하는 것이기 때문에 변수의 값이 바뀌어도 closer안의 변수 값이 바뀌지 않음 (특정 작업에 의해 변수의 값이 바뀌는 것을 방지하고자 한다면 캡쳐리스트 활용)

var num = 1

let valueCaptureListClosure = { [num] in  // 값 타입 캡쳐리스트
    print("\(num)")
}


num = 7
valueCaptureListClosure()      // 1 : 값 자체를 복사했기 떄문에 7으로 바꿔도 값이 바뀌지 않음

 

✅참조 타입 캡쳐리스트

- 위에서 처럼 y 변수를 가리켜서 거쳐거쳐 인스턴스를 가리키는 것이 아닌, 바로 인스턴스의 주소를 캡쳐함

(closure --> y 인스턴스)

class SomeClass {
    var num = 0
}

var x = SomeClass()
var y = SomeClass()

let closure = { [x] in // x: 캡쳐리스트, y: 캡쳐현상
	print("\(x.num) \(y.num)")
}

x.num = 1
y.num = 1

print("참조 초기값(숫자변경후):", x.num, y.num)      // 1, 1

// y: y변수를 가리킴 x: x 인스턴스를 바로 가리킴
closure()     // 1, 1      -> 가리키는 방식 차이임(y는 y의 변수 주소를 가리키며 x는 인스턴스 메모리 주소를 직접적으로 참조

print("참조 초기값(클로저실행후):", x.num, y.num)     // 1, 1

- x가 참조타입이므로 (클래스) 이걸 캡쳐리스트로 [x] 이렇게 묶으면 --> 거쳐 거쳐가 아니라 바로 x의 인스턴스를 가리키게 된다.

- y는 참조타입이므로 이걸 캡쳐리스트로 묶지 않으면 캡쳐현상이 발생하는 것이므로 y의 변수 주소값을 가지고 있음 --> 거쳐 거쳐서 가리키게 되는 것

 

위 예시에서는 x에서 강한 참조는 일어나나 강한 참조 사이클은 발생하지 않는데,

클로저가 클래스의 프로퍼티일 경우에는 강한 참조 사이클이 일어남 (클래스 --> 클로저, 클로저의 self --> 클래스, 서로를 가리키는 현상)

 

 

💡 강한참조 사이클은 언제 발생하는가?

 

1) (위에 예시처럼) 인스턴스끼리 서로 참조하는 경우  --> weak/unowned를 사용하여 강한 참조 사이클 방지

2) 클로저가 인스턴스의 프로퍼티일 경우  --> 캡쳐리스트에서 weak/unowned를 사용하여 강한 참조 사이클방지 ([weak self], [unowned self])

 

class Person {
  let name: String
  let hobby: String?
  
  lazy var introduce: () -> String = { [weak self] in   // 클로저
    guard let 'self' = self else {
      return "원래의 참조 인스턴스가 없어졌습니다."
    }
    
    var introduction: String = "My name is \(self.name)"
    
    guard let hobby = self.hobby else {
      return introduction
    }
    
    introduction += " "
    introduction += "My hobby is \(hobby)"
    
    return introduction
  }
  
  init(name: String, hobby: String? = nil) {
    self.name = name
    self.hobby = hobby
  }
  
  deinit {
    print("\(name) is being deinitialized")
  }
}

var yagom: Person? = Person(name: "yagom", hobby: "eating")
var hana: Person? = Person(name: "hana", hobby: "playing guitar")

// hana의 introduce 프로퍼티에 yagom의 introduce 프로퍼티 클로저의 참조 할당
hana?.introduce = yagom?.introduce ?? {" "}
// 아직 yagom이 참조하는 인스턴스가 해제되지 않았기 때문에
// 클로저 내부에서 self(yagom 변수가 참조하는 인스턴스) 참조 가능
print(yagom?.introduce()) // My name is yagom. My hobby is eating.

// 여기서 클로저의 실행이 끝나면 자동으로 RC가 감소함 

yagom = nil // yagom is being deinitialized

print(hana?.introduce()) // 원래의 참조 인스턴스가 없어졌습니다.

 

이때 메모리 누수를 방지하기 위해 개발자가 신경써서 관리해줄 필요가 있음

--> 클로저가 클래스 내부의 인스턴스이거나 프로퍼티일때는 대부분 [weak self] 를 붙여준다고 보면 됨

--> 이때 관습적으로 guard let weakSelf = self else { return } 으로 바인딩 후 self 사용

DispatchQueue.global().async { [weak self] in
    guard let weakSelf = self else { return }   // 가드문 처리 ==> 객체없으면 일종료 (옵셔널)
    print("나의 이름은 \(weakSelf.name)입니다.(가드문)")
}

 

❓ weak self를 그럼 클로저에서 항상 붙이는게 좋은건지?

https://noah0316.github.io/Swift/2022-04-08-[weak-self]-%EB%AC%B4%EC%A1%B0%EA%B1%B4-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B2%8C-%EB%A7%9E%EB%8A%94%EA%B1%B8%EA%B9%8C/ 

 

[weak self] 무조건 사용하는게 맞는걸까? 🤔

클로져에서 self를 캡쳐할 때 를 사용하는 경우는 순환 참조를 방지하기 위해 약한 참조로 클로져 내부에서 해당 클래스의 인스턴스를 사용할때 입니다. 클로져에서 약한 참조를 이용해 특정 인

noah0316.github.io

좋은 블로그 글이 있어서 가져왔다.

 

1) Non-escaping 클로저는 스택 영역에서 실행되었다가 할일이 끝나면 사라지기 때문에, 강한 참조 사이클을 만들 상황이 없으므로 weak self를 붙여줄 필요가 없다.

2) 아래의 경우에는 강한 참조 사이클이 발생할 여지가 있으므로 weak self를 붙여주는 것이 안전함

 

- 클로저가 클래스의 프로퍼티에 저장되거나 다른 클로저에 저장/전달될 경우

class Dog {
    var name = "초코"
    
    var run: (() -> Void)?
    
    func saveClosure() {
        // 클로저를 인스턴스의 변수에 저장
        run = { [weak self] in
            print("\(self?.name)가 뛴다.")
        }
    }
}

- 클로저 안의 인스턴스가 클로저 / 인스턴스에 대한 강한 참조를 유지할 경우

 

3) 아래 상황에서는 꼭 붙이지 않아도 무관하다.

GCD/animation --> 어차피 일정 기간동안 실행하고 끝나기 때문에, 메모리에서 오래 남아있지 않음

 

+ delegate 패턴에서 delegate 변수 앞에 weak --> 서로를 가리키는 형태로 빠지기 쉬움

 

❓ 앱에 메모리 누수가 있는지 확인하는 방법

https://infinitt.tistory.com/403

 

(iOS) 메모리 이슈 디버깅, 메모리 그래프 Xcode instrument, 메모리 누수

메모리 누수(Memory leak)란 ? retain cycle로 인해 메모리에서 객체를 할당 해제할 수 없는 경우에 발생한다. Swift는 ARC를 통해 메모리 관리를 하는데, 두 객체 이상이 서로에 대해 강한 참조를 하는 경

infinitt.tistory.com

반응형