Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
iOS Developer/Swift

ARC, weak self, 캡쳐리스트

by Toughie 2023. 5. 28.

스위프에서 메모리 관리는 'ARC'를 통해서 이루어진다.

ARC(Automatic Reference Counting)


ARC는 객체에 대한 강한 참조(Strong Reference) 횟수를 추적하여 메모리에서 해당 객체를 해제하는 데 사용된다.

 

객체가 생성될 때마다 강한 참조 카운터가 1 증가하며, 해당 객체를 참조하는 다른 객체가 있을 때마다 카운터가 1씩 증가한다.
객체를 더 이상 참조하지 않을 때, 해당 객체에 대한 강한 참조 카운터는 1씩 감소한다.

강한 참조 카운터가 0이 되면 ARC는 해당 객체를 메모리에서 해제한다.

*강한 참조로 인해 순환 참조(Strong Reference Cycle) 발생할 있다.
순환 참조는 이상의 객체가 서로를 강한 참조하는 상황을 말한다.
-> 이렇게 되면 메모리에서 계속 해제되지 않아 메모리 누수 현상이 발생함.

이러한 상황을 방지하기 위해서, 메모리에서 객체를 적시에 해제시키기 위해서
약한 참조(Weak Reference)나 비소유 참조(Unowned Reference)를 사용할 수 있다.


weak  VS  unowned

weak 참조

참조 대상이 메모리에서 해제될 수 있으며, 참조하는 동안 nil로 자동 설정될 수 있는 약한 참조.
일반적으로 다른 객체가 이미 존재하는 동안에만 참조해야 하는 상황에 사용된다.

  • 참조 카운트를 증가시키지 않는다.
  • 대상 객체가 메모리에서 해제되면 자동으로 nil로 설정된다.
  • Optional 타입으로 선언되어야 한다.

unowned 참조

참조 대상이 메모리에서 해제될 수 없는 비소유 참조.
unowned 참조는 항상 값이 있다고 가정하며, 참조 대상이 해제되면 액세스 시 런타임 오류가 발생할 수 있다.

  • 참조 카운트를 증가시키지 않는다.
  • 대상 객체가 메모리에서 해제된 후에도 nil로 설정되지 않는다. 따라서 해제된 객체에 액세스하면 런타임 오류가 발생함.
  • Optional 타입으로 선언되지 않으며, 항상 값이 있다고 가정함. (Swift5 이후부터 옵셔널 선언은 가능)

사실 아직까지는 unowned 사용하는 것을 본 적이 없음..
런타임 오류 발생 가능성 때문에 사용하기 까다로울 것으로 예상

결국 weak와 unowned 둘 다 가리키는 인스턴스의 RC를 증가시키지 않음. (인스턴스간 강한 참조 방지)
weak/unowned로 선언한 변수를 통해 인스턴스에 접근은 할 수 있지만 그 객체를 붙잡고 있을 수는 없음(생명 유지 불가)

 

캡쳐 현상,  캡쳐리스트

클로저 { } 는 실행되는 동안 메모리의 힙(Heap)영역에 존재한다.
클로저가 실행되는 동안 외부의 값/참조 타입을 게속 사용하기 위해서 캡쳐 현상이 발생한다.(복사/ 저장의 의미로 캡처)

 

캡처 리스트(Capture List)
클로저의 캡처 동작을 명시적으로 제어하는 사용된다.
캡처 리스트는 클로저의 매개 변수 리스트와 바로 이전에 위치하며, 대괄호([]) 둘러싸여 있다.
캡처 리스트를 사용하여 캡처된 변수나 상수에 대한 약한 참조(weak) 비소유 참조(unowned) 명시할 있다.



Value Type

var age = 20

let myClosure = {
	print("나이: \(age)")
}

-> 클로저가 힙 영역에서 실행되는 동안 외부에 있는 age(값 타입)을 계속 사용해야 하기 때문에 age의 주소를 캡쳐함. (변수 주소 참조)

var age = 20

let myClosure { [age] in
	print("나이: \(age)")
}

-> 위와 같이 캡쳐리스트를 사용하면 외부 변수인 age의 값을 복사해서 사용함. (20이라는 정수를 그냥 복사해서 씀)

* 외부의 값을 복사해서 변화에 상관 없이 저장하고 사용하기 위해 활용할 수 있음.

Reference Type

//  Created by Toughie on 2023/05/28.
//

import Foundation

class MyClass {
    var age = 20
}

var x = MyClass()

let referenceCapture1 = {
    print("나이: \(x.age)")
}

let referenceCapture2 = { [x] in
    print("나이:\(x)")
}

-> referenceCapture1은 x 변수의 주소를 캡쳐함. (스택 영역에 있는 x 변수의 주소)
referenceCapture2는 캡쳐리스트를 통해서 x의 주소를 직접 캡쳐함.(힙 영역에 있는 x의 주소)
이 둘은 현재 강한 참조를 하고 있음.
즉 클로저의 실행이 끝나도 x의 참조 횟수(RC)가 줄어들지 않음.

참조 관계를 끊으려면 클로저 변수/상수에 nil을 할당해주거나

weak, unowned를 활용할 수 있음.
(변수나 상수에 클로저를 할당한 경우 nil을 할당하지 않는 이상 계속 힙 영역에 존재함)

강한 참조 방지/ 효율적인 메모리 관리를 위한 weak self

class Cat {
    var name = "Hayang"
    
    func meowForChur() {
        DispatchQueue.global().asyncAfter(deadline: .now() + 100) { [weak self] in
            print("\(self?.name)은 츄르가 먹고 싶다옹")
        }
    }
}

캡처리스트 내부에서 weak self 선언을 통해 약한 참조(RC를 올리지 않음)

    var myCat = Cat()
    myCat.meowForChur()

힙 영역의 myCat의 RC는 1임.
mewForChur()가 실행되면 클로저가 힙 영역에 생기고, myCat의 주소를 참조(가리킴)하지만 RC는 올라가지 않음.

weak는 언제 어떻게 써야할까?
강한 참조 사이클/메모리 누수 방지도 좋지만 모든 클로저에서 weak를 사용해도 된다는 의미는 아니라 생각한다.
꼭 완료되어야 하는 작업은 강한 참조를 해야하는 경우가 있을 수도 있으니..

약한 참조를 한 상태인데 갑자기 참조하고 있는 인스턴스가 메모리에서 해제되면
클로저 내부 self도 nil이 되어서 작업이 제대로 완료되지 못할 것이다.

네비게이션 뷰를 통해서 화면을 휙휙 넘어가는 앱이 있다고 치자.

화면마다 시간이 좀 걸리는 작업이 있어서 (데이터 로드 등) 
DispatchQueue.global().asyncAfter를 사용한다.

근데 여기서 만약 weak self가 아니라 그냥 self를 사용하면 
유저는 이미 다른 화면으로 넘어갔는데도 강한 참조 때문에 메모리에 불필요한 객체가 남아 있게 된다.
따라서 여기서는 weak self를 사용해서 다른 화면으로 넘어가면 이제 필요 없어진 객체를 
메모리에서 해제시켜줄 필요가 있는 것이다. 



스위프트에서 메모리 관리를 ARC를 통해서 하고,
강한 순환 참조(서로가 서로를 가리켜 메모리에서 해제가 되지 않아 메모리 누수를 일으킴)
를 방지하기 위한 대표적인 키워드인 weak에 대해 공부했다.


완벽하지는 않더라도 코드를 작성할 때 메모리를 계속 생각하는 습관을 기르자!