스위프트 중간 언어(SIL)
: 스위프트 컴파일러, swiftc는 스위프트 코드를 SIL(Swift Intermediate Language)로 만들고 LLVM IR로 변환한다. 또 IR 최적화기를 거쳐서 최종적으로 타깃머신에 맞는 기계코드를 생성한다.
swift 코드 -> SIL -> LLVM IR -> 기계 코드
Objective-C 호환성
: Objective-C에서 스위프트 객체를 사용하려면 NSObject를 최상위 클래스로 지정하고 상속받아 만들어야 한다. 그렇지 않으면 스위프트 전용 객체로 동작하기 때문에 Objective-C 에서는 사용할 수 없다.
NSObject 객체를 상속받은 경우 스위프트 객체는 내부에서 Objective-C 객체로 자동 변환되고, Objective-C 런타임에서 동작한다.
Objective-C 객체와 메모리 구조
: 모든 운영체제는 동일한 구조로 메모리를 관리한다.
1- Stack: 함수, 함수 내에 변수에 대한 메모리 저장
2- Heap: 클래스, 클래스 인스턴스에 대한 메모리 동적 할당(클래스에 대한 메모리를 프로그램 실행시 로드된다.), Stack과 Heap 사이에 빈공간이 있음, Stack 윗 공간은 커널 영역이기 때문에 커널 영역을 보호하기 위해서 스택은 위에서 부터 메모리를 사용한고 Heap은 밑에서 부터 사용하기 때문에 두 메모리 공간을 많이 사용하면 할수록 빈 메모리 공간이 줄어든다.
3- BSS(심벌): 아직 초기화 되지않은 전역변수에 대한 메모리 저장
4- Data: 초기화가 완료된 전역변수에 대한 메모리 저장
5- Text: 프로그램 실행코드(실제 함수 구현, heap에 클래스에 대한 메모리가 저장되는데 여기에 클래스 메서드, 인스턴스 메서드의 Selector 변수들이 저장되는 것이고, 이 Selector에 대한 실제 구현 함수 코드는 Text영역에 할당된다.)
객체 예외성
: 모든 코코아 객체 인스턴스가 힙 영역에 생성되는 것은 아니다. 특이하게도 힙이 아니라 텍스트 영역과 데이터 영역에 생기는 경우가 있다.
NSString 클래스는 NSObject에서 상속받는 코코아 클래스 중에서 유일하게 전역변수로 선언할 수 있다. NSString을 메서드 내에서 사용하더라도 NSString 지역변수는 Stack에 저장되고 NSString 클래스와 클래스 인스턴스에 대한 메모리는 Data영역에 저장되고 해당 문자 리터럴은 Text 영역에 저장된다. 즉 스택에 여러개의 NSString 변수가 생성되더라도 문자 리터럴이 같으면 중복으로 저장해서 사용할 필요가 없기 때문에 하나의 클래스, 클래스 인스턴스 메모리만 Data영역에 저장되고 문자 리터럴도 Text영역에 하나만 저장된다.
* Swift에서 String 객체는 네이티브 문자열 객체를 만들 수도있고, NSString을 연결해서 쓸수도 있다. 다만, 네이티브 문장열로 만들 경우에는 Objective-C의 NSString처럼 클래스, 클래스 인스턴스에 대한 메모리 할당 없이 Text영역에 있는 문자 리터럴을 포인터로 바로 연결해서 사용하기 때문에 NSString 보다는 가벼운 형태라고 할 수 있다.
hash 메서드
: isEqual: 메서드를 오버라이드한 경우라면 반드시 -hash 메서드도 다시 구현해야 한다. 왜냐하면 NSDictionary 같은 컬렉션 객체는 -isEqual: 메서드 대신 -hash 메서드를 사용해서 해시 값을 비교하기 때문이다. NSObject에 기본적으로 구현된 -hash 메서드는 self 메모리 포인터 값을 NSUInteger 타입 숫자로 바꿔줄 뿐이다. 따라서 -hash 메서드를 구현할 때도 -isEqual 메서드를 구현할 때와 마찬가지로 모든 속성에 대해 -hash 메서드를 호출해서 해시 값을 구하면 된다.
인스턴스, 클래스, 메타 클래스
: 인스턴스, 클래스, 메타 클래스 모두 isa 포인터를 가지고 있다. 인스턴스의 isa 포인터는 클래스를 가리키고 클래스의 isa 포인터는 메타 클래스를 가리키고 있다. 마지막으로 메타 클래스의 isa 포인터는 슈퍼메타 클래스가 있으면 그것을 가리키고 자신이 root 메타 클래스이면 자기 자신을 가리킨다. 인스턴스는 멤버 변수에 대한 정보만 가지고 있고, 클래스는 인스턴스 메서드 목록과 코드를 가지고 있으며, 메타 클래스는 클래스 메서드 목록과 코드를 가지고 있다.
ARC 규칙
1) C 구조체 내부에 객체 포인터를 넣지마라
: C언어에서 사용하는 struct나 union 내부에 Objective-C 객체 포인터를 넣으면 ARC에서 메모리 관리를 할 수 없기 때문에 컴파일 에러가 발생한다.
ARC가 관리하지 않는다는 것을 강제로 명시하려면 '__unsafe_unretained' 수식어를 사용해야 한다. 다만, 해당 수식어를 사용하면 dangling 포인터에 접근할 위험이 있다.
2) id와 void* 타입을 명시적으로 타입 변환하라
타입 연결(Objective-C 객체 <-> Core Foundation 객체)
: Core Foundation 객체란, C로 만들어진 구조체를 말한다.
대부분의 Core Foundation 구조체는 대응되는 Objective-C 객체와 toll-free bridged 된다. 즉, 무비용으로 타입 변환이 되는것이다. (CFRunLoop <-> NSRunLoop, CFBundle <-> NSBundle 등은 제외)
Core Foundation 구조체는 ARC가 메모리 관리를 하지 못하기 때문에 프로그래머가 직접 메모리 관리를 해줘야 한다. 때문에, Objective-C 객체로 타입 변환할 때에도 적절한 메모리 관리 코드가 삽입되야 한다.
이제 부터 여러가지 타입변환 방법에 대해 알아보겠다.
1) __bridge 방식
: 객체의 소유권을 넘기지 않고 타입 연결만 하는 경우에 사용한다. 다만, 허상 포인터가 생길 수 있기 때문에 매우 위험하다.
id sObject = [[NSObject alloc] init];
void *pObject = (__bridge void*) aObject;
id sOtherObject = (__bridge id) pObject;
2) __bridge_retained 또는 CFBridgingRetain 방식
: Objective-C 객체를 Core Foundation 포인터로 연결하면서 소유권도 주는 경우에 사용한다. 참조가 끝나면 CFRelease()와 같은 함수를 이용해서 소유권을 반환해야 한다.
id sObject = [[NSObject alloc] init];
void *pObject = (__bridge_retained void*) sObject;
// (__bridge_retained void*) 대신 CFBridgingRetain() 매크로를 사용해도 된다.
위 코드는 컴파일러에 의해서 다음과 같이 변환된다.
id sObject = [[NSObject alloc] init];
void *pObject = aObject;
[(id)pObject retain];
3) __bridging_transfer 또는 CGBridgingRelease 방식
: __bridge_retained와는 반대로 Core Foundation 참조 포인터를 Objective-C 객체로 연결하면서 소유권을 넘기는 경우에 사용한다.
id sObject = (__bridge_transfer id)pObject;
// (__bridge_transfer id) 대신 CFBridgingRelease() 매크로를 사용해도 된다.
위 코드는 컴파일러에 의해서 다음과 같이 변환된다.
is sObject = (id)pObject;
[sObject retain]:
[(id)aObject release];
Collection 클래스 성능
- Collection 클래스들은 직접 for문을 돌면서 objectAtIndex:, objectForKey: 등을 통해 iterating 하는것 보다 'for in' (fast enumeration), enumerateKeysAndObjectsUsingBlock: 등을 사용해서 iterating을 할때 더욱 성능이 좋다. 그 이유는 objectAtIndex, objectForKey 같은 메서드들은 해당 index, key에 대해 매칭되는 값을 찾아야 되고 해당 index, key에 대한 int, string 값을 생성하는 시간도 필요하기 때문이다.
'for in' 이나 enumerateKeysAndObjectsUsingBlock: 같은 경우는 이러한 것들을 고려하지 않고 단순히 iterating에만 신경 쓰면 된다.
NSArray vs NSSet
- Construct
: NSArray가 빠르다. 중복체크를 할필요가 없기 때문
- iterating
: NSArray가 빠르다. 구성 방법이 NSSet보다 더 간단할 것이기 때문이다. 예를들어, NSArray는 단순한 Linked List로 구성되어 있다면 NSSet은 객체의 hash값을 테이블 형태로 저장할 것으로 보인다. (NSDictionary와 비슷한 형태로.. 왜냐하면 containsObject:를 써서 NSSet안에 있는 객체를 찾을때 객체의 hash값으로 O(1) 복잡도로 바로 찾도록 구현되어 있을것이기 때문)
- search
: NSSet이 빠르다. NSSet은 NSDictionary와 비슷하게 객체의 hash 값으로 찾기 때문에 0(1) 복잡도면 되지만, NSArray는 전체를 다 검색해야 되기 때문이다.
NSInvocation
: Objective-C 메세지를 객체로 추상화 하는 클래스이다. 메시지 이름(selector), 시그니처(methodSignature), 메시지 수진자(target), 모든 인자 값(arguments)들을 포함하고, NSInvocation을 실행한 이후 리턴 값(return value) 까지도 담을 수 있다.
: 일반적으로 타깃과 액션을 지정해서 메시지를 보내는것보다 매우 번거로운 작업이다. 그러나 NSUndoManager 클래스와 비슷한 동작을 구현하는 것을 매우 쉽게 만들어 준다.
- 주의사항
1) 반드시 invocationWithMethodSignature 지정 생성자를 이용해서 생성하고 초기화를 해야 한다. (alloc, init 으로 하면 안됨)
2) -setArgument:atIndex: 메서드로 인자 값을 설정할 때 두 번째부터 인자 값을 지정해야 한다. 0번째와 1번째는 self, _cmd 이다.
3) 넘기는 인자 값에 객체 참조가 있을 경우 효율성을 위해서 retain 하지 않는다. 따라서 인자 값으로 넘어가는 객체에 대한 소유권을 invoke시점까지 유지하고 싶으면 -retainArguments 메시지를 보내야 한다.
메시지 포워딩
: 특정 객체에 구현되지 않은 메시지를 보내면 셀렉터와 일치하는 메서드가 없어서 오류가 난다. 이렇게 타깃 객체에 셀렉터와 일치하는 메서드가 없어도 에러를 발생시키지 않고 해결할 수 있는 방법은 다음과 같다
1) 동적 메서드 추가하기
: 우선 객체는 일치하는 메서드를 찾지 못하면 +resolveInstanceMethod: 메서드를 호출한다. 이 방법은 해당 메서드에 대한 동작을 하는 동적 메서드를 미리 구현해놓고, class_addmethod() 같은 런타임 API로 메서드를 추가하고 YES를 리턴하면 해당 메서드를 다시 호출한다.
2) 곧바로 포워딩 하기
: 다른 객체에게로 메시지를 포워딩 하는 것이다.
-forwardingTargetForSelector: 메시지를 보내서 대신 수신할 객체가 있으면 리턴해주고 없으면 nil을 반환한다.
이 메서드를 상위 클래스에 구현해놓고, 해당 메시지를 처리할 수 있는 내부 객체를 반환하는 형태로 구현하기도 한다.
3) NSInvocation 사용해서 invoke
: 이 방법은 앞에도 설명했기 때문에 생략한다.
* 코어 파운데이션 객체들은 오픈소스로 애플 개발자 사이드 http://www.opensource.apple.com/source/CF에 공개되어 있다.
'IOS > 공통' 카테고리의 다른 글
Event Handling Guide - Responders (0) | 2017.05.03 |
---|---|
Event Handling Guide - Gesture (0) | 2017.04.07 |
Cocoa Pods (0) | 2017.02.19 |
About Bundle (0) | 2017.02.14 |
iOS Architecture (0) | 2016.12.18 |