본문 바로가기
프로그래밍 놀이터/iOS

[Effective Objective-C] #30 ARC 를 사용하여 reference count 를 쉽게 만들라

by 돼지왕 왕돼지 2017. 9. 23.
반응형

 [Effective Objective-C] #30 ARC 를 사용하여 reference count 를 쉽게 만들라


출처 : Effective Objective-C

alloc, arc, arc dealloc, autorelease, boiler plate code, caller, CFRelease, cfretain, clang, cleanup routine, COPY, corefoundation, corefoundation dealloc, cpu 사이클, cxx_descruct, dealloc, descturctors, destruct 메서드, flag 설정, heap, Hooking, IOS, mutablecopy, new, nil, objc_autoreleaseReturnValue, objc_retain, objc_retainAutoreleasedReturnValue, OSX, override, processor-specific, raw machinecode instruction, reain release pair, reference count, release, retain, retainCount, super dealloc, super.dealloc, [Effective Objective-C] #30 ARC 를 사용하여 reference count 를 쉽게 만들라, __autoreleasing, __strong, __unsafe_unretained, __weak, 내성, 디스패치, 런타임, 런타임 컴포넌트, 로우 레벨 c 함수, 메모리 관리, 메모리 관리 시맨틱, 메서드 반환 후 바로 실행될 코드, 메서드 호출자가 소유한 객체, 변수와 메모리 관리 시맨틱, 보일러 플레이트 코드, 블록, 블록 리테인 순환, 소멸자, 소유한 객체, 인스턴스 변수의 ARC 처리, 재정의, 전역 데이터 구조, 정리 코드, 정적 분석기, 참조 세기, 최적화, 컴파일 에러, 컴파일러, 컴파일러 제작자, 크래시, 표식, 표준화, 하위 수준 머신 코드 인스트럭션, 후킹


-

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 호출이 반드시 적용되어야 한다.




반응형

댓글