그 옛날(?) ARC가 없던 시절에 Objective-C 및 코어 파운데이션에서 객체에 대한 참조수 관리는 완전한 수동 방식에 의존하고 있다. 어떤 객체에 대한 retain(참조수를 늘리는 것) 동작은 반드시 그에 수반하는 release(참조수를 내리는 것) 동작을 필요로 했다. 그리고 이 짝이 제대로 맞지 않으면 객체는 필요한 시점에 사라지고 없거나, 반대로 메모리 누수가 발생했다. 그러던 중 자동 참조수 카운팅(ARC)이 도입되었는데, ARC 환경에서는 모든 retain/release/autorelease 콜이 컴파일러에 의해 코드에 자동으로 삽입되었다. 이는 전체적인 코드량의 감수는 물론 개발 난이도를 매우 낮춰주는 역할을 했다.
ARC가 도입된 이후 Objective-C 메소드에 의해 반환되는 모든 Objective-C 객체와 코어 파운데이션 객체의 메모리 관리는 자동으로 이루어졌다. 하지만 C 함수에 의해 리턴되는 코어 파운데이션 객체는 이러한 은혜를 받지 못했다. 따라서 여기에 속하는 객체들에 대해서는 여전히 CFRetain()
, CFRelease()
를 호출하거나, __bridge*
로 시작하는 함수를 통해 Objecitve-C 객체로 브릿징해야 했다.
코어 파운데이션의 이름 규칙
한가지 다행스러운 것 중 하나는 코어 파운데이션의 API들은 리턴되는 객체의 메모리 관리를 돕기 위해서 엄격한 이름 규칙을 정해서 따르고 있다는 것이다. 그것은 바로 Create 규칙과 Get 규칙이다. Create 규칙은 함수에 의해서 새로이 생성되는 객체에 적용된다. 이 객체들은 소유권을 해당 함수를 호출한 측으로 넘겨준다. 따라서 함수를 호출한 쪽에서 리턴된 객체를 릴리즈할 책임을 져야 한다. 여기에 해당하는 함수들은 반드시 그 이름에 Create
나 Copy
를 포함한다. 다른 한가지 규칙은 Get 규칙으로, Create/Copy가 이름에 없고 Get
등이 이름에 있는 (없을 수도 있음) 함수가 적용받는데 어느 시점에 릴리즈가 될 객체이므로 함수를 호출한 측에서 별도로 release할 필요는 없다. 다만, 프로그래머가 명시적으로 해당 객체의 라이프 사이클을 확장하기 위해서 추가로 retain 한 경우라면 나중에 꼭 release해 줄 것을 기대한다.
Swift는 오직 ARC에 전적으로 의존하여 참조수 관리를 하고 있으며, 따라서 CFRelease
를 호출할 기회조차 가질 수 없다. 그렇다면 C API에 의해 리턴되는 객체는 Swift에서 어떻게 메모리 관리를 하게될까?
먼저 API 어노테이션이다. API 자체가 명시적으로 메모리 관리 규칙에 대한 부연을 담고 있다는 그 API는 완전한 메모리 관리가 가능하여 Objective-C 나 Swift 객체와 동일하게 다룰 수 있다. 하지만 부연이 달려있지 않은 API라면 예외적인 방법을 사용해야 하는데, 이때 사용하는 타입이 바로 Unmanaged
이다.
현재까지 많은 코어 파운데이션 API들에 어노테이션이 달리게 되었지만, AddressBook과 같이 덩치가 매우 큰 프레임워크는 그렇지 못한 상황이다.(물론 이를 대체하라고 Contacts 프레임웍이 있긴 하지만) 이곳의 API들은 메모리 관리가 자동으로 되지 않는 객체들을 리턴하며, 이들은 Swift에서 Unmanaged<T>
타입으로 다뤄지게 된다.
Unmanaged
객체는 그 자체가 살아있는 범위 내에서, 그 내부에 있는 객체에 대한 강한 참조를 유지한다. 이때 내부 객체가 Create 규칙에 의해 발생했다면, 이 객체는 기본적으로 2의 참조수를 갖게 된다. (생성할 때 +1, Unmanaged
에 의해 +1) 따라서 Unmanaged
객체가 제거될 때 그 속의 객체도 해제되길 원한다면 참조수를 하나 내린 채로 사용해야 할 것이다. 반대로 Get 규칙에 의해 획득한 객체라면 올라간 참조수는 +1 뿐이므로 참조수 내림 없이 사용해야 한다. 이 때 사용되는 메소드는 다음과 같다.
takeRetainedValue()
: 이미 retain된 객체를 사용하기 위해 참조수 -1한 객체를 리턴한다. Create 규칙을 따르는 API의 리턴 객체에 사용한다.takeUnretainedValue()
: retain되지 않은 객체를 사용하기 위해 참조수를 내리지 않고 리턴한다. Get 규칙일 때 사용한다.
다음 예시를 보면 좀 더 명확할 것이다. Create/Get API를 호출하면서, 그 이름에 의해 retain되었는지 여부를 결정하고, Unmanaged 내부의 객체를 사용할 것이다. 코드가 종료되는 시점에 Unamaged 객체들이 해제되면 그 내부의 객체들도 마지막 남은 참조가 없어지면서 자동으로 파괴될 것이다.
let bestFriendID = ABRecordID( ... )
// Create 규칙에 의한 객체획득
// Unmanaged는 중간 리턴값이며, 최종타입은 아니다.
let addreaddBook: ABAddressBook = ABAddressBookCreateWithOptions(nil, nil)
.takeRetainedValue()
if let bestFriendRecord: ABRecord = ABAddressBookGetPersonWithRecordID(
addressBook, bestFriendID)?.takeUnretainedValue(),
// Get 규칙이어서 takeUnretainedValue()를 사용.
// 다시 아래는 Copy가 들어가므로 Create 규칙이다.
let name = ABRecordCopyCompositeName(bestFriendRecord)?.takeRetainedValue() as? String
{
print("\(name): BFF!")
}
take[Un]Retained() 외에 pass[Un]Retained() 메소드가 있다. 이는 Swift 에서 만들어진 T 타입 객체를 C API로 넘겨주기 위해 사용하는 것이다. 마찬가지로 *Retained, *UnRetained는 추가적인 참조수 변경을 수행하는지 안하는지를 구분한다.
passUnretained()
는 Swift 객체의 참조수를 건드리지 않고, 내부 객체 자체가 참조수 +0인 상태인 것을 가정하게 한다. (참조수가 실제로 0이라는 말은 아니다. Swift가 관리하는 상태에서는 균형이 맞기 때문에 +0이라고 한다.) 대부분의 API들이 인자로 전달받은 객체를 내부에서 해제할 일이 없기 때문에, 주로 많이 쓰일 부분이다. 코어 파운데이션 타입으로 브릿징이 가능한 타입을 가지고 C 함수를 호출할 때, 사용될 수 있다.
예를 들어 CFAarraySetValueAtIndex()
는 CFMutableArrayRef의 특정 인덱스의 값을 변경한다. 이를 Swift에서 호출하려면 Array<String>
같은 걸 이렇게 쓸 수 있을 것이다.
/* Signature in C
void CFArraySetValueAtIndex(
CFMutableArrayRef theArray,
CFIndex idx,
const void *value
);
Signature in Swift
func CFArraySetValueAtIndex(
_ theArray: CFMutableArray!,
_ idx: CFIndex,
_ value: UnsafeRawPointer!
)
*/
CFArraySetValueAtIndex(
.passUnretained(array),
i,
.passUnretained(object).toOpaque()
)
passRetained는 Swift 객체의 참조수를 +1 해서 “추가로 retain된” Unmanaged 객체를 만든다. 함수 내부에서 인자로 받은 객체의 참조수를 -1 하는 동작을 하는 API가 있다면 passRetained를 통해서 참조수를 +1 시켜주어야 나중에 Unamaged 가 해제될 때까지 내부 객체가 살아있게 될 것이다.
Unretained 객체는 toOpaque()
, fromOpaque()
를 통해서 void 포인터(UnsafeMutableRawPointer)로 상호변환이 가능하다. Objective-C 메소드 중에서 context를 받는 것들이 있는데, 여기를 통해서 Swift의 T 타입 객체를 넘겨주는 것이 가능하다. 이 때 이 변환 메소드들은 참조수에는 영향을 끼치지 않는다.
물론 UnsafeMutableRawPointer(&theObject)
를 통해서 포인터를 만들 수도 있겠지만… 사실 이는 좋은 방법이 아니다. inout 표현으로 함수/메소드 인자에 사용할 때에는 실질적인 동작은 실제 메모리 주소를 넘기는 것이 아니라, 브릿징된 포인터를 넘기기 때문이다. 그리고 이 포인터는 해당 함수의 실행 종료와 함께 파괴된다. 이렇게 이니셜라이저에 inout 인자를 넘기는 것은 흔한 실수이며, 예기치 못한 결과를 초래할 수 있다.
// 특정한 T타입 객체를 OpaquePointer를 인자로 받는 함수에 전달하고 싶다면,
// Unmanaged를 경유해서 OpaquePointer로 생성하는 전략을 취한다.
var theObject: SomeObject = ....
let pointer:UnsafeMutableRawPointer = Unmanaged.passUnretained(theObject).toOpaque()
// 다시 반대의 동작을 취하여 T타입 객체를 얻으면 된다.
let someObj = Unmanaged<SomeObject>.fromOpaque(context!).takeUnreatinedValue()