[Effective Objective-C] #29 참조 세기를 이해하라
출처 : Effective Objective-C
-
메모리 관리는 오브젝티브-C 같은 객체 지향 언어에서 중요한 개념이다.
어떠한 객체 지향 언어든 그 언어의 메모리 관리 모델을 깊이 이해하고 있으면 메모리를 효과적으로 사용하고 버그가 없는 코드를 작성할 수 있다.
이 법칙을 이해하고 나면 오브젝티브-C 의 메모리 관리가 더는 복잡하지 않고 ARC 를 쉽게 사용할 수 있을 것이다.
ARC 는 거의 모든 메모리 관리에 대한 결정을 컴파일러에 넘긴다.
그래서 개발자는 비지니스 로직에만 집중할 수 있다.
-
오브젝티브-C 는 메모리 관리를 위해 reference count 를 사용한다.
객체가 계속 살아 있길 원하면 count 수를 올리고, 그 객체의 사용이 끝났을 때 count 수를 내린다.
객체의 카운터가 0이 되면 그 객체를 사용하는 주체가 하나도 없게 되어 언제든 객체를 제거해도 된다.
-
맥 OS X 용 오브젝티브-C 코드에서 사용된 가비지 컬렉터는 맥 OS X 10.8 부터 공식적으로 deprecated 되었다.
그리고 가비지 컬렉터는 iOS 에서는 사용할 수도 없다.
Reference counting 은 어떻게 동작하는가?
-
reference counting 아키텍처에서 객체는 카운터를 가지고 있다.
카운터는 해당 객체가 살아 있는지 여부에 관심 있는 객체들의 수다.
오브젝티브-C 에서는 이 수를 리테인 수(retain count)라고도 하고, 참조 수(reference count)라고도 한다.
NSObject 프로토콜에 선언되어 있는 다음 세 가지 메서드는 카운터를 증가/감소하는 조작을 할 수 있다.
retain : 리테인 수를 늘린다.
release : 리테인 수를 줄인다.
autorelease : 리테인 수를 나중에 줄인다. 오토릴리스 풀(pool) 이 drain 될 때 리테인 수가 줄어든다.
-
retainCount 라는 리테인 수를 검사하는 메서드는 일반적으로는 전혀 유용하지 않다.
이는 디버깅할 때도 마찬가지다.
그래서 애플은 이 메서드를 사용하지 않는 것을 적극 추천한다.
-
객체는 항상 리테인 수가 최소 1인 상태로 생성된다.
retain 메서드를 호출하는 것은 이 객체의 생존 여부에 관심 있는 객체가 하나 있다는 것을 뜻한다.
객체가 코드의 특정 시점에서 더는 필요 없어져서 관심을 둘 필요가 없어지면 release 나 autorelease 가 호출된다.
리테인 수가 최종적으로 0에 다다르면 객체는 제거된다( dealloc )
-
메모리 트리의 최상위 객체(root)는 맥 OS X 의 경우 NSApplication 객체이고,
iOS 앱의 경우 UIApplication 이다.
둘 다 앱이 기동할 때 생성된 싱글턴이다.
-
release 를 직접 호출하면 ARC 로 컴파일되지 않을 것이다.
오브젝티브-C 에서 alloc 을 호출하면 호출자가 소유하는 객체가 반환될 것이다.
이는 호출자의 관심이 이미 등록되었다는 것을 말한다.
alloc 을 사용했기 때문이다.
그러나 리테인 수가 정확히 1이 아닐 수 있다는 점을 아는 것이 중요하다.
리테인 수는 1보다 클 수 있다.
alloc 또는 initWithInt: 등과 같은 함수의 내부 구현에서 반환할 객체에 대해 retain 을 여러 번 호출했을 수 있기 때문이다.
리테인 수는 최소 1이 보장되는 것이다.
항상 리테인 수를 이와 같이 생각해야 한다.
객체의 리테인 수가 여러분이 증가, 감소시킨 만큼 변경되어 있으리라 가정하면 안 된다.
-
객체가 제거되었을 때 그 객체의 메모리가 단지 가용 풀(available pool)로 되돌려진다.
NSLog 가 호출되었을 시점에 그 객체가 쓰던 메모리를 덮어쓰지 않았다면 객체는 여전히 존재할 것이고 크래시는 일어나지 않을 것이다.
너무 일찍 릴리즈된 객체에 대한 버그는 디버깅하기 어렵다.
더는 유효하지 않은 객체를 우연히 사용하는 것을 줄이기 위해 release 바로 뒤에서 포인터에 nil 값을 주는 코드를 가끔 보게 될 것이다.
이는 댕글링 포인터(dangling pointer)라고 하는 유효하지 않은 객체를 가리키는 포인터를 아무도 접근할 수 없게 보장한다.
[number release]
number = nil;
프로퍼티 접근자의 메모리 관리
-
- (void)setFoo:(id)foo{
[foo retain];
[_foo release];
_foo = foo;
}
새로운 값은 리테인되고 이전 값은 릴리즈된다.
오토릴리즈 풀
-
오브젝티브-C 의 reference count 구조에서 중요한 기능 중 하나가 오토릴리즈 풀이다.
release 를 호출해 즉각적으로 리테인 수를 줄이는 대신 (그리고 잠재적으로 객체를 할당 해제한다.) autorelease 를 호출할 수 있다.
이는 때때로 나중에 릴리즈를 수행한다.
-
이 기능은 매우 유용하다.
특히 메서드에서 객체를 반환받을 때 유용하다.
-
객체를 return 해주는 메서드에서 바로 그 객체를 release 할 수 없다.
반환하기 전에 바로 할당 해제되어 버리기 때문이다.
그래서 autorelease 를 사용해 객체가 지금이 아닌 나중에 릴리스되리라는 것을 가리킨다.
그러나 호출자가 반환받은 객체를 필요로 한다면 리테인할 시간만큼은 충분히 보장해준다.
( 즉 그 시간 안에는 오토릴리스한 객체를 릴리스하지 않는다. )
다시 말하면 객체가 메서드 호출 범위 내에서는 생존이 보장된다는 것이다.
사실 release 는 가장 바깥쪽의 오토릴리스 풀이 마르면 발생할 것이다.
자신의 오토릴리스 풀을 가진 경우를 제외하고 현재 스레드 루프가 끝나면 바로 발생할 것이다.
-
오토릴리즈 풀 내에 있는 객체들은 이벤트 루프가 끝나기 전에는 릴리즈되지 않을 것이다.
리테인 순환
-
많은 경우, 일명 리테인 순환(retain cycle) 이라는 것을 통해 참조 세기에 대해 알게 된다.
다수의 객체가 서로를 순환적으로 참조할 때 발생하고 메모리 누수를 초래한다.
순환 관계의 객체들은 어느 것도 리테인 수가 0이 될 수가 없기 때문이다.
-
가비지 컬렉터를 쓸 때는 이 상황을 보통 격리의 섬(island of isolation)이라는 것으로 인식하고 들어내 버린다.
이 상황에서 컬렉터는 이 세 객체 모두 할당 해제를 한다.
이 고급 해결책은 오브젝티브-C 의 reference count 구조에서는 불가능하다.
이 문제는 보통 weak 참조로 해결한다.
또는 외부에서 영향을 주어 객체 중 하나가 다른 객체의 리테인을 포기하게 만들어 해결한다.
이 경우 리테인 순환은 깨지고 메모리 누수도 더는 없을 것이다.
기억할 점
reference count 메모리 관리는 카운터를 증가/감소 시키는 방법으로 동작한다.
객체는 적어도 1의 카운트를 가지고 생성된다.
리테인 수가 양수인 객체는 살아 있다.
리테인 수가 0으로 떨어지면 객체는 파괴된다.
객체의 생애 주기 동안 객체는 이 객체의 참조를 가진 다른 객체에 의해 리테인되고 릴리스된다.
리테인하는 것과 릴리스하는 것은 각각 리테인 수를 증가시키고 감소시킨다.
'프로그래밍 놀이터 > iOS' 카테고리의 다른 글
[Effective Objective-C] #31 참조를 릴리스하고 관찰 상태(observation state)를 정리하는 일은 dealloc 메서드에서만 하라 (0) | 2017.09.24 |
---|---|
[Effective Objective-C] #30 ARC 를 사용하여 reference count 를 쉽게 만들라 (0) | 2017.09.23 |
[iOS] Swift 는 어떻게 Objective-C 보다 훨씬 빠른가? (0) | 2017.09.14 |
[iOS] Swift vs. Objective-C (0) | 2017.09.13 |
[Effective Objective-C] #28 프로토콜을 이용해 익명 객체를 제공하라. (0) | 2017.09.12 |
댓글