[iOS] 능력자의 블로그를 통해 되돌아본 ARC

즐겨찾기 해놓은 iOS 개발 블로그를 둘러보다 ARC에 관한 좋은 글을 발견하였다. 글 내용 중 본인이 몰랐던 또는 잊고 있었던 내용 중심으로 일부를 정리한다.  (원문 블로그 링크 )

이전에 본인이 포스팅한  [iOS] Automatic Reference Counting보다 훨씬 실용적인 내용이 많이 담겨져 있으므로 꼭 원문 블로그 링크의 글을 읽어보길 권한다.

Toll-Free Bridging

ARC는 Objective-C의 Object만을 대상으로 한다. Core Foundation과 같은 C/C++ 기반의 코드는  ARC 범주에서 벗어나 있다.

어플리케이션 개발에 Objective-C 클래스만을 이용할 수도 있겠지만, 때에 따라 Core Graphics와 같은 Core Foundation이 필요한 경우도 있다.

앞서 언급했듯이 Core Foundation은 ARC의 범주가 아니므로 Core Foundation API를 사용할 때는 개발자가 수동으로 메모리 관리를 해야한다. 그러나 다행히(?)도 Core Foundation 중 일부 클래스는 Objective-C  클래스로의 타입 캐스팅을 통해 Objectiv-C 객체처럼 사용가능하며 ARC에게 메모리 관리 책임을 맡길 수 있다. (이런 호환 가능한 클래스를 toll-free bridged type이라 한다. 리스트 참고)

Objective- C → Core Foundation이든 그 반대든, toll-free bridged type이 타입 캐스팅을 할 때는 compiler에게 메모리 관리 책임에 관한 정보를 알려줄 필요가 있는데 이때 사용하는 지시어가 __bridge, __bridge_transfer, __bridge_retained 이다.

1) __bridge

Objective-C Object인 NSObject 타입을 잠시동안만 Core Foundation 타입으로 변환하고 싶을 때 사용한다. ARC가 메모리 해지의 책임을 갖고 있다.


-(void)testMethodWithString:(NSString*)text{
...
... CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef)text, NULL,  .....);
...
}

위 코드의 CFURLCreateStringByAddingPercenctEscapes 메소드의 두번째 파라미터는 CFStringRef 타입이고 이는 NSString과 호환되는 toll-free bridged type이다.

text는 testMethodWithString: 함수의 파라미터로 strong 성격을 지닌다. 함수의 시작점부터 끝날 때까지는 text 객체는 해지되지 않는 것을 보장받는다. Core Foundation API인 CFURLCreateStringByAddingPercenctEscapes를 사용하면서도 이러한 속성은 유지가 되어야 하며 CFStringRef로 변환되더라도 ARC가 text 객체의 메모리 관리 책임을 갖는다.

2) __bridge_transfer

Core Foundation에서 NSObject로 타입 캐스팅 하는 경우 사용한다. 보통 생성된 Core Foundation 객체는 CFRelease()를 이용하여 메모리 해지를 해줘야 하는데, NSObject 타입으로 바뀌었으니 메모리 해지는 ARC가 담당해라 라는 의미다.


NSString *temp = (__bridge_transfer NSString*)CFURLCreateStringByAddingEscapes(...);

CFURLCreateStringByAddingEscapes(…); 메소드는  CFStringRef타입을 반환한다. CFStringRef는 NSString로 타입 변환이 가능하며, 보통 NSString으로 다루는 것이 개발자에게 편리하다. NSString 타입으로 변환하여 반환하는데 이때 ARC에게 메모리 관리 책임은 이제 너라고 알려주는 역할을 __bridge_transfer가 한다.

CFBridgingRelease()라는 helper 함수가 존재하는데 위 코드는  NSString *temp = CFBridgingRelease(CFURLCreateStringByAddingEscapes(…))과 동일하다.

CFBridgingRelease라는 이름은 CFURLCreate…로 Core Foundation 객체를 생성했으니 이와 쌍으로 release 한다는 의미를 가진다. 보통 Create, Copy, Retain으로 명명된 Core Foundation 함수를 호출할 때 생성 객체가 toll-free bridged type이면 CFBridgingRelease()를 사용하면 된다.

3) __bridge_retained

NSObject 타입을 Core Foundation타입으로 캐스팅하는 경우 사용한다.  __bridge와 다른 것은 메모리 해지의 책임이 ARC로 유지되는 것이 아니라 ARC의 책임이 아니게 된다. 즉 Core Foundation 타입으로 변환된 객체에 대한 메모리 해지의 책임은 개발자에게 있으며 CFRelease()를 통해 수동으로 관리를 해야 한다.

...
CFStringRef s2 = (__bridge_retained CFStringRef)s1; // s1은 NSString 타입
...

CFRelease(s2); //명시적으로 메모리 해지 필요

CFBridgingRetain()이라는 helper 함수가 존재하는데 위 코드는 CFStringRef s2 = CFBridgingRetain(s1); 과 동일한 의미를 가진다. CFRelease()와 쌍을 맞추기 위해 Retain()이 들어갔다고 이해하면 된다.

__bridge는 Core Foundation간의 변환에만 사용되지는 않는다. 어떤 API는 void* 를 파라미터로 갖는 경우도 있다. void* 파라미터에는 Objective-C object, Core Foundation object등 어떠한 타입도 들어갈 수 있다는 의미다. (Objective-C 내에서 사용되는 id 타입과 헷갈리지 말자.)

// objective-c => void*
MyClass *myObject = [[MyClass alloc] init];
[UIView beginAnimations:nil context:(__bridge void*)myObject];
...

// *void -> objective-c
-(void)animationDidStart:(NSString*)animationid context:(void*)context{
...
MyClass *myObject = (__bridge MyClass*)context;
...

}

Strong

  • strong 속성을 지닌 property와 Instance variable을 사용할 경우, 변수가 더 이상 필요하지 않는 시점에 변수를 nil로 할당하는 것이 좋다. (개인적으로 자주 까먹는다. nil처리를 하지 않았다고 어플이 쉽게 죽거나 하진 않기 때문).
  • UIViewController 안에 strong 변수가 있는 경우 viewDidUnload() 메소드에서 nil 처리를 하면 UIViewController가 deallocated 되기 전에 불필요한 메모리 사용을 막을 수 있다.

read-only 속성

Property로 선언된 변수인 경우 self.name  같이 항상 Property 방식으로 접근하는 것이 좋다. property로 선언된 변수를 property’s backing instance ( .m 파일에서 프로퍼티 변수를 _name과 같은 형식으로 접근할 수 있다.)로 접근하는 경우는  init 메소드 또는 custom getter/setter 메소드로 한정하라.

@property (nonatomic, strong, readonly) NSNumber *temperature;

헤더파일에 위와 같이 readonly로 정의된 property인 경우, @implentation 에서 _temperature를 통하여 값을 변경한다면 ARC는 이상한 버그를 낼 수도 있다.

이 경우 좋은 솔루션은 header 파일의 선언은 그대로 두고 .m 파일에서 class extension을 이용하여 @property(nonatomic, strong, readwrite) NSNumber *temperature 로 재정의 하여 사용하는 것이다.

IBOutlet 

  • IBOutlet property에 권장하는 속성은 weak이다.
  • nib 파일에서 최상위 view(보통 self.view)가 IBOutlet으로 정의한 view를 소유하고 있으므로 weak 지시어로 생성하여도 무방하다.
  • IBOutlet을 weak으로 선언하는 가장 큰 이점은 viewDidUnload()메소드에서 nil처리를 하지 않아도 된다는 점이다. (최상위 view가 파괴될 때 자동으로 nil 처리가 된다. viewDidUnload시점엔  nil 이 되어 있다.)
  • iOS 디바이스가 low-memory warning을 받으면 view가 unload되는데 이때 weak으로 처리해야 IBOutlet에 연결된 view도 자동으로 해제된다.

그 밖의 정보 & Tip

  • LLVM 3.0 컴파일러를 도입하면서 .m 파일 (@implementation 내부)에 instance variable 설정이 가능해졌다.
  • MRC 에서 개발자가 @property(nonatomic, retain)을 즐겨 사용하는 이유는 메모리 관리를 편하게 하기 위해서였다. 프로퍼티에 retain 키워드를 넣음으로써 변수 할당때마다 retain (or copy) 키워드를 넣을 필요가 없다. ARC에서 이는 더이상 특별한 이점이 아니다. ARC에선 다른 클래스에서 사용할 경우에만 property를 이용하는 것이 좋다.
  • copy property : NSString 또는 NSArray에 copy property를 사용하는 경우가 많다. 이는 프로퍼티에 할당하더라도 기존의 값이 변경되지 않길 바라는 경우에 사용한다. copy를 하면 할당된 객체와 동일한 객체를 새로 만들어서 변수에 할당한다. copy는 strong과 동일한 성격을 지닌다.

== updated 2013/7/25  ==

확인해보니 iOS 6.0부터 viewDidUnload 메소드가 deprecated 되었다. iOS 6 부터는 low-memory 상태에서 viewDidUnload가 호출되지 않는다.

< from iOS developer library >

위에서 정리된 내용 중 viewDidUnload()가 언급된 곳은 IBOutlet 과 strong property 두 곳이다.

  •  IBOutlet : 권장 프로퍼티가 weak라는 것에는 문제는 없어 보인다. low-memory warning과 별개로 nib파일의 최상위 view에서 이미 IBOutlet으로 선언한 view를 소유하고 있으므로, UIViewController에서 다시 strong 속성으로 view를 소유하는 것은 효율적이지 않아 보인다.
  • strong 변수의 nil 처리 : viewDidUnload는 low-memory warning에서만 호출되도록 되어 있다. 그러나 더이상 low-memory 상태에서 호출되지 않으므로 didReceiveMemoryWarning()에서 현재 뷰에 사용되지 않는 strong 변수에 대하여 nil처리를 하여야 한다. strong 변수를 property로 선언하고 getter메소드에 nil인 경우를 처리해 놓는다면, didReceiveMemoryWarning()에서 nil로 할당된 변수가 다음에 사용될 경우 간편하게 처리될 수 있을 것 같다.

[iOS] Automatic Reference Counting (ARC)

iOS 5부터 Apple이 LLVM Compiler 를 채용함에 따라 ARC (Automatic Reference Counting)가 iOS 개발에 적용되었다.

Java나 C#만을 다룬 상태에서 iOS 4.x 후반대에 iOS 공부를 시작한 본인으로서는 개발 입문 후 곧 소개된 ARC가 큰 힘(?)이 되었다.

ARC 관련하여 개발 서적, iOS developer reference 등을 보았지만 가장 이해하기 쉬운 자료는 WWDC 2011 자료(애석하게도 WWDC 2011 Introduction Automatic Reference Counting 설명 자료는 Apple 개발자 계정이 있어야 볼 수 있음)였다. 이번 포스팅은 WWDC 자료를 바탕으로 ARC 에 대해 정리한다.

ARC 도입 이유

< from WWDC 2011 >

애플은 ARC 도입 이유는 다음과 같다.

  • 앱의 비정상 종료 원인 중 많은 부분이 메모리 문제. 메모리 관리는 애플의 앱 승인 거부(Rejection)의 대다수 원인 중 하나. 
  • 많은 개발자들이 수동적인 (retain/release) 메모리 관리로 힘들어함.
  •  retain/release 로 코드 복잡도가 증가.

결국 개발자가 좀 더 편하게 앱 개발을 할 수 있도록 해주겠단 의미다. 개발자는 객체 관계에 더 집중하면 된단다.

ARC 란?

< from WWDC 2011 >

ARC는 Automatic Reference Counting의 약자로 기존에 수동(MRC라고 함)으로 개발자가 직접 retain/release를 통해 reference counting을 관리해야 하는 부분을 자동으로 해준다.

위 자료를 보면 Object가 노란색으로 강조되어 있다. 실제로 ARC는 Objective-C의 Object만을 대상으로 하며 이로 인해 구조체나 C 기반의 Core Foundation 사용에는 제약이 존재한다. 이에 대해서는 아래에서 다시 언급하도록 하겠다.

< from WWDC 2011 >

ARC는 GC (Garbage Collector)와는 다르게 런타임이 아닌 컴파일 단에서 처리된다.

GC는 런타임에 메모리를 검사하기 때문에 앱 퍼포먼스에 악영향을 준다. 그러나  ARC는 개발자가 직접 코딩하던 retain/release를 컴파일러가 자동으로 코드에 삽입시키므로, 동작시에는 MRC와 동일하여 성능 저하를 유발하지 않는다.

Apple은 iOS SDK 문서에서 차후 Mac OSX에서도 ARC가 GC를 대체할 것이라 밝히고 있다. 애플에서 제공하는 ARC 퍼포먼스 향상 효과는 다음과 같다.

< from WWDC 2011 >

ARC를 위한 규칙

컴파일러가 자동으로 retain/release 코드를 삽입시키기 위해 몇 가지 규칙을 제안한다. 이를 알아보자.

< from WWDC 2011 >

[첫 번째]  retain/release/autorelease 를 이용하지 마라.

더 이상  retain/release/autorelease를 코드에 직접 삽입할 필요가 없다. 컴파일러가 알아서 다 해준다. 블록(blocks)도 Object이므로 기존 블록에 사용하던 copy, autorelease는 불필요하다. 메소드의 return에 대한 autorelease도 고려할 필요 없다. 그냥 짜면 컴파일러가 알아서 해준다.

dealloc 메소드는 사용할 수는 있지만 이는 인스턴스 변수들의 메모리 해제(release)가 아닌 자원 관리 차원에서 허용된다. 또한 개발자에 의해 재정의된 dealloc 메소드에서는 [super dealloc]을 호출해서는 안된다

< from WWDC 2011 >

[두 번째 규칙] C 구조체 내에 Object 타입을 사용하지 마라.

ARC는 Object만을 고려한다. 구조체 내의 Object는 ARC가 계산하기 어렵다. 그냥 구조체가 아닌 Object  (Objective-C Class)를 사용하라.

< from WWDC 2011 >

[세 번째 규칙] Core Foundation은 Objective-C의 Object 타입이 아니므로 Core Foundation과 Object 간의 명시적인 타입 캐스팅이 필요하다.

추가로 Core Foundation 스타일의 객체에는 CFRetain, CFRelease 와 같은 메모리 관리 함수를 사용할 수 있다. 다음을 참조하자 – “Managing Toll-Free Bridging”

< from WWDC 2011 >

[네 번째 규칙] NSAutoreleasePool 사용하지 마라. NSAutoreleasePool은 내부적으로 보면 실제로는 Object가 아니다. 대신 @autoreleasepool{}을 사용하라. 속도도 더 빠르다. @autoreleasepool{}은  loop에 쓰이는 경우가 많다. NSError에도 쓰이지만 자주 사용되지는 않는다. 그래도 경험상 거의 사용할 일이 없는 듯 하다.

새로운 지시어

1) 프로퍼티 속성

아래 예처럼 strong, weak 키워드가 프로퍼티에 사용된다.

// The following declaration is a synonym for: @property(retain) MyClass *myObject;
@property(strong) MyClass *myObject;

/ The following declaration is similar to "@property(assign) MyClass *myObject;"
/ except that if the MyClass instance is deallocated,
// the property value is set to nil instead of remaining as a dangling pointer.

@property(weak) MyClass *myObject;

strong은 retain과 의미가 거의 같다.

weak는 assign 키워드처럼 참조 카운트를 증가시키지 않는다.  그러나 assign과는 다르게 참조하는 객체가 해제(deallocated)되면 weak로 선언된 프로퍼티는 자동으로 nil이 대입된다. assign의 경우엔 nil 이 대입되지 않아 의도치 않은 포인터를 가리킬 수 있었다. assign과 거의 동일한 의미의 지시어는 unsafe_unretained 이다.

ARC는 강한 참조 사이클(strong reference cycle)을 방지하지 못한다. 이를 위해 개발자가 강한 참조 사이클이 발생하지 않도록 약한 참조(weak)를 잘 사용해야 한다. (참고- “Practical Memory Management”)

2) 변수 지시자(variable qualifier)

  • __strong : 디폴트 지시자. 객체의 소유권을 획득한다. (retain count 증가)
  • __weak : 객체의 소유권을 가져오지 않고 참조한다. 참조한 객체가 소멸되면 nil 로 설정된다.
  • __unsafe_unretained : __weak 와 비슷하지만 참조 객체 해제되어도 nil 로 재설정되지 않는다. 변수의 값은 특정 포인터를 계속 가리키고 있으므로 변수 사용에 유의해야 한다. 오류가 발생할 수 있다.
  • __autoreleasing : 보통 메소드의 (id *) 타입 파라미터 인수에 사용하며 함수 리턴시 자동으로 해제된다. Cocoa Framework 의 간편 생성자(Convenience Initializer)역시 __autoreleasing 타입의 객체를 반환함.

__strong, __weak, __autoreleasing 변수는 생성시 자동으로 nil 로 초기화 된다. 변수 선언 형식은 다음과 같다.

ClassName * qualifier variableName;

__weak 를 사용할 경우 아래와 같은 실수를 하지 않도록 주의해야 한다.

NSString * __weak string = [[NSString alloc] initWithFormat:@"First Name: %@", [self firstName]];
NSLog(@"string: %@", string);

결과적으로  string 변수는 바로 nil 로 설정된다.

단계적으로 다음과 같이 이해하면 쉽다.

1) 우측 구문이 샐행되어 NSString 객체 생성.

2) NSString 객체가 string 변수에 할당

3) string 변수는 __weak으로 선언. 따라서 기존의 참조 카운트를 그대로 갖는다. 따라서 초기화된 NSString의 참조카운트 0(NSString 객체를 강한 참조로 소유하는 다른 객체 없음)이 그대로 넘어가고, 참조 카운트가 0이므로 weak 특성에 따라 바로 nil 로 재설정.

__autorelease 는 일반적으로 잘 사용되지 않는다.

기타

1) 지원 OS

  • Xcode 4.2
  • Mac OS X v10.6 and v10.7 (64 – bit application)
  • iOS 4 and iOS 5
  • weak 참조는 OS X 10.6 및 iOS 4 에서 미지원. 따라서 weak 참조를 지원하는 완벽한 ARC 기능은 OS X 10.7 및 iOS 5 버전부터

2) NSAllocateObject , NSDeallocateObject 를 사용할 수 없다. alloc을 이용해서 객체를 생성하면, Runtime이 객체 해제(deallocate)를 알아서 처리한다.

3) ARC 는 다음과 같은 메소드 네이밍에 대한 제약을 갖는다.

  • 프로퍼티 접근자(accessor) 이름이 new 로 시작해서는 안된다. new 로 이름을 명명이 가능한 경우는 아래와 같이 getter 의 이름을 다른 이름으로 명시적으로 지정했을 경우이다.
// Won't work:
@property (strong, nonatomic) NSString *newTitle;
// Works:
@property (strong, nonatomic, getter=theNewTitle) NSString *newTitle;

* Reference

UIAlertView, delegate 그리고 ARC

며칠 전 직면했던, 지금 돌이켜 보면 사소한(?) 상황에 대해 정리한다. ARC 를 너무 생각없이 편하게만 – C#과 Java 처럼 – 사용하여 발생한 문제다.

핵심 상황은 다음과 같다.

UIViewController인 TestUIViewController가 있다. TestUIViewController는 알림 메시지(UIAlertView) 통보를 AlertViewManager에게 위임한다. AlertViewManager는 상황을 판단하여 필요한 경우 알림 메시지를 띄운다. 예시 코드는 다음과 같다.

// View Controller
@implementation TestUIViewController

-(void)alertTrigger{

    [[[AlertViewManager alloc] init] alert];
}
@end

// Alert 을 담당하는 별도 클래스
@interface AlertViewManager : NSObject

// View Controller에서 알림 메시지 호출을 위해 사용
-(void)alert;

@end

@implementation AlertViewManager

-(void)alert{

    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Alert Test" message:@"ARC가 delegate를 지울 수도 있어요" delegate:self cancelButtonTitle:@"어쩌라고" otherButtonTitles:@"땡큐", nil];

    [alertView show];

}

// UIAlertViewDelegate handler
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{

    switch (buttonIndex) {
       case 0:

          // do something
          break;

       case 1:

          // do something
          break;

       default:
          break;
     }
}

@end

코드를 실행하여 알림 메시지 창을 띄운 후, 알림 메시지창 버튼을 클릭하면 앱은 종료(Crash)된다. 문제 원인은 TestViewController에서 사용하는 AlertViewManager 객체 life cycle과 관련있다.

Alert View

TestViewController의 alertTrigger 메소드 안에서 생성된 AlertViewManager 객체는 TestViewController의 속성이나 인스턴스 변수로 할당이 되지 않고 바로 사용된다. 이로 인해 alertTrigger 메소드 종료시점에 AlertViewManager 객체는 참조 관계를 잃어 ARC에 의해 해제(release)된다.

문제는 AlertViewManager가 UIAlertViewDelegate로 할당되었다는 사실이다. delegate는 retain cycle을 피하기 위해 __weak 참조를 기본으로 하고 있다. ([iOS]Practical Memory Management 참조)

delegate 할당이 retain count를 증가시키지 않으므로 AlertViewManager 객체가 release되면 delegate=self 로 할당된 delegate 도 해제된다.  결국 알림 메시창의 버튼 클릭 이벤트를 처리할 delegate 객체가 없어진 상태에서 버튼이 클릭되어 앱이 종료된 것이다.

Instruments의 Zombie를 통해 정확히 확인할 수 있다.

UIAlertView Zombie

위 그림의 RefCt (Reference Count)의 변화를 보면 UIViewController의 메소드 (캡쳐 화면 메소드 이름과 예시 상황에서 설명한 메소드 이름이 상이하지만 이해에 문제 없을 것 같다)에서 AlertViewManager는 할당되고 해제된다. AlertView의 buttonClicked: 에서 Zombie 객체가 확인된다.

이 상황의 해결법 중 하나는  AlertViewManager를 TestUIViewController의 변수로 추가시켜 ViewController가 소멸될 때까지 AlertView release가 되지 않도록 방지하는 것이다. 수정되는 코드는 아래와 같다.

// View Controller
@implementation TestUIViewController{

    // private 필드로 지정. (인스턴스 생성 코드는 생략)
    AlertViewManager *alertManager;
}

-(void)alertTrigger{

    // 인스턴스 변수를 통해 alert 호출
    [alertManager alert];
}
@end

생각해보면 별 것도 아니고 실제로 자주 발생하는 문제도 아니다. 그러나 ARC의 편의를 누리려면  ARC에 대해 좀 더 명확히 알 필요가 있을 것 같다.

%d bloggers like this: