직역하면 자동 참조 카운팅입니다. iOS의 메모리 관리 기법입니다. 간단하게 이야기하면 어떤 객체가 메모리에서 해제되기 위해서는 그 객체를 가리키는(= 참조하는) 객체의 갯수가 0이 되어야 합니다. Heap 영역에 저장되는 객체가 차지하는 메모리를 관리하기 위해서 사용합니다.
강한 참조와 약한 참조는 RC를 관리하는 기법입니다. 강한 참조는 RC를 증가시키는 반면 약한 참조는 RC를 증가시키지 않습니다. 말로만 설명하니까 어렵네요. 아래 코드를 함께 보면서 설명드려보겠습니다.
강한 참조는 RC를 증가시키는 참조 방법입니다. 모든 참조의 default 값은 강한 참조입니다.
아래 코드를 playground에서 실행해봅시다.
class SomeClass {
var name = "some class"
func printClassName() {
DispatchQueue.global().async {
sleep(3)
print("클래스 이름: \(self.name)")
}
}
deinit {
print("\(name) 메모리에서 해제")
}
}
func someFunction() {
let someClass = SomeClass()
someClass.printClassName()
print("some function 실행 끝")
}
someFunction()
콘솔에 출력되는 결과는 아래와 같습니다.
"
some function 실행 끝
클래스 이름: some class
some class 메모리에서 해제
"
분명히 someFunction이 리턴되면 내부의 someClass는 메모리에서 해제되어야 하는데 return 되어도 해제되지 않고 메모리에 남아있다가 3초 뒤에 클래스 이름을 출력된 이후에야 해제되는 것을 볼 수 있습니다. 메모리에서 어떤 일이 일어나고 있었을까요?
약한 참조는 RC를 증가시키지 않는 참조 방법입니다. 변수 앞에 weak라는 키워드를 붙여서 선언합니다. 클로저에서는 아래 코드처럼 캡쳐하는 대상 앞에 weak를 붙여서 사용합니다.
class SomeClass {
var name = "some class"
func printClassName() {
DispatchQueue.global().async { [weak self] in
sleep(3)
print("클래스 이름: \(self?.name)")
}
}
deinit {
print("\(name) 메모리에서 해제")
}
}
func someFunction() {
let someClass = SomeClass()
someClass.printClassName()
print("some function 실행 끝")
}
someFunction()
콘솔에 출력되는 결과는 아래와 같습니다.
"
some function 실행 끝
some class 메모리에서 해제
클래스 이름: nil
"
이번에는 출력되는 순서도 다르고 클래스의 이름도 nil이 출력되고 있습니다. 메모리에서 일어나는 일을 살펴봅시다.
참조 사이클은 서로 강하게 참조하고 있어서 서로 메모리에서 해제되지 않는 현상을 가르킵니다. 해제되어야 할 메모리가 계속 남아있어 메모리 누수 (Memory Leak)의 주범입니다.
참조 사이클을 해결하기 위해서는 한쪽 참조를 약한 참조로 바꾸어서 해결할 수 있습니다.
개발을 하다보면 종종 의도치않게 참조 사이클을 만들어 버리곤 합니다. 저도 자주 실수를 했었습니다. 제가 주로 실수한 부분을 여러분께 공유드려보도록 하겠습니다.
class ListCell: UITableViewCell {
weak var delegate: ListCellDelegate?
}
UITableView나 UICollectionView를 사용하다보면 시스템에서 구현된 delegate 패턴이 아니라 커스텀 delegate를 지정해서 사용해야할 때가 있습니다. 이 때 보통 UIViewController를 delegate로 지정합니다.
delegate를 선언할 때는 반드시 weak로 선언해주시기 바랍니다. weak로 선언하지 않을 경우 VC가 해제되어야 할 때 ListCell이 VC를 강하게 참조해서 VC가 해제되지 않습니다. 즉 VC는 Cell을 강하게 참조하고 Cell은 VC를 강하게 참조해서 retain cycle이 발생하는 것입니다.
class VC: UIViewController {
func fetchData() {
let service = FirebaseService()
service.fetchNewData { [weak self] data in
self.updateUI(data)
}
}
}
간단한 예시로 설명드리겠습니다. 위 코드는 네트워크 (Firebase)에서 데이터를 받아와서 UI 업데이트를 하는 간단한 코드입니다. 이 때 completionHandler로 전달하는 클로저에서는 약한 캡쳐리스트를 사용하는 것이 좋습니다.
왜냐하면 네트워크에서 데이터를 받아오는 함수가 바로 return 된다는 보장이 없기 때문입니다. 위에서 예로 들은 Firebase는 데이터를 보내주고 바로 return 하는 API도 있지만 많은 API가 옵저버 형식으로 구현되어 있습니다. 쉽게 설명하면 데이터를 보낸 이후에도 데이터가 추가되면 추가된 데이터를 보내주기 위해서 return 하지 않고 메모리에 남아 있습니다.
이 경우에 강한 참조를 쓰게되면 VC를 메모리에서 해제하려고 해도 Firebase의 API가 VC를 참조하고 있기 때문에 메모리에서 해제되지 않게 됩니다. 즉 VC가 API를 강하게 참조하고 API가 VC를 강하게 참조해서 retain cycle이 만들어진 것이죠.