아리의 iOS 탐구생활

[Swift/iOS] 메모리를 관리해주는 ARC에 대해 알아보자. 본문

Swift/iOS

[Swift/iOS] 메모리를 관리해주는 ARC에 대해 알아보자.

Ari Lee 2021. 8. 15. 19:52
반응형

🔍  Swift는 메모리 관리를 어떻게 할까?

ARC(Automatic Reference Counting)를 사용한다.

 

 

 

 

🔍  RC(Referenc Count)란 무엇인가?

인스턴스를 현재 누가 가르키고 있느냐 없느냐(참조하냐 안하냐)를 숫자로 표현한 것.

참조 계산 시점: Complie Time에 언제 참조되고 해제되는지 결정되어 런타임 때 그대로 실행된다.

장점: 개발자가 참조 해제 시점을 파악할 수 있고, RunTime 시점에 추가 리소스가 발생하지 않는다.
단점: 순환 참조가 발생 영구적으로 메모리가 해제되지 않을 있다.

 

Objective-C MRC(직접 할당/해제) 사용했었다. 

 

 

 

 

🔍  RC는 어떤 기준으로 숫자를 셀까?

Count Up :
인스턴스의 주소값을 변수에 할당할 때
인스턴스 생성(새로운 변수 대입)
기존 인스턴스 다른 변수에 대입
Count Down :
인스턴스를 가리키던 변수가 메모리에서 해제되었을 때
nil이 지정되었을 때
변수에 다른 값을 대입한 경우
프로퍼티의 경우 속해 있는 클래스 인스턴스가 메모리에서 해제될 때

 

 

 

 

🔍  ARC의 메모리 관리 방법

Referenc Count(참조 횟수)를 계산하여 참조횟수가 0이면
더 이상 사용하지 않는 메모리라 생각하여 해제한다.

 

기본적으로 클래스의 객체를 가리키는 각각의 reference는 강한 참조이다. 최소한 하나의 강한 참조가 있는   객체의 메모리는 해제되지 않는다. 만일 객체에 대한 강한 참조가 존재하지 않는다면 메모리에서 해제된다.

 

🔍  예제를 통하여 눈으로 직접 확인해보기

class TestClass {
    init() {
        print("init")
    }
    deinit {
        print("deinit")
    }
}

var test: TestClass? = TestClass() // init
test = nil // deinit
👉🏻    var test로 TestClass 객체에 대해 강한 참조가 이루어졌다. 후에 nil로 값을 바꾸어줌으로 강한 참조가 사라져 결국 TestClass 객체는 메모리에서 사라지게 된다.

 

 

 

 

🔍  Retain cycle

ARC의 원리는 제대로 작동을 하고 대부분의 경우 메모리에 대해서 개발자가 생각할 필요가 없다. 그러나 이러한 ARC가 작동하지 않는 상황이 몇몇 있으며 우리는 그런 상황을 생각해봐야 한다.

class TestClass {
    var test: TestClass? = nil
    init() {
        print("init")
    }
    deinit {
        print("deinit")
    }
}

var A: TestClass? = TestClass() // init
var B: TestClass? = TestClass() // init

A?.test = B
B?.test = A

A = nil
B = nil
// 각각 nil을 할당했음에도 불구하고 순환 참조로 인하여 deinit이 호출되지 않는다.
👉🏻    TestClass 클래스의 객체를 두 개 생성하였고, 그 두 객체 내의 인스턴스가 서로를 가리키고 있다. 두 객체를 전부 nil로 할당해주어 각각의 객체는 강한 참조를 하나씩 잃었다. 그러나 내부적으로 한개씩으로 참조를 더 갖고있다. 강한 참조가 하나 이상 이루어져있으면 메모리가 해제되지 않기 때문에 메모리가 해제되지 않을 것이고, 더 심각한 것은 우리의 코드에서 두 객체에 대한 참조가 존재하지 않기 때문에 더이상 메모리를 해제할 방법이 존재하지 않는다는 것이다.

 

🔍  순환 참조

두 개의 객체가 서로가 서로를 참조하고 있는 형태를 말한다.

순환 참조가 발생 시 영구적으로 메모리가 해제되지 않을 수 있다.

그러나 strong으로 선언된 변수들이 순환 참조가 됐을 시 문제점은 서로가 서로를 참조하고 있으면 RC가 0이 되지 못한다는 점이다. 이렇게 강한 참조를 사용하여 순환 참조에 문제가 생긴 경우를 ‘강한 순환 참조’라고 한다.

 

이런 경우를 메모리 누수(Memory Leak)라고 한다. 메모리 누수를 방지하기 위해 고안된 방법이 “연결 상태를 strong, weak, unowned으로 각각 상황에 따라서 약하게 연결할지 강하게 연결할지 결정하자!” 라는 것인데, 이것이 바로 Retain Cycle이라고 말할 수 있겠다.

 

iOS 앱에 이러한 메모리 누수가 많으면 메모리 사용량이 증가할 것이고, 결국엔 앱을 죽인다. 이것이 Retain Cycle 다뤄야 하는 이유이다. 이러한 메모리 누수를 피하려면 weak키워드로 피할 있다.

 

 

 

 

 

참조 키워드에 대해서 더 자세히 알아볼까?

 

 

 

🔍  참조 키워드 종류

🔍  String(강한 참조)

인스턴스의 주소값이 변수에 할당 될때 RC가 증가하면 강한 참조이다.

보통 참조의 dafault값이 강한 참조라고 볼 수 있겠다.

 

 

🔍  weak(약한 참조)

인스턴스를 참조할 시 RC를 증가시키지 않는다.

인스턴스가 메모리에서 해제된 경우 자동으로 nil이 할당되어 메모리가 해제된다.

weak는 무조건 Optional 타입의 변수여야 한다.

순환 참조를 일으키는 프로퍼티 앞에 weak키워드를 붙여주면 된다.

class TestClass {
    weak var test: TestClass? = nil // 약한 참조(weak) 선언
    init() {
        print("init")
    }
    deinit {
        print("deinit")
    }
}

var A: TestClass? = TestClass() // init
var B: TestClass? = TestClass() // init

A?.test = B
B?.test = A

A = nil // deinit
B = nil // deinit

즉 약한 참조는 서로를 참조할 수는 있지만 Reference Count가 되지 않는다.

 

 

🔍  unowned (미소유 참조)

weak와 같이 강한 순환참조를 해결할 수 있고, Reference Count를 증가시키지 않는다.

그러나 인스턴스를 참조하는 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신한다. 따라서 참조하던 인스턴스가 만약 메모리에서 해제된 경우 nil을 할당 받지 못하고 해제된 메모리 주소값을 계속 들고 있는다. (옵셔널 강제언래핑과 같은 느낌?)

그래서 다른 인스턴스의 수명이 동일하거나 길 때 사용한다. 

만약 미소유 참조로 선언된 인스턴스가 해제되었는데 접근하게 된다면 런타임 에러가 발생할 수 있다. 

 

 

 

 

✔️ Retain cycle in Closures

클로저가 인스턴스를 캡쳐하고 인스턴스가 클로저를 강한 참조로 저장하고 있다면 여기서도 마찬가지로 메모리 누수가 발생한다.

경우에도 약한참조와 미소유참조를 통해서 문제를 해결해야 한다.

class calculate {
    var A = 0
    var B = 0
    
    lazy var sum: () -> Int = { // 강한 순환 참조를 발생시키는 클로저
        return self.A + self.B
    }
    func addNumber() {
        self.A = 150
        self.B = 150
    }
    
    deinit {
        print("calculate deinit")
    }
}

var myCalculte: calculate? = calculate()
myCalculte?.addNumber()
myCalculte?.sum()
myCalculte = nil // nil을 할당했는데도 deinit이 호출되지 않는다.

 

 

 

 

✔️ Closure Capture List

클로저에서 약한참조 키워드를 추가하려면 Closure Capture List 이용해야 한다.

{ [참조] (파라미터) -> 반환타입 in
    code
}

보통 클로저에서는 파라미터와 리턴타입을 생략할 있었으나 클로저 캡쳐 리스트를 사용하는 경우에는 in키워드는 생략할 없다. 클로저 바디와 구분하기위해 반드시 필요하다.

{ [참조 캡쳐할대상]  in 
    code
}

 

 

따라서 위 예제를 아래와 같이 해결할 수 있다.

 

  • weak 키워드를 사용하여 gurad문으로 옵셔널 바인딩을 해주는 방법.
	lazy var sum: () -> Int = { [weak self] in 
        guard let strongSelf = self else { return 0 }
        return strongSelf.A + strongSelf.B
    }

 

  • unowned 키워드를 활용하는 방법
	lazy var sum: () -> Int = { [unowned self] in
        return self.A + self.B
    }

 

 

 

 

 

✔️  Refernce

 

Automatic Reference Counting — The Swift Programming Language (Swift 5.5)

Automatic Reference Counting Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you don’t need to think about memory management your

docs.swift.org

 

Retain Cycles, Weak and Unowned in Swift - Thomas Hanning

Retain cycles and the usage of the keywords weak and unowned are a little bit confusing. In this article you'll learn everything you need to know.

thomashanning.com

 

[Swift] 메모리 관리하기

오늘은 Swift로 메모리관리를 어떻게 하는지에 대해서 간단하게 적어볼게요!! 아래의 목차를 따라 적어보았습니다! Swift에서의 메모리 관리 Retain cycle Weak 키워드 Retain cylce in Delegate Retain cycle in..

wodyios.tistory.com

 

반응형
Comments