[Objective-C] 블록 객체
출처 : OS X 구조를 이해하면서 배우는 Objective-C Chap 14.
Notice : 정리자(돼지왕 왕돼지)가 remind 하고 싶은 내용이나 모르는 내용 기반으로 정리하는 것이기 때문에 구체적인 내용은 책을 사서 보시기를 권장드립니다.
14.1. 블록 객체란
* 14.1.1. C 컴파일러와 GCD
-
다른 프로그래밍 언어에서는 클러져 (closure)라고 하는 기능에 해당한다.
-
Mac OS X 10.6 및 iOS 4 부터 멀티 코어에서 스레드가 더 효율적으로 동작하도록 그랜드 센트럴 디스패치(GCD : Grand Central Dispatch)라는 구조 도입
* 14.1.2. 블록 객체 정의
-
^ 부터 인수열, 본체의 마지막 괄호가 나올 떄까지의 블록 객체의 내용을 블록 리터럴(block literal)이라고 부른다.
-
함수 포인터 선언에서 * 을 사용하는 부분에 블록 객체는 ^ 를 사용해서 정의한다.
-
int 형 인수를 하나 받아서 값을 돌려주지 않는 블록 객체를 다음과 같이 만들 수 있다.
void (^b) (int); // 변수 이름은 b 이다.
-
블록 객체를 변수 b 에 대입하는 코드는 다음과 같다.
void (^b) (int) = ^(int i){
// .. do something
};
* 14.1.3. 블록 객체와 형식 선언
-
함수 포인터와 마찬가지로 형식을 간결하게 작성하기 위해 typedef 선언을 할 수 있다.
typedef int (^myBlockType) (int);
-
블록 리터럴은 함수와 달리 반환값 형식을 명시하지 않는다.
컴파일러는 반환값형이 무엇인지 추론하는데, 의도한 대로 동작시키려면 return 이 반환하는 값의 형식을 밝혀두는 편이 좋다.
* 14.1.4. 블록 객체에 포함된 변수 동작
-
자동 변수란 함수 블록 내부의 지역 변수로, static 수식자가 없는 걸 말한다.
-
블록 객체는 그 블록 리터럴이 작성된 위치에서 자동 변숫값을 저장하고 있는다. ( 그런 동작을 한다. )
-
블록 객체는 실행 가능한 코드와 작성된 시점에서 접근 가능한 변수를 통째로 “감싸버려서” 나중에 평가할 수 있게 포장한다.
클로저라는 이름도 변수 등의 환경을 둘러싼다는 것에서 온 이름이다.
클로저가 자신의 외부 변수를 참조해서 가져 오는 걸 캡쳐(Capture)라고 부른다.
-
1. 블록 리터럴 본체에는 블록 리터럴 자체의 지역 변수와 매개변수 외에도 그 블록 리터럴이 작성된 위치에서 참조 가능한 변수를 포함할 수 있다.
그런 변수에는 외부 변수와 그 블록 리터럴을 포함한 블록 내부에서 참조 가능한 지역 변수가 있다.
2. 외부 변수 및 정적 변수(static 변수)는 블록 객체 내부에서도 변수 자체에 직접 접근할 수 있다.
블록 객체 내부에서 변숫값을 변경할 수도 있다.
3. 블록 리터럴을 포함한 블록 내부에 참조 가능한 지역 변수 중에서 자동 변수(스택 내의 변수)는 블록 객체가 작성된 시점의 값이 저장되어 참조된다.
(ㄱ) 따라서 원래 변수값이 변화되더라도 블록 객체의 평가에는 반영되지 않는다.
(ㄴ) 변수값은 참조 가능하지만 변경할 수가 없다.
(ㄷ) 자동 변수가 배열일 때 컴파일 에러가 발생한다. // MRC 일 때만
-
블록 객체에는 작성된 위치에 접근 가능한 변수를 포함할 수 있다는 것과 자동 변수는 복사된 값을 읽어들이기만 가능하다.
자동 변수는 실행할 때는 형 수식자 const 가 붙은 변수처럼 다룬다.
* 14.1.5. 정렬 함수와 블록 객체
-
함수 포인터를 사용할 때는 목적에 따라 다른 함수를 만들거나 함수로 부가 정보를 넘기기 위한 추가 인수나 외부 변수를 사용해야 하므로 독립성이 떨어지고 이해하기 어려운 코드가 되곤 한다.
블록 객체를 활용하면 이런 함수나 메서드를 알기 쉽고 유연하게 고쳐 쓸 수 있다.
14.2. 블록 객체 구조
* 14.2.1. 블록 객체의 실체와 수명
-
블록 리터럴이 컴파일되면 필요한 정보를 저장한 메모리 영역(실체는 구조체)와 함수가 만들어진다.
변수에 대입하거나 실인수로 함수에 넘어오는 건 실제로는 이 메모리 영역을 가리키는 포인터이다.
-
함수 외부에 작성된 블록 리터럴이 컴파일되면 블록 객체의 메모리 영역은 외부 변수와 마찬가지로 정적인 데이터 영역이 된다.
-
함수 블록 내부에 작성된 블록 리터럴은 다르게 동작한다.
그 블록 리터럴을 포함한 함수가 실행되면 자동 변수처럼 블록 객체의 메모리 영역은 스택에 확보된다.
따라서 이런 블록 객체의 수명은 자동 변수와 같아 함수가 실행되는 동안에만 존재한다.
-
1. 블록 리터럴을 함수 외부에 기술하면 블록 객체의 메모리 영역은 동적인 데이터 영역에 하나만 만들어진다.
이 영역은 프로그램이 실행되는 동안 계속 존재한다.
2. 블록 리터럴을 함수 내부에 기술하면 블록 객체의 메모리 영역은 자동 변수와 마찬가지로 그것을 포함한 함수가 실행될 동안 스택에 올라간다.
이 메모리 영역은 그 함수가 실행되는 동안에만 존재한다.
* 14.2.2. 피해야 할 작성 패턴
-
스택 위에 생성된 블록 객체는 그 수명이 끝나면 사용해서는 안 된다.
-
for( int i=0; i < 10; i++ ){
blocks[i] = ^{ return i; };
}
위의 구문에서 MRC 에서는 배열 blocks[i] 번째 요소에 값으로 i 를 돌려주는 블록 객체가 각각 저장되어 있다고 생각할지 모른다.
하지만 블록 객체 실체의 메모리 영역은 하나뿐이므로 for 반복문 안에서 배열에 대입된 포인터는 모두 같다.
결국 모두 9 를 돌려주는 동일한 블록 객체가 매핑된다.
// 해당 구문이 들어있는 block 이 끝나면 이마저도 garbage 가 된다.
MRC 에서 각각의 요소에 다른 블록 객체를 대입하고 싶을 경우에는 다음에 설명하는 복사를 사용해야 한다.
하지만 ARC 를 사용할 때는 그럴 필요가 없다.
strong 참조시 자동으로 copy 를 해서 heap 영역으로 올린다.
* 14.2.3. 블록 객체의 복사
-
함수 내부에 작성된 블록 객체는 자동 변수와 같이 그 함수가 실행되는 사이에만 있다.
-
블록 객체의 복제를 힙 영역에 새로 작성하는 함수가 있다.
이 기능을 사용하면 함수 내부에 작성된 블록 객체도 스택과는 관계없이 계속 사용할 수 있다.
그리고 불필요해진 블록 객체를 해제하는 기능도 있다.
Block_copy(block)
인수가 스택 위의 블록 객체일 때 그 복제를 힙 영역에 만들어 돌려준다.
그렇지 않다면 (인수가 정적인 데이터 영역 또는 힙 영역 블록 객체) 복제는 만들어지지 않고 인수가 반환값이 되며, 인수의 블록 객체 참조 카운터를 늘린다.
Block_release(block)
인수의 블록 객체 참조 카운터를 줄이며 0이 되면 블록 객체의 메모리 영역을 해제한다.
위의 기능을 사용하려면 소스 파일에 Block.h 헤더 파일을 읽어들여야 한다.
이 파일은 /usr/include 안에 있다.
* 14.2.4. 특별한 변수 지정 __block
-
__block 수식자를 지정하면 const 처럼 사용하지 않을 수 있다.
1. 함수 내부에 작성된 블록 리터럴에서 참조된 __block 변수는 그 블록 객체에서 읽고 쓸 수 있는 변수로 사용할 수 있다.
같은 변수 범위의 여러 블록 객체에서 참조될 때면 그 사이에 값이 공유된다.
2. __block 변수는 정적 변수가 아니라 블록 구문이 실행될 떄마다 영역이 확보된다. 즉, 같은 블록 구문(변수 스코포) 안의 블록 객체와 그 사이에 공유되는 block 변수는 실행할 때마다 동적으로 생성된다.
3. __block 변수를 참조하는 블록 객체의 복제를 만들면 새롭게 생성된 블록 객체에서도 __block 변수의 값을 공유할 수 있다.
4. 동일한 __block 변수를 참조하는 블록 객체가 여러 개 있을 때 그 중 하나라도 계속 존재하고 있다면 그 사이에 __block 변수도 존재한다.
__block 변수를 참조하는 블록 객체가 없어지면 __block 변수도 소멸한다.
-
블록 객체의 복제에 의해 __block 변수의 메모리 위치가 변할 수도 있다.
따라서 포인터를 사용해서 __block 변수를 참조하는 코드를 작성하면 안 된다.
기대와는 다른 결과가 나올 수 있다.
14.3. Objective-C 와 블록 객체
* 14.3.1. 메서드 정의와 블록 객체
-
변수 block 에 블록 객체를 대입할 떄는 다음처럼 작성한다.
BOOL (^block)(int, int) = ^(int index, int length){ … ; };
-
인수로 받는 경우에는 다음과 같이 작성한다.
- (void) setBlock: (BOOL (^)(int, int)) block;
-
형식 부분에 매개변수를 작성할 수도 있다.
작성된 본문이 길어질 수도 잇지만 각각의 인수 역할을 쉽게 알 수 있는 장점이 있다.
-
메서드 반환값으로 블록 객체를 넘길 때도 인수와 같은 형태이며, parameter 에 매개변수를 작성할 수 있다.
-(BOOL (^)(int index, int length)) currentBlock;
* 14.3.2. Objective-C 객체로서의 블록 객체
-
블록 객체는 Objective-C 객체로 동작한다.
-
블록 객체는 NSObject 를 상속한 클래스의 인스턴스로 동작하며 보통 인스턴스 객체에 대한 조작이 완전하지는 않지만 적용할 수는 있다.
id 형의 변수에 대입할 수도 있다.
-
copy 메서드를 사용해서 인스턴스 객체의 복사본을 만들 수 있다.
이것은 Block_copy 의 적용에 해당한다.
수동 카운터 관리 방식을 사용할 경우 retain, release 및 autorelease 의 각 메서드를 사용할 수 있으며, 배열 등의 컬렉션에 포함할 수도 있다.
retainCount 메서드가 반환하는 값은 참조 카운터의 올바른 값이 아니다.
* 14.3.3. ARC 와 블록 객체
-
ARC 를 사용할 때와 사용하지 않을 때 블록 객체의 취급이 달라지므로 주의해야 한다.
-
ARC 에서는 블록 객체의 유지가 필요하다면 컴파일러가 자동으로 copy 작업 코드를 삽입한다.
-
구체적으로는 강한 참조 변수에 대입될 때와 return 의 결과로 반환될 때이다.
이런 경우에는 프로그램에서 명시적으로 블록 객체를 복사해 둘 필요가 없다.
그러나 메서드의 인수로 전달된 블록 객체는 자동으로 copy 가 실행되지 않는다.
따라서 선언 프로퍼티 값으로 블록 객체를 유지할 경우에는 프로퍼티 옵션으로 copy 를 지정하는 것이 보통이다.
-
Block_copy 와 Block_release 는 사용해선 안 된다.
(void *) 형의 포인터를 인수로 받도록 정의되어 있으므로 ARC 가 소유권 관련 추론을 할 수 없다.
-
__block 변수의 동작이 달라진다.
ARC 를 사용하지 않으면 __block 변수는 단순히 값이 대입될 뿐이므로 댕글링 포인터가 될 가능성이 있다.
ARC 에서는 __block 변수는 __strong 수식자가 붙은 강한 참조 변수로 취급되므로 댕글링 포인터가 되지 않는다.
* 14.3.4. 블록 객체에 포함된 변수의 동작
-
1. 블록 리터럴 안에 인스턴스 변수가 있으면 인스턴스 변수에 직접 접근할 수 있으므로 값을 변경할 수 있다.
2. 블록 리터럴 안에 인스턴스 변수가 있을 때, 그 블록 객체의 복사본을 만들면 인스턴스 변수가 아니라 self 에 retain 이 보내져 참조 카운터가 늘어난다. 인스턴스 변수의 형식은 객체가 아니라도 가능하다.
3. 블록 리터럴에 인스턴스 변수가 아닌 객체가 있을 때 스택에 있는 블록 객체의 복사본을 만들면 포함된 객체에 retain 이 보내져 참조 카운터가 늘어난다.
4. 이미 복사되어 힙 영역에 있는 블록 객체에 copy 메서드를 보내도 블록 객체 자체의 참조 카운터가 늘어날 뿐이고 포함된 객체 참조 카운터는 변하지 않는다.
5. 복사된 블록 객체가 해제될 때 포함된 객체에도 release 가 보내진다.
* 14.3.5. 컬렉션 클래스에 추가된 메서드
-
정렬
-(NSArray*) sortedArrayUsingComparator:(NSComparator) cmptr;
// NSComparator 는 블록 객체 형식.
-
블록 객체가 지정하는 조건에 해당하는 첫 요소의 인덱스를 돌려주는 메서드.
조건에 해당하는 요소가 없으면 NSNotFound 라는 값을 돌려준다.
-(NSUInteger) indexOfObjectPassingTest: (BOOL (^)(id obj, NSUInteger idx, BOOL *stop)) predicate
// 블락 안에서는 조건에 일치하면 YES, 그렇지 않으면 NO 를 return 한다.
-
배열의 처음부터 순서대로 요소 객체와 그 인덱스를 블록 객체에 넘긴다.
-(void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block
-
사전 클래스인 NSDictionary 에 추가된 녀석은 사전에 저장된 엔트리를 차례로 꺼내서 그 키와 요소 객체를 블록 객체에 넘긴다.
-(void)enumerateKeysAndObjectsUsingBlock:(void (^) (id key, id obj, BOOL *stop))block
* 14.3.6. 시트 표시에 블록 객체 사용 예
* 14.3.7. ARC 로 블록 객체 사용 시 주의사항
-
ARC 를 사용해서 소프트웨어를 개발할 떄 주의해야 할 점은 유지 순환이 발생하지 않도록 하는 것이다.
블록 객체를 사용할 때 관련 객체가 자동으로 유지될 때가 있는데, 이 때 의도하지 않은 유지 순환이 발생할 수 있다.
-
블록 리터럴에 인스턴스 변수가 포함되면 인스턴스 변수의 객체가 아니라 self 의 참조 카운터가 늘어난다.
-
clang 컴파일러는 유지 순환이 발생하는 전형적인 상황은 어느 정도 확인해주지만 모든 경우를 검출해주지는 못 한다.
-
참조해야 하는 인스턴스 변수를 __weak 으로 선언해서 사용하면 문제가 없다.
__week MyClass* weakMyClass = _myClass;
handler.block = ^{
// do something with weakMyClass
};
-
self 자체를 약한 참조의 변수로 캡처해 블록 객체 안에서 강한 참조를 사용해서 유지하는 방법도 있다.
__weak MyClass* weakSelf = self;
handler.block = ^{
MyClass* strongSelf = weakSelf; // 처리 도중에 해지되지 않음을 보장
if(strongSelf){
// do something with strongSelf
}
};
다음 글 : [Objective-C] 메시지 송신 패턴
'프로그래밍 놀이터 > iOS' 카테고리의 다른 글
[Objective-C] 어플리케이션 구조 (0) | 2018.01.08 |
---|---|
[Objective-C] 메시지 송신 패턴 (0) | 2018.01.07 |
[Objective-C] 객체 복사와 저장 (0) | 2018.01.05 |
[Objective-C] 프로토콜 (0) | 2018.01.04 |
[Objective-C] 추상 클래스와 클래스 클러스터 (0) | 2018.01.03 |
댓글