꿈돌이랜드

코코아 인터널스 - 3장 자동 메모리 관리 본문

Programming/iOS

코코아 인터널스 - 3장 자동 메모리 관리

loinsir 2023. 8. 9. 09:36
반응형

3.1.1 수동 참조 계산 방식과 비교

ARC를 사용해서 자동으로 메모리를 관리한다고 해서 2장에서 설명한 참조 계산 방식이 다른 방식으로 새롭게 바뀐 것은 아니다.

…중략

ARC에서도 여전히 객체마다 참조 횟수가 있고, 객체 소유권에 대한 동일한 규칙을 기준으로 참조 계산을 진행한다. 수동 참조 계산 방식은 객체 소유권에 대한 동일한 규칙을 기준으로 참조 계산을 진행한다. 수동 참조 계산 방식은 객체를 생성하면서 소유권을 가지며, 특정 객체를 참조하기 전에 소유권을 요청하고 참조한 이후에는 소유권을 반환한다. ARC에서도 참조 계산을 위한 규칙과 방식을 그대로 적용한다.

시간 흐름 →

MRC: alloc → init → doAction → retain → copy → release → release → dealloc

ARC: alloc → init → doAction → copy

3.1.2 ARC 규칙

ARC 기준으로 새로운 규칙을 알아보자. ARC에서는 수동 참조 계산 코드에서 쓰는 retain, release 메서드를 보내는 코드가 필요 없다. 컴파일러가 컴파일을 하는 동안 객체 인스턴스별로 생명주기를 분석해서 자동으로 retain, release 메시지를 보내는 코드를 채워 넣어주기 때문이다. 실행하기 위해 최종적으로 만들어진 바이너리 코드로는 수동 참조 계산 방식으로 작성한 코드나 컴파일러에 의해서 관련 코드가 자동으로 추가된 코드와 거의 동일하다. ARC를 적용하기 위한 규칙은 다음과 같다.

  1. 메모리 관리 메서드(retain, release, retainCount…)를 구현하지 말라.
  1. 객체 생성을 위한 메서드 이름 규칙을 따르라.
  1. C 구조체 내부에 오브젝티브-C 객체 포인터를 넣지말라.
  1. id와 void* 타입을 명시적으로 타입 변환하라.
  1. NSAutoreleasePool 대신 @autoreleasepool 블록 코드를 사용하라.
  1. 메모리 지역(zone)을 사용하지 마라.

3.1.3 소유권 수식어

ARC 방식에서는 객체를 선언할 때 변수 앞에 붙이는 소유권 수식어를 다음과 같이 정의하고 있다.

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing

__strong 수식어

해당 수식어는 소유권 수식어를 아무것도 입력하지 않았을 때 적용되는 기본 수식어다.

…중략

__strong 수식어의 의미는 해당 객체의 포인터를 (소유권을 갖고) 강하게 참조하고 있으므로 객체가 ‘살아있다’는 뜻이다.

.. 중략

범위 내에서 객체를 생성해서 소유권을 갖고 있다가도 범위를 벗어날 경우에는 소유권을 반환하기 위해서 release 메시지를 보내는 것이 원칙이다. 하지만 앞서 ARC 규칙을 설명했듯이, release 메시지를 보내는 작업은 생략해야만 한다. 그러면 컴파일러가 컴파일을 진행하면서 객체 생명주기를 해당 범위까지로 판단하고 범위가 끝나기 직전에 원래 있어야 했던 것과 동일하게 release 코드를 추가한다.

__weak 수식어

__weak 수식어는 __strong과 반대로 참조하는 객체가 살아있다는 것을 보장하지 않는 약한 참조를 의미한다. 대신 해당 객체를 참조하는 곳이 없으면 객체는 즉시 사라지고 포인터는 nil이 되어버린다.

…중략

__autoreleasing 수식어

코코아 프레임워크 내부에서 만든 객체를 넘겨받을 때는 __autoreleasing 지시어를 사용해서 자동 해제될 대상이라고 명시한다.

__unsafe_unretained 수식어

해당 방식은 __weak 수식어를 사용할 때와 마찬가지로 소유권을 갖지 않는 참조 관계는 비슷하다. 하지만 객체가 사라지면 nil로 바꿔주지도 않고 메모리 관리를 하지 않아서 안전하지도 않다. 따라서 ARC 기반에서 객체 포인터를 일시적으로 참조만 하는 경우에만 예외적으로 사용하기를 권한다.

…중략

안전하지는 모르지만 참조하는 객체가 확실히 존재하는 경우나 참조할 객체가 약한 참조될 수 없는 경우에 사용한다. 이런 경우를 제외하면 대부분은 __weak를 쓰는 것이 더 안전하다.

3.1.4 타입 연결

오브젝티브-C로 만들어진 코코아 프레임워크 내부에는 C언어로 만들어진 코어 파운데이션이라는 프레임워크가 있다. NSArray나 NSString와 같은 오브젝티브-C로 만든 객체도 내부에 구현한 코드는 코어 파운데이션 C 구조체를 사용하고 있다. 그래서 코어 파운데이션에 있는 CFArrayRef나 CFString 구조체는 오브젝티브-C 객체 포인터로 타입 연결할 수 있다. 물론 반대로도 가능하다. 이렇게 코어 파운데이션 구조체와 오브젝티브-C 객체 사이 연결은 추가적인 비용이 발생하지 않는다고 해서 무비용 연결(toll-free bridge)이라고 부른다.

…중략

오브젝티브-C객체와 코어 파운데이션 구조체 사이를 연결하기 위한 방법은 두 가지가 있다. 하나는 오브젝티브-C런타임에 구현되어 있는 객체 소유권 수식어를 사용하는 방법이고, 다른 하나는 코어 파운데이션 스타일의 매크로를 사용하는 방법이다.

방법#1. __bridge 방식

객체의 소유권을 넘기지 않고 타입 연결만 하는 경우에 사용

…중략

이와 같은 __bridge를 사용하는 타입 연결은 허상 포인터가 생길 수 있기 때문에 매우 위험하다.

방법#2. __bridge_retained 또는 CFBridgingRetain 방식

오브젝티브-C 객체를 코어 파운데이션 포인터로 연결하면서 소유권도 주는 경우에 사용한다. 객체 소유권을 주기 때문에 객체 참조 횟수는 1 증가한다. 참조가 끝나면 CFRelease() 같은 함수를 이용해서 소유권을 반환해야 한다.

… 중략

방법#3. __bridge_transfer 또는 CFBridgingRelease 방식

__bridge_retained와는 반대로 코어 파운데이션 참조 포인터를 오브젝티브-C 객체로 연결하면서 소유권을 넘기는 경우에 사용한다.

…중략

방법#4. 무비용 연결 타입

코어 파운데이션 구조체와 파운데이션 객체가 무비용 연결로 연결한다. ex) CFArrayRef ↔ NSArray

3.1.5 프로퍼티와 인스턴스 변수

클래스의 프로퍼티를 선언할 때 속성으로 지정하는 수식어와 ARC의 소유권 수식어는 밀접합 관계를 가진다. 특히 인스턴스 변수를 미리 선언하는 경우 인스턴스 변수의 소유권 수식어를 프로퍼티 속성과 동일하게 맞춰야만 한다.

… 중략

3.2 ARC 구현 방식

컴파일러가 추가하는 코드는 오브젝티브-C 런타임 함수로 구성된다…중략

3.2.1 강한 참조

…중략

강한 참조 방식으로 선언한 변수는 ARC기반에서 objc_retain() 함수를 사용해서 소유권을 갖는다는 것을 기억하자.

…중략

3.2.2 자동 반환용 리턴 값

객체 인스턴스를 만들 때, 오브젝티브-C에서는 객체를 생성하고 초기화하는 두 단계를 거쳐서 만든다. 첫 번째 단계에서는 객체 인스턴스를 힙 공간에 생성한다. 두 번째 단계에서는 할당한 메모리 공간을 초기 값으로 채워넣는다. 이런 방식을 두 단계 생성 패턴이라고 부른다.

…중략

객체 생성 메서드 중에 두 단계 초기화 패턴을 한꺼번에 처리해주는 간편한 메서드로 객체를 만드는 경우에는 만들어진 객체가 자동 해제 대상이다.

…중략

{
	NSDictionary __strong *dictionary = [NSDictionary dictionary];
}

간편한 메서드로 객체를 생성하면 자동 반환 대상으로 자동 반환 목록(AutoreleasePool)에 등록한 객체를 반환한다. ARC 기반에서 위 코드는 컴파일러가 다음과 같이 번역한다.

id tmp = objc_msgSend(NSDictionary, @selector(dictionary));
objc_retainAutoreleasedReturnValue(tmp);
NSDictionary *dictionary;
objc_storeStrong(&dictionary, tmp);

이 objc_retainAutoreleasedReturnValue 함수의 역할은 자동 반환 목록에 등록되고 리턴받은 객체에 대해 소유권을 갖는 것이다. 런타임 코드를 좀 더 깊이 살펴보면, 이 함수는 성능 최적화를 위해 무조건 소유권(retain)을 가져오지는 않는다. 이 함수에는 해당 객체가 생성됐는지 확인하기 위해 스레드 TLS 영역에 정보를 저장하는 최적화 루틴을 포함한다. 런타임 내부에서는 이 과정을 빠른 자동 반환 방식이라고 칭한다.

…중략

최신 런타임에서는 스레드 TLS 영역을 활용하는 최적화를 위해 SUPPORT_RETURN_AUTORELEASE 컴파일 옵션을 제공한다. 객체를 자동 반환 목록(AutoreleasePool)에 항상 등록하지는 않고, TLS 영역에 객체 생성 여부를 기록한다. 특히 iOS에서는 자동 반환 목록을 처리하는 비용이 커서 내부적으로 최적화한다.

3.2.3 약한 참조

__weak 소유권 수식어로 선언하는 약한 참조 방식에 대한 내부 구현을 알아보자.

{
	NSString __weak *aString = [[NSString alloc] init];
}

위 코드는 런타임 C 함수들로 재구성하면 다음과 같이 정리할 수 있다.

id tmp = objc_msgSend(NSString, @selector(alloc));
objc_msgSend(tmp, @selector(init));
NSString* aString;
objc_initWeak(&aString, tmp);
objc_release(tmp);
objc_destroyWeak(&aString);

…중략

objc_initWeak() 함수는 NSObject.mm 파일에 다음처럼 구현되어 있다.

id objc_initWeak(id *addr, id val)
{
	*addr = 0;
	if (!val) return nil;
	return objc_storeWeak(addr, val);
}

객체 포인터 값을 0으로 지정하고 objc_storeWeak() 함수를 호출한다. 해당 함수는 addr포인터에 있는 이전 객체와 val 객체에 대한 약한 참조 목록을 저장하는 일종의 해시 테이블을 구현하고 있다. 이전 객체가 있으면 기존의 약한 참조는 해지하고, 새로운 객체에는 약한 참조를 등록하는 방식으로 동작한다.

objc_destroyWeak() 함수는 다음과 같이 구현되어 있는데, objc_destroyWeak_slow() 함수를 호출하고 있다.

void objc_destroyWeak(id *addr)
{
	if (!*addr) return;
	return objc_destroyWeak_slow(addr);
}

objc_destroyWeak_slow() 함수는 앞서 설명한 objc_storeWeak() 함수로 등록한 약한 참조 목록에 대한 해시 테이블에서 해당 객체의 약한 참조를 해지한다.

…중략

약한 참조 중인 객체가 사라질 때 nil로 바꿔주는 동작(zeroing)은 객체가 소멸될 때 한꺼번에 처리된다. 런타임에서 객체가 소멸될 때 dealloc 메서드를 호출한 후에 object_dispose() 함수를 호출한다. 그리고 objc_destructInstance() 함수에서 C++ 객체인 경우 소멸자를 호출하고, 인스턴스와 관련된 객체를 제거한다. 마지막으로 objc_clear_deallocating() 함수에서 객체에 대한 약한 참조 포인터를 모두 nil로 바꿔준다.

약한 참조 불가능 객체

…중략

__weak 수식어와 관련된 숨겨진 -(BOOL)allowsWeakReference와 -(BOOL)retainWeakReference 메서드를 사용하는 약한 참조 불가능한 객체가 그것이다.

objc_storeWeak() 함수를 통해 약한 참조 객체를 등록하는 과정에서 객체에 -allowsWeakReference 메서드를 호출한다. 만약 리턴 값이 NO이면, 해당 객체는 이미 메모리가 해제된 상태에서 중복 해제됐다고 가정한다. 그리고 다음과 같이 에러를 표시하고 앱은 멈춘다.

…중략

-retainWeakReference 메서드는 약한 참조로 선언한 변수를 참조할 때 호출하는 objc_loadWeak() 함수 내부에서 사용한다. -retainWeakReference 메서드 자체가 구현되어 있지 않거나, 해당 객체에 메시지를 보내서 NO를 반환하면 objc_loadWeak() 함수는 nil을 반환한다. YES가 넘어와야만 해당 객체 참조 포인터를 반환하는 것이다. 따라서 약한 참조가 가능한 객체는 반드시 -retainWeakReference 메서드에서 YES를 반환해야 한다.

3.2.4 자동 반환 방식

객체 참조 변수에 __autoreleasing 소유권 수식어를 명시적으로 지정하는 자동 반환 방식에 대해 알아보자. ARC 방식이 아닌 수동 메모리 관리 규칙에서 NSAutoreleasePool 클래스를 사용하는 것과 참조 방식이 동일하다. 자동 반환 객체를 생성하는 코드를 살펴보자.

@autoreleasing {
	NSDictionary __autoreleasing *dictionary = [[NSDictionary alloc] init];
}

컴파일러가 바꾼 코드를 런타임 C 함수들로 재구성하면 다음과 같이 정리할 수 있다.

id pool = objc_autoreleasePoolPush();
id tmp = objc_msgSend(NSDictionary. @selector(alloc));
objc_msgSend(tmp, @selector(init));
NSDictionary * dictionary = tmp;
objc_autorelease(dictionary);
objc_autoreleasePoolPop(pool);

이 코드를 보면 우선 자동 반환 목록(pool) 객체를 준비하고, 객체를 두 단계로 생성한다는 것을 알 수 있다. objc_autorelease() 함수를 호출해서 해당 객체를 자동 반환 목록에 등록한다.

다음으로 편리한 메서드를 사용해서 객체를 만드는 경우와 비교해보자.

@autoreleasepool {
	NSDictionary __autoreleasing *dictionary = [NSDictionary dictionary];
}
id tmp = objc_msgSend(NSDictionary, @selector(alloc));
objc_msgSend(tmp, @selector(init));
NSDictionary *dictionary = tmp;
objc_retainAutoreleasedReturnValue(dictionary);
objc_autorelease(dictionary);
objc_autoreleasePoolPop(pool);

3.2.3 약한 참조에서 설명한 것처럼 자동 반환용 리턴 값을 이용해서 성능 최적화를 하는 objc_retainAutoreleasedReturnValue() 함수를 사용한다. 그리고 대상 객체에 따라 자동 반환 대상인지 판단하는 코드가 동작한다.

3.2.5 요약

…중략

ARC구현 방식은 새로운 운영체제 버전이 나올 때마다 개선된다. OS X 10.9 까지는 약한 참조로 객체를 참조할 때마다 자동 반환 목록에 등록하던 방식으로 동작했었지만, OS X 10.10부터는 더 이상 자동 반환 목록에 등록하지 않는다.

…중략


Uploaded by N2T

반응형