ARC 규칙

IOS/Objective-C 2017. 1. 1. 16:03

개요
: 요약하면 프로그래머가 더 이상 "retain", "release"를 호출할 필요가 없다는 것이다. 다음 조건에서 여러분의 소스코드는 자동으로 ARC가 활성화된 상태로 컴파일 된다.
- clang(LLVM 컴파일러) 3.0 이상
- 컴파일 옵션 "-fobjc-arc"
* xCode 4.2부터는 Clang이 기본 컴파일러이고, -fobjc-arc 옵션도 기본으로 켜져있다. 따라서, Xcde 4.2 이상에서 작업하면 추가설정이 필요없다.


1) 소유권 수식어
- Objective-C는 id나 각 객체 타입을 사용하여 객체의 데이터 형을 지정한다.

객체 데이터 형은 NSObject *와 같이 Objective-C 클래스의 포인터 형이다. id은 클래스 이름을 숨길 때 사용한다. id는 C언어에서 void*와 동일하다.
ARC에서 id와 객체 데이터형 변수는 다음 네 가지 소유권 수식어 중 하나를 가져야 한다.

- __strong
- __weak
- __unsafe_unretained
- __autoreleasing

*_weak vs __unsafe_unretained 수식어
: __unsafe_unretained는 이름이 암시하듯 전혀 안전하지 않다. 이 속성을 갖는 변수를 ARC의 자동 메모리 관리에서 제외되기 때문에 직접 변수를 관리해 주어햐 한다. 

id __weak obj1 = nil;

{
    id __strong obj0 = [[NSObject alloc] init];
    obj1 = obj0;
    NSLog(@"A: %@", obj1);
}

NSLog(@"B: %@", obj1);
// 결과
// A: <NSObject: 0x753e180>
// B: (null)
// B를 찍을때는 obj0 변수가 변수범위를 벗어나서 강한참조가 사라지면서 이 객체가 제거될 때, 약한참조도 제거되어 obj1에 자동으로 nil이 대입된다. 

만약 위 코드에서 id __unsafe_unretained obj1 = nil;가 사용되면 B를 찍을때 obj1에 nil이 대입되는 로직이 적용되지 않아서 B를 찍는시점에는 ojb1에는 허상 포인터가 들어가 있을 것이다. 이 허상포인터에 접근하면 크래쉬를 발생시킨다. 

id __unsafe_unretained obj = [[NSObject alloc] init];

__unsafe_unretained 변수는 __weak과 마찬가지로 객체의 소유권을 갖지 않고, 위의 예제에서 객체는 생성되자마자 릴리스 된다. 


2) 컴파일러가 알아서 __autoreleasing을 처리해준다.
- 객체를 생성하지 않고 얻어오려면 alloc/new/copy/mutableCopy 메서드 그룹이 아닌 다른 메서드를 사용해야 한다. 위 4가리 그룹 메서드가 아닌 메서드가 반환하는 객체는 자동으로 오토릴리스 풀에 등록시킨다. 예외적으로 init으로 시작하는 메서드는 반환값을 오토릴리스 풀에 등록시키지 않는다.

- __weak 속성을 갖는 변수가 사용되면 객체는 언제나 오토릴리스 풀에 등록된다. 
id __weak obj1 = obj0;
NSLog(@"class =%@", [obj1 class]);

위 소스코드는 아래와 동일하다.

id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);

컴파일러가 첫번째 코드를 두번째 코드로 자동변환하는 이유는 __weak으로 수식되면 강한참조가 없기 때문에 어느 시점에서든 제거될 수 있기 때문이다. 만일 객체가 오토릴리스 풀에 등록되면 @autoreleasepool 블록이 끝나기 전에는 그 객체가 계속 존재하게 된다. 따라서 __weak 변수를 안전하게 사용하기 위해 객체가 자동으로 오토릴리스 풀에 등록된다.

- 더블 포인터 변수는 __autoreleasing 소유권 수식어를 갖는다.(id*, NSObject**)

NSError *error = nil;
NSError **pError = &error;
// 컴파일 오류를 발생시킨다. 객체 포인터를 대입하려면 두 변수의 소유권 수식어가 동일해야 한다.

컴파일 오류를 피하려면 다음과 같이 바꿔줘야 한다.
NSError *error = nil;
NSError *__strong *pError = &error;

코코아 프레임워크에는 수행결과를 인자로 반환하는 메서드들이 많이 있다.
performOperationWithError:(NSError **)error;
같은 메서드 들이다. ARC환경에서는 컴파일러가 다음과 같이 변환한다.
-> perfromOperationWithError:(NSError * __autoreleasing *)error;

NSError *error = nil;
BOOL result = [obj performOperationWithError:*error];
는 컴파일러가 어떻게 변환할까?

NSError __strong *error = nil;
NSError __autoreleasing *tmp = error;
BOOL result = [obj performOperationWithError:&tmp];
로 변환한다.


3) 객체 형의 변수들은 C언어의 구조체 또는 공용체의 멤버가 될 수 없다.

struct Data
{
    NSMuttableArray *array;
}
// 컴파일 에러 발생.

심지어 LLVM 컴파일러 3.0에서는 C 언어 명세의 제한 때문에 C구조체의 생명주기를 관리할 방법이 없다. ARC에서는 컴파일러가 메모리를 관리하기 위해 객체의 생명주기를 알고 관리해야 한다. 만약 C구조체에 객체를 넣길 원한다면 객체를 "void *"로 캐스팅하거나 __unsafe_unretained 소유권 수식어를 사용해서 할 수 있다.

컴파일러는 __unsafe_unretained 소유권 수식어를 사용한 변수를 관리하지 않는다. 메모리 누수 또는 애플리케이션이 강제 종료되지 않게 하려면 소유권을 직접 관리해야 한다.


4) 'id' 그리고 'void *'는 명시적으로 형 변환되어야 한다.

non-ARC 환경에서 'id'에서 'void *'로의 형 변환은 다음과 같이 아무 문제없이 작동한다.
그리고 void*에서 다시 대입한 id 변수를 통해서 메서드 호출도 문제없다.
/* non-ARC */
id obj = [[NSObject alloc] init];
void *p = obj;
id o = p;
[o release];

하지만 위 소스는 ARC 환경에서 컴파일 에러가 발생한다. ARC 환경에서는 id 또는 객체 타입들과 'void *' 사이에서 형 변환을 하기 위해서는 특별한 종류의 형 변환을 사용해야 한다.
단지 할당만 하기 위해서는 __bridge 형 변환을 사용할 수 있다.

- __bridge case
id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
id o = (__bridge id)p;

void *를 위한 __bridge 형 변환을 사용(void *p = (__bridge void *)obj;)하면 __unsafe_unretained 수식어 변수보다 더 위험하다. 객체의 소유권을 주의깊게 관리하지 않으면 댕글링 포인터 때문에 크래쉬가 난다.

__bridge_retained cast
id obj = [[NSObject alloc] init];
void *p = (__bridge_retained void *)obj;

위의 소스는 non-ARC 환경에서 다음과 같이 재작성할 수 있다.
id obj = [[NSObject alloc] init];
void *p = obj;
[(id)p retain];

__bridge_transfer cast
: 할당이 된 직후에 객체를 릴리즈하게 된다.

id obj = (__bridge_transfer id)p;

위 소스는 non-ARC 환경에서 다음과 같이 재작성할 수 있다.

id obj = (id)p;
[obj retain];
[(id)p release];
// obj 변수는 __strong으로 수식되었기 때문에 retain 되었다.

*Objective-C 객체와 Core Foundation 객체
- Core Foundation 객체는 Core Foundation 프레임워크에서 사용하는 객체로, 대부분 C 언어로 작성되었고 레퍼런스 카운트를 갖고있다. Core Foundation 프레임워크에서 사용하는 CFRetain, CFRelease 함수는 Objective-C에서 non-ARC 일때 사용하는 retain, release 메서드와 동일하다. Core Foundation 객체와 Objective-C 객체는 어느 프레임워크에서 객체를 생성하느냐에 따라 생성하는 방식이 다를 뿐 거의 같다. 일단 객체를 생성하고 나면 두 가지 프레임워크 모두에서 동일하게 사용할 수 있다. 예를 들어 Foundation 프레임워크 API로 생성한 객체여도 Core Foundation 프레임워크 API로 릴리즈할 수 있으며, 그 반대로 가능하다.

엄밀히 얘기해서 Core Foundation 객체와 Objective-C 객체는 동일한 객체다. 따라서 ARC 환경이 아닌 경우에 C 스타일 형 변환 방식으로 객체를 서로 변환할 수 있다. 이런 객체 형 변환 방식은 CPU 비용이 발생하지 않기 때문에 Toll-Free Bridge라고 부른다. 

ARC를 활성화한 환경에서 Objective-C와 Core Foundation 객체 사이의 Toll-Free Bridge 변환을 위해 다음과 같은 함수들이 제공된다.

CFTypeRef CFBridgingRetain(id X)
{
    return (__bridge_retained CFTypeRef)X;
}

id CFBridgingRelease(CFTypeRef X)
{
    return (__bridge_transfer id)X;
}

CFBridgingRetain Function
CFMutableArrayRef cfObject = NULL;

{
    id obj = [[NSMutableArray alloc] init];
    cfObject = CFBridgingRetain(obj);
    CFShow(cfObject);
    printf("retain count = %d\n", CFGetRetainCount(cfObject));
}

printf("retain count after the scope = %d\n", CFGetRetainCount(cfObject));
CFRelease(cfObject);
// 결과
// () . 빈 배열을 의미한다.
// retain count = 2
// retain count after the scope = 1

위 소스에서 cfObject = CFBridgingRetain(obj); 대신 cfObject = (__bridge_retained CFMutableArrayRef)obj;를 사용할 수 있다. 선호하는 것을 사용하면 된다.

CFBridgingRelease function
CFMutableArraytRef cfObject = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
printf("retain count = %d\n", CFGetRetainCount(cfObject));

id obj = CFBridgingRelease(cfObject);
printf("retain count after the cast = %d\n", CFGetRetainCount(cfObject));
//결과
// retain count = 1;
// retain count after the cast = 1;

위 소스에서 id obj = CFBridgingRelease(cfObject); 대신 id obj = (__bridge_transfer id)cfObject;로 대체할 수 있다. 선호하는 것을 사용하면 된다.

Property
ARC 환경에서 여러가지 Property 수식어들을 사용할 수 있다.

- assign: __unsafe_unretained 소유권 수식어와 대응.
- copy: __strong 소유권 수식어와 대응.
- retain: __strong 소유권 수식어와 대응.
- strong: __strong 소유권 수식어와 대응.
- unsafe_unretained: __unsafe_unretained 소유권 수식어와 대응.
- weak: __weak 소유권 수식어와 대응.
프로퍼티에 대입하는 것은 소유권 수식어에 해당되는 변수에 대입하는 것과 동일하다.

프로퍼티를 위해 인스턴스 변수를 선언했을 때 이는 프로퍼티와 같은 소유권 수식을 가져야 한다.
@interface SomeObject:NSObject
{
    id obj;
}
@end

@property (nonatomic, weak) id obj;
// 이것은 컴파일 에러를 발생 시킴.
// 멤버 변수를 __weak으로 수식하던가 property를 strong 속성으로 지정하여야 한다.

'IOS > Objective-C' 카테고리의 다른 글

Block의 모든것(2)  (0) 2017.01.02
Block의 모든것(1)  (0) 2017.01.02
GCD(Grand Central Dispatch)(2)  (0) 2016.12.19
GCD(Grand Central Dispatch)(1)  (0) 2016.12.19
Objective-C의 동적 바인딩, 그리고 Message dispather와 Runtime  (0) 2016.12.18
Posted by 홍성곤
,