[Effective Objective-C] #30 ARC 를 사용하여 reference count 를 쉽게 만들라 |
출처 : Effective Objective-C
-
Clang 컴파일러 프로젝트(맥 OS X 와 iOS 개발에 사용되는 컴파일러 프로젝트)는 문제가 있는 참조 세기의 위치를 가리킬 수 있게 정적 분석기를 도입했다.
이 정적 분석기를 통해 메모리 누수가 있다고 알려줄 수 있다.
어디에 메모리 관리 문제가 있는지도 알려줄 수 있다.
이것이 ARC 가 만들어진 배경이다.
-
ARC 는 이름이 말하는 그대로의 일을 한다.
즉 reference counting 을 자동으로 한다.
그래서 이전 코드 조각에서는 release 등을 써 주던 것을 이제는 하지 않아도 ARC 가 알아서 추가해준다.
-
ARC 를 사용할 때 reference counting 은 여전히 계속 작동한다.
그러나 ARC 는 개발자 대신에 retain 과 release 를 추가한다.
-
ARC 가 retain, release, autorelease 를 여러분을 대신해 추가해주기 때문에 ARC 를 쓰면서 메모리 관리 메서드를 직접 호출하는 것은 허용되지 않는다.
특히 다음 메서드는 사용할 수 없다.
retain
release
autorelease
dealloc
이러한 메서드를 직접 호출하면 컴파일 에러가 발생한다.
ARC 가 어디에 메모리 관리 호출이 필요한지 계산하는 작업을 방해할 수 있기 때문이다.
ARC 가 수동으로 reference counting 하기 어려워 하는 개발자를 대신해 잘 해낼 것이라고 믿어 주기만 하면 된다.
-
사실 ARC 는 평범한 오브젝티브-C 메시지 디스패치를 이용해 이러한 메서드를 호출하지 않는다.
대신 동일한 일을 하는 로우 레벨 C 함수를 호출한다.
이 함수들은 최적화되어 있다.
retain 과 release 는 빈번하게 수행해야 하므로 CPU 사이클을 많이 아끼면 큰 이득이 된다.
retain 과 동일한 함수는 objc_retain 이다.
또 이는 retain, release, autorelease 를 재정의하면 안 되는 이유다.
이 메서드들은 절대 직접 호출되지 않는다.
ARC 를 적용하는 메서드 이름 짓기 규칙
-
메서드 이름을 이용해 메모리 관리 시맨틱을 결정하는 것은 오브젝티브-C 의 오래된 관례다.
그러나 ARC 는 관례를 좀 더 강한 규칙으로 바꾸었다.
이 규칙은 간단하고, 메서드 이름에 관련되어 있다.
객체를 반환하는 메서드는 만약 메서드 이름이 다음과 같이 시작하면 메서드 호출자(caller)가 소유한 객체로 반환한다.
alloc
new
copy
mutableCopy
‘호출자가 소유한’ 의 의미는 위 메서드들 중 하나를 호출하는 코드는 반환받은 객체를 릴리스할 의무가 있다는 것을 말한다.
즉 반환받은 객체의 호출하는 코드가 균형을 맞추어야 할 1의 값을 가진 리테인 수를 가질 것이다.
리테인 수가 1보다 클 수 있다.
그 이유는 객체가 추가적으로 리테인하거나 오토릴리스 할 수 있기 때문이다.
이것이 바로 retainCount 메서드를 사용하면 안 되는 이유다.
-
나머지 메서드 이름은 반환한 객체를 호출한 코드가 소유하지 않는다.
이 경우 객체는 오토릴리스되어 반환된다.
그래서 그 값(객체)은 메서드 호출 범위 내에서만 살아 있다.
객체가 더 오랫동안 생존하길 원하면 호출 코드는 그 객체를 리테인해야 한다.
ARC 는 자동으로 모든 메모리 관리가 이러한 규칙을 지키게 한다.
-
ARC 는 메모리 관리 규칙을 이름 짓기 관례를 통해 표준화한다.
오브젝티브-C 를 처음 배우는 이들에게는 생소할 수도 있다.
극소수의 언어가 오브젝티브-C 가 하는 것처럼 이름 짓기에 중점을 둔다.
이 개념에 익숙해지는 것이 훌륭한 오브젝티브-C 개발자가 되는 지름길이다.
-
retain, release 를 추가할 때도 ARC 는 이점이 있다.
그건 바로 직접 하기에는 어렵거나 불가능한 성능 최적화를 대신 해주기 때문이다.
예를 들어 컴파일 시간에 ARC 는 제거해도 되는 쓸모없는 retain, release, autorelease 를 없애버린다.
같은 객체에 대해 여러 번 retain 하고 여러 번 release 하면 ARC 는 때때로 retain, release 쌍(pair)를 제거하기도 한다.
또한 ARC 는 런타임 컴포넌트를 포함하고 있다.
여기에 적용된 최적화는 좀 더 흥미로울 뿐 아니라 새로 작성하는 모든 ARC 를 이용해야 하는 이유를 보여준다.
-
ARC 로 컴파일한 코드는 ARC 가 아닌 코드와 호환되어야 한다.
autorelease 상태로 return 하는 내용물이 바깥쪽에서 retain 한다면
ARC 에서는 autorelease 를 없애고 메서드로부터 반환하는 모든 객체가 +1 리테인 수를 갖게 해도 상관이 없다.
그러나 하위 호환성이 깨질 것이다.
사실 ARC 에는 필요 없는 autorelease 뿐 아니라 즉시 retain 을 하는 코드를 찾는 런타임 기능이 있다.
이는 객체가 오토릴리스된 상태로 반환 될 때 수행되는 특별한 함수를 통해 수행된다.
객체의 평범한 autorelease 메서드를 호출하는 대신에 objc_autoreleaseReturnValue 를 호출한다.
이 함수는 현재 메서드가 반환된 후 바로 실행될 코드를 살펴본다.
반환된 객체를 리테인하는 코드를 발견하면 autorelease 를 수행하는 대신에 전역 데이터 구조(프로세서 종속된)에 표식(flag)을 설정한다.
비슷하게 메서드에서 반환된 자동 해제된 객체를 리테인하는 것은 직접 retain 을 호출하는 대신에 objc_retainAutoreleasedReturnValue 라는 함수를 사용해 호출한다.
이 함수는 표식을 살펴본다.
그리고 설정되어 있으면 retain 을 수행하지 않는다.
이 표식을 설정하고 검사하는 부가적인 일은 autorelease 와 retain 을 수행하는 것보다 빠르다.
이 특별 함수들은 최적화된 프로세서 전용(processor-specific) 구현을 활용한다.
-
obj_autoreleaseReturnValue 에서 호출 코드가 객체를 바로 리테인하는지 여부를 탐지하는 방법은 프로세서에 따라 다르다.
이 탐지 방법은 오직 컴파일러 제작자만이 구현할 수 있다.
하위 수준의 머신 코드 인스트럭션(raw machinecode instruction)의 내성을 사용하기 때문이다.
컴파일러 제작자는 메서드를 호출하는 코드가 이와 같이 탐지되도록 보장해줄 수 있게 코드를 짤 수 있는 사람들이다.
-
이 최적화 방법은 메모리 관리를 컴파일러와 런타임에 넘길 수 있기 때문에 가능한 방법이다.
이는 ARC 를 사용하면 좋은 이유를 설명하는 데 도움이 된다.
컴파일러와 런타임이 발전하면 또 다른 최적화가 적용될 것이기 때문이다.
변수와 메모리 관리 시맨틱
-
ARC 는 또한 지역 변수와 인스턴스 변수의 메모리 관리를 다룬다.
기본으로 모든 변수는 객체를 강한 참조로 잡는다(hold)고 말한다.
이것을 이해하는 것은 중요하다.
인스턴스 변수는 특히 더 중요하다.
-
setter 를 이전에는 다음과 같이 작성했을 것이다.
- (void)setObject:(id)object {
[_object release];
_object = [object retain];
}
이미 인스턴스 변수로 설정된 값이 현재 설정된 인스턴스 변수와 동일한 값이면 어떻게 되는가?
이 객체가 인스턴스 변수의 객체를 참조하고 있는 마지막 것이면 세터에서의 릴리스 호출은 리테인 수를 0으로 떨어뜨리고 인스턴스 변수의 객체는 할당 해제(dealloc) 될 것이다.
이어지는 리테인 호출은 앱 크래시를 유발할 것이다
ARC 는 이런 종류의 실수를 막아준다.
동일한 세터를 ARC 를 사용하면 다음과 같이 쓸 수 있다.
- (void)setObject:(id)object{
_object = objet;
}
ARC 는 인스턴스 변수를 설정하기 전에 새로운 값을 리테인한 이후에 이전 값을 릴리스함으로써 안전하게 인스턴스 변수를 설정한다.
reference count 를 이해하고 있다면 이와 같이 올바르게 세터를 작성할 것이다.
그러나 ARC 를 사용하면 이러한 드문 경우(edge case)를 걱정할 필요가 없다.
-
지역 변수와 인스턴스 변수의 시맨틱은 다음 식별자를 적용해 변경한다.
__strong
기본값, 값이 리테인된다.
__unsafe_unretained
값이 리테인되지 않는다.
그리고 잠재적으로 안전하지 않다.
변수가 다시 사용될 때 이미 할당 해제되었을 가능성이 있다.
__weak
값은 리테인되지 않지만 안전하다.
현재 객체가 할당 해제되면 자동으로 nil 로 설정된다.
__autoreleasing
객체가 메서드에 참조로 전달될 때 이 특별한 식별자가 사용된다.
값이 반환될 때 오토릴리스된다.
-
인스턴스 변수의 동작이 ARC 를 사용하지 않을 때도 동일하게 만들려면 __weak, __unsafe_unretained 속성을 적용하라.
두 경우 모두 인스턴스 변수를 설정할 때 객체는 리테인되지 않을 것이다.
-
__weak 식별자를 가진 weak 참조를 자동으로 nil 로 설정하는 것은 오직 최신판 런타임( 맥 OS X 10.7 과 iOS 5.0 이상)에서만 가능하다.
최신판에 추가된 기능을 사용하기 때문이다.
-
지역 변수를 적용할 때 블록과 함께 발생할 수 있는 리테인 순환을 깨뜨리기 위해 식별자를 자주 사용한다.
블록은 자기가 캡처한 모든 객체를 자동으로 리테인한다.
때때로 블록을 리테인하는 객체가 블록에 의해 리테인되면 리테인 순환을 만들어 낼 수 있다.
__weak 지역 변수를 이 리테인 순환을 께뜨리기 위해 사용할 수 있다.
인스턴스 변수의 ARC 처리
-
ARC 는 인스턴스 변수 메모리 관리도 다룬다.
ARC 는 객체가 할당 해제될 때 정리하는 코드를 자동으로 생성해야 한다.
강한 참조의 모든 변수는 릴리스되어야 한다.
ARC 는 dealloc 메서드를 후킹(hooking) 하여 수행한다.
기존에는 아래와 같이 직접 release 해주었다.
- (void)dealloc{
[_foo release];
[_bar release];'
[super delloac];
}
ARC 를 사용하면 이렇게 dealloc 메서드를 직접 구현할 필요가 없다.
-
생성된 정리 코드(cleanup routine)는 오브젝티브-C++ 의 기능을 활용하여 위 코드의 두개의 릴리스를 여러분 대신 수행한다.
오브젝티브-C++ 객체는 할당 해제하는 동안 객체에 의해 잡힌(hold) 모든 C++ 객체의 소멸자(destructors) 를 호출해야 한다.
컴파일러가 C++ 객체를 포함(contain) 하고 있는 객체를 발견하면 .cxx_destruct 를 호출하는 메서드를 생성할 것이다.
ARC 는 이 메서드를 활용하여 이 메서드 내에 필요한 정리 코드를 생성해낸다.
-
CoreFoundation 객체나 alloc() 으로 생성된 heap 에 할당된 메모리 같은 것이 있으면 이것들에 대한 정리 코드도 필요하다.
그러나 이전에 했던 대로 상위 클래스의 dealloc 을 호출할 필요는 없다.
ARC 에서 명시적으로 dealloc 을 호출하는 것은 허용되지 않는다는 사실이 기억나는가?
그래서 ARC 는 여러분을 위해 .cxx_destruct 메서드를 생성하고 실행한다.
도 자동으로 상위 클래스의 dealloc 메서드를 호출한다.
ARC 에서 dealloc 메서드는 다음과 같이 될 것이다.
- (void)dealloc {
CFRelease(_coreFoundationObject);
free(_heapAllocatedMemoryBlob);
}
-
ARC 가 할당 해제 코드를 생성한다는 사실은 보통은 dealloc 메서드가 필요 없다는 것을 뜻한다.
이는 때때로 프로젝트 소스 코드의 상당한 양을 줄인다.
그리고 보일러 플레이트 코드를 줄이는 데도 도움을 준다.
메모리 관리 메서드 재정의하기
-
ARC 나오기 전엔 메모리 관리 메서드를 재정의할 수 있었다.
예를 들어 싱글턴을 구현할 땐 싱글턴 객체가 릴리스되지 않도록 release 를 아무런 일도 하지 않게 재정의하였다.
그렇게 하는 것은 ARC 에서는 허용되지 않는다.
이렇게 하면 ARC 가 객체의 생애 주기를 파악하는 데 방해가 될 수 있고 또 ARC 내에서 이러한 메서드를 호출하고 재정의하는 것은 허용되지 않기 때문이다.
ARC 는 오브젝티브-C 메시지 디스패치를 사용하지 않는 최적화를 수행한다.
retain, release, autorelease 가 실행될 필요가 있을 떄 런타임 깊숙한 곳의 C 함수를 대신 실행하는 최적화가 일어난다.
이는 이전에도 설명했지만 ARC 가 오토릴리스된 객체를 반환받는 즉시 리테인하는 최적화를 할 수 있다는 것을 의미한다.
기억할 점
ARC는 개발자가 대부분 메모리 관리에 대해 신경 쓰지 않게 해준다.
클래스의 보일러플레이트 코드를 없애주는 ARC 를 사용하라.
ARC 는 적절한 곳에 retain, release 를 추가함으로써 객체 생애 주기 거의 대부분을 다른다.
변수 식별자(qualifier)는 메모리 관리 시맨틱을 가리키기 위해 사용될 수 있다.
이전에는 retain, release 가 수동으로 배치되었다.
반환하는 객체의 메모리 관리 시맨틱을 알리기 위해 메서드 이름이 사용된다.
ARC 는 이것을 견고하게 만들었고, 이 규칙을 따르지 않는 것은 불가능하다.
ARC 는 오직 오브젝티브-C 객체만 다른다.
이는 ARC 가 CoreFoundation 객체를 다루지 않는다는 것을 의마한다.
그래서 적절한 CFRetain/CFRlease 호출이 반드시 적용되어야 한다.
'프로그래밍 놀이터 > iOS' 카테고리의 다른 글
[Effective Objective-C] #32 안전한 예외 처리 코드를 작성하려면 메모리 관리를 주의 깊게 다루라 (0) | 2017.09.25 |
---|---|
[Effective Objective-C] #31 참조를 릴리스하고 관찰 상태(observation state)를 정리하는 일은 dealloc 메서드에서만 하라 (0) | 2017.09.24 |
[Effective Objective-C] #29 참조 세기를 이해하라 (0) | 2017.09.22 |
[iOS] Swift 는 어떻게 Objective-C 보다 훨씬 빠른가? (0) | 2017.09.14 |
[iOS] Swift vs. Objective-C (0) | 2017.09.13 |
댓글