스위프트의 메모리 관리

개요


모든 언어에서 메모리 관리는 중요하다.

C/C++은 포인터로 직접 관리해주고 자바는 가비지컬렉터라는 친구가 해준다.

어떻게 하는지 알아보자




메모리 관리의 역사


메모리 관리는 프로그램이 메모리를 필요로 할 때 메모리 영역을 프로그래머가 할당하고 필요하지 않을 때 해제하는 것을 말한다.

Objective-C에서는 메모리 관리를 레퍼런스 카운팅으로 바꿔 말할 수 있다.

레퍼런스 카운팅은 1960년 George E. Collins에 의해 제안되었다.

코코아 환경에서 처음 도입한 메모리 관리 모델은 Manual Retain Release(MRR)이다.

Manual Reference Counting(MRC)라고도 부른다. MRC는 객체의 소유권을 기반으로 메모리를 관리한다.

모든 인스턴스는 하나 이상의 소유자가 있는 경우 메모리에 유지된다.

소유자는 메모리가 더이상 필요없는 경우 소유권을 포기하고 소유자가 하나도 없는 메모리는 해제된다.

MRC는 객체의 소유권을 관리하기 위해 Retain Count를 사용한다.

이 모델을 사용해서 프로그램을 개발한다면 retain, release, autorelease를 이용하여 인스턴스 소유권과 관련된 코드를 짜야한다.

이건 너무 어렵다.

그래서 애플은 Automatic Reference Counting(ARC)를 도입했다.

MRR과 동일한 참조 카운트 모델을 사용하지만 향상된 컴파일러가 메모리 관리 코드를 자동으로 추가한다.

아래의 메서드들은 Objective-C의 언어가 아니라 Foundation 프레임워크에서 NSObject 클래스의 메서드들이다.



alloc/new


인스턴스 생성. c언어에서 malloc과 같은 역할



dealloc


인스턴스 소멸. free와 같은 역할



retain


인스턴스에 retain 메시지를 보내면 참조 카운트가 1 증가한다. 메세지를 보낸 곳에서 객체를 소유한다.

만약 alloc하고 retain을 했다면 두번 release 해야 한다.



release


인스턴스에 release 메시지를 보내면 참조 카운트가 1 감소한다. 메세지를 보낸 곳에서 객체를 포기한다.



autorelease


autorelease 메시지를 보내면 현재 사용 중인 autorelease pool 블록의 실행이 종료되는 시점에 참조 카운트가 1 감소한다.

여기서 autorelease pool이란 autorelease 메시지를 받은 인스턴스가 해제되기 전까지 저장되는 공간이다.

autorelease pool이 없는데 autorelease 메시지를 보내면 메모리 누수가 된다.

그래서 모든 프로젝트는 메인 스레드에서 동작하는 기본 pool을 제공한다.

모든 스레드는 autorelease pool을 가지고 있다.

중요하지만 GUI가 아닌 것을 개발할 때나, 반복문에서 다수의 임시 객체를 생성할 때, 스레드를 직접 생성할 때를 제외하고는 건들일이 없다.



autorelease와 release 차이






ARC


컴파일러가 코드를 분석한 후 객체의 생명주기에 적합한 메모리 관리 코드를 추가하는 것을 의미한다.

WWDC 2011에서 발표했다.

가비지 컬렉터는 런타임에 주기적으로 메모리를 정리하지만 ARC는 컴파일 시점에 코드가 자동으로 추가된다.

따라서 런타임에 메모리 관리를 위한 오버헤드가 발생하지 않는다.

오버헤드란 함수를 호출할때 함수 내용이 아닌 함수를 호출하는데 들어가는 비용을 말한다. 꼭 함수가 아니다.

ARC는 인스턴수룰 생성할 때마다 객체에 대한 정보를 저장하는 별도의 메모리 공간을 생성한다.

이 공간에는 인스턴스의 타입 정보와 속성 값이 저장된다.




강한 참조


해제된 메모리에 접근하는 코드는 런타임 오류의 원인이 된다.

ARC는 이러한 문제를 방지하기 위해 객체를 참조하고 있는 속성, 상수, 변수를 추적한다.

활성화된 참조가 하나라도 있으면 인스턴스는 해제되지 않는다.

이를 위해서 새로 생성된 객체는 자신이 할당되는 속성, 상수, 변수와 강한 참조를 유지한다.

쉽게말해 인스턴스가 계속해서 메모리에 남아있게 해주는 것을 강한 참조라고 할 수 있다.

흔히 우리가 참조한다고 하면 다 강한 참조이다.

인스턴스에 nil을 넣으면 참조횟수가 감소한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person {
    let name = "kyujin"
    var car: Car?
}

class Car {
    var model: String
    var owner: Person?

    init(model: String) {
        self.model = model
    }
}

var person: Person? = Person() // person's cnt == 1
var car: Car? = Car(model: "sonata") // car's cnt == 1

person?.car = car // 2
car?.owner = person // 2

person?.car = nil // 1
car?.owner = nil // 1

person = nil // 0
car = nil // 0

위에 처럼 안에서부터 다 해제시켜줘야할 수가 있다. 이걸 Stong Reference Cycle이라고 한다.

참고로 Strong Reference Cycle은 위에서 본 retain cycle이랑 같다. 컴파일러는 retain commands로 간주한다.

이건 너무 귀찮다.

그래서 약한 참조가 있다. 알아보자




약한 참조


약한 참조는 자신이 참조하는 인스턴스의 참조 횟수를 증가시키지 않는다.

해당 참조 구문 앞에 weak를 써주면 된다.

상수를 약한 참조 할 수는 없다. 약한참조는 항상 옵셔널 변수이어야한다.

ARC는 Zeroing Weak Reference라고 하는 약한 참조 방식을 사용하는데

이 참조는 자신이 참조하고 있는 인스턴스가 해제될 때 자신의 값을 nil로 초기화한다.




미소유 참조


이 방식은 한 객체가 다른 객체에 대한 참조 없이는 절대 존재할 수 없지만 Strong Reference일 필요는 없을 때 유용하다.

이 참조는 옵셔널 변수가 필수가 아니다. 상수나 일반 변수도 가능하다.

그러나 이건 위험하다. 따라서 참조되는 애가 참조자보다 확실히 오래 사는 것이 확실할 때만 사용한다. 그러나 이경우도 weak쓰는게 낫다.

예를들어 사람은 개를 가질 수도 안가질 수도 있지만 개는 주인이 무조건 있어야한다고 가정해보자.

개는 사람에 대한 참조를 가지고 있어야하지만 사람은 개가 있을 경우에만 필요하다. potentially retain cycle인 상황인 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func testUnowned() {
  class People {
    var dog: Dog?
    deinit {
      print("bye people")
    }
  }
  
  class Dog {
    unowned let people: People
    init(people: People) { self.people = people }
    deinit {
      print("bye Dog")
    }
  }
  let p = People()
  let d = Dog(people: p)
  p.dog = d
}
// Without unowned : No result
// With unowned: bye people, bye Dog




Stored anonymous functions


익명함수를 저장할 때 retain cycle이 발생할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class FunctionHolder {
  var function: (() -> ())?
  deinit {
    print("bye FuntionHolder")
  }
}
func testFunctionHolder() {
  let fh = FunctionHolder()
  fh.function = { // [weak fh] in
    print(fh)
  }
}
// without square brackets's result: Empty
// with square brackets's result: bye FunctionHolder

위의 예제에서 function이라는 익명함수를 저장하기 때문에 메모리 누수가 난다. 함수는 클로저이다.

FunctionHolder의 인스턴스 fh는 익명함수가 print(fh)라고 선언할 때 강한참조로 카운팅된다.

그러나 익명함수는 FunctionHolder 클래스가 만들어질 때 카운팅된다. 따라서 두번 카운팅 된다.

이 경우에는 function을 약한참조할 수 없다. class의 참조타입일 때만 약한 참조할 수 있기 때문이다.

그렇기 때문에 값을 Capture할 때 weak나 unowned로 선언해야한다.

그러나 caputure list를 weak로 하려면 익명함수가 옵셔널이어야한다.




weak self


escaping closure일 경우에 사용한다. escaping을 잘 생각해보면 만들어진곳 즉 self가 없어져도 클로저는 계속 사용할 때이다.

따라서 escaping closure가 아닐 경우에도 self가 클로저보다 먼저 없어지면 사용한다.