Cocoa Internals - 자동 메모리 관리

자동 메모리 관리

ARC에 대해 알아본다.

ARC

Automatic Reference Counting

2010년 Clang을 공개하면서 정적 분석을 통해 실행 시점에서 발생할 수 있는 이슈를 Xcode 상에 표시할 수 있었다.

2011년 WWDC에서 ARC가 소개되었다. 이는 Clang 정적 분석 기술의 발전에 의한 것이었다.

수동 참조 계산 방식과 비교

여전히 객체마다 참조 횟수가 있고 객체 소유권에 대한 동일한 규칙을 적용하지만 ARC에서는 객체 소유권을 획득 및 반환하는 코드를 개발자가 작성하지 않고 컴파일러가 채워넣어준다.

ARC 규칙

메모리 관리 메소드를 구현하지 않는다.

retain, release, retainCount, autorelease, dealloc과 같은 메소드를 구현해서는 안되며, 호출해서도 안된다.

dealloc 메소드 (Swift의 deinit) 도 옵저버를 제거하는 등의 작업을 하지 않는다면 이 메소드를 구현할 필요가 없으며, [super dealloc] 코드를 넣지 않아야 한다.

Core Foundation 스타일의 객체를 관리하기 위한 CFRetain이나 CFRelease 등의 함수는 여전히 사용할 수 있다.

객체 생성을 위한 메소드 이름 규칙을 따른다.

alloc, new, copy, mutableCopy 계열의 메소드는 객체 소유권을 증가시키며, ARC 기반에서도 이러한 메소드 이름 규칙을 따르도록 한다.

init 계열의 인스턴스 메소드는 +alloc 메소드로 생성한 객체를 초기화하여 반환하는 용도로 사용되어야 한다.

C 구조체 내부에 객체 포인터를 넣지 않는다.

C 언어의 structunion 내부에 Objective-C 객체 포인터를 넣으면 ARC가 메모리 관리를 할 수 없다.

__unsafe_unretained 수식어를 사용하여 ARC가 관리하지 않도록 할 수 있으나 이에 대한 위험은 개발자가 감수해야 할 것이다.

id와 void* 타입을 명시적으로 타입 변환한다.

ARC에서는 객체 생명 주기를 관리하기 위해 타입 변환시 명시적으로 타입 변환 연산자를 사용해야 한다.

NSAutoreleasePool 대신 @autoreleasepool 블록을 사용한다.

NAutoreleasePool 객체를 사용하려 한다면 컴파일러가 오류를 표시할 것이다.

메모리 지역zone을 사용하지 않는다.

zone은 deprecated되었으며 ARC 환경에서도 마찬가지다.

소유권 수식어

__strong

강한 참조. Swift의 strong과 같은 개념. 기본 수식어.

객체 소유권을 획득하면서 참조한다.

__weak

약한 참조. Swift의 weak와 같은 개념.

객체 소유권을 획득하지 않으면서 참조한다.

해당 객체를 참조하는 곳이 없으면 객체는 즉시 사라지고 포인터는 nil이 된다.

__unsafe_unretained

미소유 참조. Swift의 unowned와 같은 개념.

객체 소유권을 획득하지 않으면서 참조한다.

객체가 사라지면 nil로 바꿔주지 않고 메모리 관리를 하지도 않아서 위험하다. 허상 포인터가 될 가능성이 있다.

참조하는 객체가 확실히 존재하는 경우나 참조할 객체가 약한 참조될 수 없는 경우에 이를 사용할 수 있다.

__autoreleasing

이 수식어를 통해 자동 해제될 대상이라고 명시할 수 있다.

타입 연결

Objective-C로 작성된 Cocoa 프레임워크 내부에는 Core Foundation 프레임워크가 있으며, 이는 C 언어로 작성되었다.

Objective-C 객체의 내부 구현도 Core Foundation C 구조체를 사용하므로 서로 타입 연결할 수 있다.

Core Foundation 구조체와 Objective-C 객체 사이를 비용 없이 연결toll-free bridge할 수 있다.

__bridge

Objective-C 객체 <-> Core Foundation 포인터 양방향 연결에 모두 사용 가능하다.

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

허상 포인터가 생길 가능성이 있으므로 개발자가 잘 관리해 주어야 한다.

__bridge_retained 또는 CFBridgingRetain

Objective-C 객체를 Core Foundation 포인터로 연결하면서 객체 소유권도 주기 위해 사용한다.

객체 소유권을 획득하므로 참조가 끝나면 CFRelease()와 같은 함수를 이용하여 소유권을 반환해야 한다.

__bridge_transfer 또는 CFBridgingRelease

Core Foundation 참조 포인터를 Objective-C 객체로 연결하면서 객체 소유권을 넘기기 위해 사용한다.

무비용 연결 타입

Core Foundation 구조체와 Foundation 객체는 서로 무비용 연결이 가능하며, 그러한 쌍들이 존재한다.

CFRunLoop <-> NSRunLoop / CFBundle <-> NSBundle과 같은 경우는 무비용 연결이 불가능하다.

프로퍼티와 인스턴스 변수

프로퍼티 수식어 ARC 소유권 수식어 특징
copy __strong 새로운 객체가 복사되고 소유권 획득
assign __unsafe_unretaiend 값만 그대로 할당
retain __strong 소유권 획득
strong __strong 소유권 획득
weak __weak 약한 참조
unsafe_unretained __unsafe_unretained 미소유 참조

요약

ARC를 사용하더라도 메모리 관리가 내부적으로 어떻게 이루어지는지 이해해야 한다.

Swift와 C/C++ 코드를 연결하기 위해서는 Objective-C 객체로 포장을 해야 하는 경우가 있다.

ARC 구현 방식

ARC 환경에서 컴파일러가 추가하는 코드는 Objective-C 런타임 함수로 구성된다.

강한 참조

NSString __strong *aString = [[NSString alloc] init];
id tmp = objc_msgSend(NSString, @selector(alloc));
objc_msgSend(tmp, @selector(init));
NSString* aString;
objc_storeStrong(&astring, tmp);

objc_storeStrong 구현

void objc_storeStrong(id *location, id obj) {
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}

강한 참조 방식으로 선언한 변수는 ARC 기반에서 objc_retain() 함수를 사용하여 객체 소유권을 갖게 된다.

자동 반환용 리턴 값

두 단계 생성 패턴 : 객체 인스턴스를 힙 공간에 생성 -> 할당한 메모리 공간을 초기 값으로 채워 넣음

Objective-C의 간편한 메소드convenience method는 위의 과정을 한번에 하면서 만들어진 객체는 자동 해제 대상이 된다.

NSDictionary __strong *dictionary = [NSDictionary dictionary];

위의 코드처럼 간편한 메소드로 객체를 생성하면 자동 반환 대상으로 자동 반환 목록에 등록한 객체를 반환한다.

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

objc_retainAutoreleasedReturnValue() 함수가 한 줄 추가된다. 자동 반환 목록에 등록되고 리턴받은 객체에 대한 소유권을 갖게 된다.

NSDictionary의 +dictionary 메소드 살펴보기

+ (instancetype)dictionary {
instancetype tmp = objc_msgSend(NSDictionary, @selector(alloc));
objc_msgSend(tmp, @selector(init));
return objc_autoreleaseReturnValue(tmp);
}

위처럼 간편한 메소드는 alloc, init, autorelease 메소드를 차례대로 호출한 것과 같은 효과를 내도록 구현되어 있다.

objc_retainAutoreleasedReturnValue() 함수에 대하여

내부적으로 이 함수는 무조건 소유권을 가져오지 않고, 해당 객체가 생성되었는지 확인하기 위해 스레드 TLS (스레드 내부 저장소) 영역에 정보를 저장하는 최적화 루틴을 포함한다.

이를 빠른 자동 반환Fast Autorelease for Return이라고 부른다.

iOS에서는 자동 반환 목록을 처리하는 비용이 커서 내부적으로 최적화한다.

약한 참조

NSString __weak *aString = [[NSString alloc] init];
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 구현

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

objc_storeWeak() 함수는 내부적으로 약한 참조 목록을 저장하는 해시 테이블을 구현하고 있다.

이전 객체가 있으면 기존의 약한 참조를 해지하고, 새로운 객체에 약한 참조를 등록한다.

objc_destroyWeak 구현

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

약한 참조 중인 객체가 사라질 때 nil로 바꿔주는 동작은 객체가 소멸될 때 한꺼번에 처리된다.

약한 참조 불가능 객체

objc_storeWeak() 함수의 수행 중 -allowsWeakReference 메소드를 호출하여 리턴 값이 NO이면 해당 객체는 이미 메모리가 해제된 상태에서 중복 해제되었다고 가정하며, 런타임 에러를 발생시킨다.

iOS에서는 약한 참조 불가능한 클래스가 존재하지 않는다.

자동 반환 방식

@autoreleasepool {
NSDictioanry __autoreleasing *dictionary = [[NSDictionary alloc] init];
}
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);

자동 반환 목록을 준비하고 / 객체를 생성하고 / objc_autorelease() 함수를 통해 객체를 자동 반환 목록에 추가한다.

요약

위의 내용은 ARC 환경에서 나타나는 메모리 문제를 해결하는 데 도움을 줄 것이다.

ARC 환경에서도 객체 인스턴스에 대한 메모리 관리를 신경 써야 한다.