iOS. Memory Management, ARC

sanghee·2021년 12월 23일
0
post-thumbnail

매주 진행하는 면접스터디에서 아래의 질문들에 대한 정리를 모은 글입니다.
Interview_Question_for_Beginner/iOS
https://github.com/Yongjai/TIL/blob/master/iOS
[Swift] 메모리 관리 ARC

왜 메모리 관리를 해야하는가?

메모리 부하가 일정 수준을 넘어가면 Warning이 발생한다. 더 올라가면 백그라운드 앱들이 꺼진다. 그럼에도 부하가 심하다면 현재 켜져있는 앱이 강제로 killed된다.

MRC(Manual Reference Count)

수동 래퍼런스 카운트이다. 말 그대로 수동으로 Reference Counting하는 것이다. Objective-C에서만 지원하며 Swift에서는 지원하지 않는다.

  • retain: 객체를 참조하고 싶은 경우
  • release: 참조를 풀고 싶은 경우

MRC 예시

  1. Person 클래스에서 me라는 객체를 만들었더니 Reference Count가 1이 되었다.
  2. retain을 이용해 객체를 참조하였더니 2가 되었다.
  3. release를 이용해 참조를 해제하여 0이 되었다.
  4. 마찬가지다.
  5. dealloc이 자동으로 호출되어 me 객체는 완전히 소멸된다.

📌ARC(Automatic Reference Count)

자동 래퍼런트 카운트이다. 말 그대로 자동으로 메모리 관리를 처리한다.

클래스의 인스턴스 등의 참조타입에만 적용된다.

보유한 카운트가 0이 되면 메모리에서 해제한다.

ARC 작동 방식

클래스의 새로운 인스턴스를 만들 때마다, ARC는 인스턴스에 대한 정보를 저장하기 위해 메모리 덩어리(chuck)를 할당한다. 그리고 인스턴스가 더이상 필요하지 않을 때 ARC는 사용된 메모리를 해제한다.

해제한 인스턴스에 접근한다면 크래쉬(Crash)가 나기에 ARC는 인스턴스에 대해 참조가 하나라도 있다면 해제하지 않는다. 프로퍼티, 상수, 변수에 클래스 인스턴스를 할당할 때마다 강한 참조를 만든다. 인스턴스를 강하게 유지하며 강한 참조가 남아있으면 메모리를 해제하지 않는다.

class Person {
    let name: String
    
    init(name: String) {
        self.name = name
        print("\(name) is inited.")
    }
    
    deinit {
        print("\(name) is deinited.")
    }
}

reference1에 Person 인스턴스가 강한 참조로 된다. 그리고 reference2, reference3까지 총 강한 참조가 3이다. 마지막 참조가 깨져야 Person의 인스턴스는 해제되며 deinit이 실행된다.

var reference1: Person?
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "이름") // 이름 is inited.
reference2 = reference1
reference3 = reference1

reference1 = nil
reference2 = nil
reference3 = nil // 이름 is deinited.

📌문제: 강한 순환 참조(Strong Reference Cycles)

ARC의 문제점이라 하면 두 개의 객체가 서로를 참조하는 경우와 같은 강한 순환 참조가 만들어질 수 있다는 점이다. 이 상태에서는 래퍼런스 카운트가 0에 도달하지 못하고 메모리 누수가 발생한다.

강한 순환 참조 해결방법

ARC의 문제점인 강한 순환 참조를 해결하기 위해 약한 참조나 미소유 참조를 사용한다.

  • 약한 참조는 다른 인스턴스의 생명주기가 짧을 때 사용하며
  • 미소유 참조는 다른 인스턴스가 같은 생명주기를 가지거나 더 길게 유지될 때 사용한다.

📌약한 참조(Weak Reference): weak var

약한 참조는 참조하는 인스턴스를 강하게 유지하지 않는 참조이며 다른 인스턴스의 생명주기가 짧은 경우 사용한다. 참조하고 있는 인스턴스를 강하게 유지하지 않기에, 약한 참조로 참조하고 있는 동안 인스턴스가 메모리 해제되는 것이 가능하다.

참조하는 인스턴스가 메모리에서 해제되면, ARC는 자동으로 약한 참조를 nil로 설정한다. 약한 참조는 언제든지 nil로 값이 변경될 수 있기에 상수가 아닌 옵셔널 타입의 변수로 선언되어야 한다.

주의할 점은, ARC가 약한 참조를 nil로 설정했을 때, 프로퍼티 옵저버는 호출되지 않는다.

📌미소유 참조(Unowned Reference): unowned let

📌키워드: unowned

미소유 참조도 인스턴스가 참조하는 것을 강하게 유지하지 않는다. 하지만 약한 참조와 달리 미소유 참조는 다른 인스턴스와 같은 생명주기를 가지거나 더 긴 생명주기를 가질 때 사용한다.

미소유 참조는 항상 값을 가지고 있는 것으로 간주하기에 ARC는 미소유 참조의 값을 nil로 설정하지 않으며 상수로 선언되어야 한다.

주의할 점은, 메모리가 해제되지 않는 인스턴스를 참조하는 게 확실할 때 미소유 참조를 사용한다. 인스턴스의 메모리가 해제된 후에 미소유 참조의 값에 접근을 시도하면, 크래쉬가 발생한다.

클로저에서의 강한 순환 참조

클래스처럼 클로저도 참조 타입이기 때문에 강한 순환 참조가 발생할 수 있다.

  • 클래스 인스턴스의 프로퍼티에 클로저를 할당할 때 클로저에 참조를 할당하는 경우
  • 클로저의 본문이 인스턴스를 캡쳐(capture)할 때 클로저가 self를 캡쳐하는 경우

강한 순환 참조가 발생할 수 있다.

캡쳐(capture)란 클로저의 본문이 인스턴스 프로퍼티에 접근하거나, 인스턴스의 메서드를 호출하는 것을 말한다.

클로저에서 강한 순환 참조 해결

클로저의 선언부에서 캡쳐 목록(capture list)를 정의하는 것으로 해결할 수 있다. 캡쳐 목록은 클로저 본문에 하나 이상의 참조를 캡쳐할 때 사용하는 규칙을 정의한다.

또는 강한 참조 대신 약한 참조나 미소유 참조로 선언해서 정의한다. 캡쳐된 참조가 나중에 nil이 될 수 있다면 약한 참조로 참조하고, 그렇지 않은 경우에는 미소유 참조로 캡쳐한다.

lazy var someClosure: () -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // 클로저 본문 작성...
}
profile
👩‍💻

0개의 댓글