ARC란
- 자동으로 메모리 관리를 해주는 친구
- 객체에 대한 참조카운트를 관리하고 0이 되면 자동으로 메모리해제
- 메모리 누수가 발생할 수 있다.
- 옛날 Objective-C에선 MRC => 수동으로 관리
- Compile time할때 실행된다.
과연 Compile time에 실행되는 ARC가 동적으로 할당되는 메모리의 refrence count를 하고 메모리관리를 어떻게할까?
사전에 알아야 할 지식은 MRC이다.
MRC
엣날 objc에선 retain. release, sutorelease등을 통해 수동으로 메모리 관리를 했다.
class Person{
let name: String
init(name: String){
self.name = name
}
}
let clamp = Person(name: "clamp")
retain(clamp) //참조카운트 1증가
clamp = nil
release(clamp) //참조카운트 1감소 => 0이므로 메모리 해제
정확한 문법은 아니지만 retain과 release를 적절히 직접 사용하여 개발자가 직접 참조카운트를 증가. 감소시켜야했다.
ARC의 메모리 관리
MRC방식을 어떻게 자동으로 ARC를 할까?
compile time에 자동으로 컴파일러가 retain, release를 자동으로 삽입해준다.
ARC는 자동으로 retain, release를 삽입해서 referenceCount를 관리하고, 0이 될 때 deinit을 호출해서 메모리 해제를 시킨다.
그렇다면 referenceCount가 괸리되는 곳은어디인가.
메모리영역은 크게 Code, Heap, Stack, Data영역에 저장이되며 참조되는 객체들은 Heap영역에 저장된다. ==> class, closure
또한 이 공간은 개발자가 동적으로 객체들을 할당하는 공간이기 때문에 메모리 관리가 필요하다.
참조타입의 캡처현상
class SomeClass{
var num = 0
}
var x = SomeClass()
var y = SomeClass()
print("참조 초기값(시작값):", x.num, y.num) // 0, 0
let refTypeCapture = { [x] in
print("참조 출력값(캡처리스트):", x.num, y.num)
}
// x를 직접적으로 캡처하면 x의 주소를 클로저 내부에서 직접 가리킨다(힙영역의 클로저 내부에 x클래스 주소를 갖고있음.
// y는 힙영역의 클로저 내부에서 외부의 y변수의 주소를 가리키고, 외부의 y변수가 다시 힙 영역의 y클래스를 가리킨다.
x.num = 1
y.num = 1
print("참조 출력값(값 변경 후):", x.num, y.num) // 1, 1
refTypeCapture() // 1, 1
메모리 누수(Memory Leak)와 강한참조
컴파일러가 정상적으로 메모리해제를 시키지 못하는 경우가 생긴다.
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
}
}
일단 코드상 Dog은 Person을 가질 수 있다.
Person은 Dog를 가질 수 있다.
아래같은 상황이 생길 때 메모리누수가 발생한다.
let ggomul: Dog? = Dog(name: "ggomul")
let clamp: Person? = Person(name: "clamp")
ggomul.owner = clamp
clamp.pet = ggomul
ggomul = nil
clamp = nil
이유
let ggomul: Dog? = Dog(name: "ggomul")
//retain(ggomul) ==> ggomul refCnt = 1
let clamp: Person? = Person(name: "clamp")
//retain(clamp) ==> clamp refCnt = 1
ggomul.owner = clamp
//retain(ggomul) ==> ggomul refCnt = 2
clamp.pet = ggomul
//retain(clamp) ==> clamp refCnt = 2
ggomul = nil
//release(ggomul) ==> ggomul refCnt = 1
clamp = nil
//release(clamp) ==> clamp refCnt = 1
위의 경우 메모리 해제가 일어나지 않는다.
왜냐하면 ggomul과 clamp의 최종적인 참조카운트는 1이 되기 때문이다.
각생성시 ref는 1 증가하고
서로의 멤버로 추가되기 때문에 1이 증가한다.
이런경우 메모리 해제가 일어나지 않고 메모리 누수가 발생한다.
또한 위와같은 상황이 강한참조사이클이다.
정상적인 메모리 해제는 아래와같이 설정해야한다.
ggomul.owner = nil
clamp.pet = nil
ggomul = nil
clamp = nil
Memory Leak의 해결방안
RC(RefrenceCount)을 고려하여 참조 해제 순서를 주의해서 코드를 작성해야한다
하지만 이런 방법은 신경쓸 것이 너무 많아지고 실수의 가능성이 높다.
그래서 사용 가능한 방법이!
- Weak Reference(약한 참조)
- Unowned Reference(비소유 참조)
Weak와 Unowned의 공통점은
"가르키는 인스턴스의 RC숫자를 올라가지 않게한다"
인스턴스 사이의 강한참조를 제거한다.
weak/unwoned로 선언한 변수를 통해 인스턴스에 접근은 가능하지만, 인스턴스를 유지시키는 것은 불가능하다는 뜻이다.
1. Weak Reference(약한 참조)
class Dog {
var name: String
weak var owner: Person?
init(name: String) {
self.name = name
}
}
class Person {
var name: String
weak var pet: Dog?
init(name: String) {
self.name = name
}
}
크게 달라진건 없다 각 객체가 소유한 owner와 pet객체를 weak(약한참조)로 설정해뒀다.
그럼 owner와 pet은 약한참조가 되어 각 객체의 RC(Reference Count)를 증가시키지 못한다.
Weak 약한참조의 경우엔 참조하고있던 인스턴스가 사라지면 nil로 초기화된다.
nil로 초기화 되기 때문에 옵셔널로 사용해야한다.
이 말은 이렇게된다.
clamp = nil
ggomul.owner // ==> nil
ggomul의 주인을 nil로 할당한게 아닌 clamp만 nil을 할당했지만 ggomul의 주인도 nil로 바뀐다.
즉 ggomul.owner가 참조하고있던 clamp가 사라지면 자동으로 ggomul.owner도 nil로 초기화된다는 뜻이다.
아래처럼 메모리 해제를 할 수 있게된다.
clamp = nil
ggomul = nil
//둘 다 메모리 해제가 일어남
2. Unowned Reference(비소유 참조)
class Dog {
var name: String
unowned var owner: Person?
init(name: String) {
self.name = name
}
}
class Person {
var name: String
unowned var pet: Dog?
init(name: String) {
self.name = name
}
}
weak과 거의 흡사하다. 하지만 다른점이 있다.
비소유 참조의 경우, 참조하고 있던 인스턴스가 사라지면, nil로 초기화 되지 않는다.
weak는 nil로 초기화되고, unowned는 nil로 초기화되지 않는다.
그러므로 에러가 발생할 수 있다. nil은 값이 없음을 나타내는 키워드다. 하지만 참조하고 있던 인스턴스가 사라져서 참조주소가 nil이 되는게 아니라면 참조하고 있던 주소는 그대로인데 주소를 찾아가면 값이 없어서 에러가 발생한다.
에러가 발생하는 경우
gildong1 = nil
bori1?.owner // nil로 초기화 되지 않음 에러 발생
에러가 발생하지 않게 하려면?
gildong1 = nil
bori1?.owner = nil // 에러 발생하지 않게 하려면, nil로 재설정 필요 ⭐️
bori1?.owner
//정상적으로 메모리 해제가 일어남
Weak Refrence와 Unowned Refrence의 차이점
Weak Refrence
- 소유자에 비해, 보다 짧은 생명주기를 가진 인스턴스를 참조할 때 주로 사용 **
- 인스턴스가 nil로 확인 가능, nil인 경우 작업을 중단하는것 가능
** clamp가 ggomul이 보단 오래 살 것이다. 사람과 강아지이기 때문, 즉 clamp가 ggomul를 소유할 때
class Person{
weak var pet: Dog?
}
가 가능하다.
Unowned Reference
- 소유자보다 인스턴스의 생명주기가 더 길거나, 같은 경우 사용**
- 인스턴스 nil확인 불가능
- 실제 인스턴스가 해제되었다면 에러발생
**clamp가 ggomul보단 오래살 것이다. 사람과 강아지이기 때문, 즉 ggomul이 clamp를 소유할 때
class Dog{
unowned var owner: Person?
{
가 가능하다.
실제론 unowned사용시 한번 더 고려해야 할 것이 있기 떄문에, 실제로는 weak키워드를 사용하는 약한 참조를 실제 프로젝트에서 많이 사용한다.
weak/unowned 사용시 주의점
Swift 5.3부터는 Unowned도 Optional으로 선언할 수 있다
unwoned var name: String? <== 가능
캡처리스트 실제 사용 예시
let s = SomeClass()
let Strongcapture = { [s] in
print(s.num) // 강한참조
}
let weakcapture = { [weak s] in
print(s?.num) // 약한 참조(nil로 변할 수 있기 때문에 옵셔널로 사용)
}
// 예시를 위한 코드 실제로 이렇게 작성하진 않음.
import UIKit
class viewController: UIViewController{
var imageView: UIImageView?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
DispatchQueue.main.async {
// 이미지를 불러오는 코드...
---------------
self.imageView?.image = 불러온이미지
// 이미지를 불러오는 일은 오래걸리는 일이 될 수 있다.
//이미지를 읽어오기 전에 사용자가 viewController를 dismiss하게된다면
//DispatchQueue.main.async{}내부의 클로저가 self로 viewController를 강한참조하고있기 때문에
//viewController는 사라질 수 없다. 이미지를 다 불러온 다음(1초라면 1초 ~ 5초라면 5초)뒤에 화면에서 사라질 수 있다.
}
DispatchQueue.main.async { [weak self] in
// 이미지를 불러오는 코드...
---------------
self?.imageView?.image = 불러온이미지
// weak self를 사용해 약하게 참조한다면 클로저 내부에서 self를 강한참조하지 않기 때문에
//viewController의 RC가 상승하지 않고 사용자가 사진을 불러오기전에 viewController를 dismiss한다면
// 클로저가 self(ViewController)를 붙잡지않고 사라질 수 있다.
}
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// 이미지를 불러오는 코드...
---------------
self.imageView?.image = 불러온이미지
// self를 옵셔널 바인딩 했기 때문에 ?가 아님.
// 만약 이미지를 불러오기 전에 사라졌다면 self는 nil이 될것이고,
// guard문에서 return되어 더이상 viewController를 붙잡고 있지 않는다.
// 근데, 그 전에 이미 self는 weak이기 때문에 ViewController의 RC를 올리지 않는다.
}
}
}