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

[Effective Objective-C] #37 블록을 이해하라

by 돼지왕 왕돼지 2017. 10. 1.
반응형

 [Effective Objective-C] #37 블록을 이해하라


출처 : Effective Objective-C

>, Block, block copy, block instance variable, block retain cycle, block runtime, block stack, block syntax, block 리테인 순환, block 인스턴스 변수, C, c 레벨, C++, Capture, caret, CLASS, COPY, decriptor 변수, descriptor, dispatch queue, dispose, GCD, grand central dispatch, if 문 block, implicit self block, Invoke, invoke 변수, IOS, ISA, lexical closure, LISP, Objective-C, OSX, self 변수, thread-safe single-code execution, ui 스레드, [Effective Objective-C] #37 블록을 이해하라, __block, 강제 종료, 구조체 포인터, 디스패치 큐, 렉시컬 클로저, 리소스, 리스프, 멀티스레딩, 백그라운드 스레드, 변수 할당, 병렬처리, 불투명 void 포인터, 블록, 블록 객체, 블록 런타임, 블록 스택, 블록 싱글턴, 블록 요점, 블록 정의, 블록과 gcd, 생성, 스레드 안전 단일 코드 실행, 스케쥴, 심벌, 싱글턴, 암묵적 리테인, 인라인, 재사용, 전역 메모리, 전역 블록, 전역 블록 복사, 제거, 큐, 클로저, 포인터 변수, 할당 해제, 함수 내 함수 선언, 함수 포인터, 핵심 기술, 효과적인 싱글턴, 힙 복사


-

맥 OS X 내에서 UI 스레드가 멈추면 그 무시무시한 돌고 있는 비치볼을 보게 될 것이다.

iOS 에서는 앱이 너무 오랫동안 멈추어 있으면 강제로 종료될 것이다.



-

멀티스레딩의 핵심 기술은 블록과 GCD(Grand Central Dispatch) 다.

블록은 C, C++. 오브젝티브-C 에 렉시컬 클로저(lexical closure)를 제공하는데 이는 매우 유용하다.

( 클로저는 함수 내에 함수를 선언할 수 있게 하는 기능이다. 리스프 계열 언어에서 많이 쓰이며, 내부에 선언된 함수는 외부 함수의 파라미터들을 참조할 수 있다. )

블록은 코드를 전달하는 기법을 제공한다.



-

GCD는 스레딩을 이른바 디스패치 큐(dispatch queue)라는 추상화로 제공한다.

블록은 이 큐에 삽입될 수 있다.

그리고 GCD 가 모든 스케줄을 다룬다.

GCD는 각 큐를 처리하기 위해 백그라운드 스레드를 시스템 리소스 사용량에 기반을 두고 새로 생성, 재사용, 제거한다.

GCD는 스레드 안전한 단일 코드 실행(thread-safe single-code execution)과 시스템 리소스 사용량에 기반을 둔 병렬 처리 같은 일반적인 프로그래밍 작업을 처리하는 데 사용하기 쉬운 솔루션을 제공한다.



-

블록과 GCD 모두 최신 오브젝티브-C 프로그래밍의 중요한 기둥들이다.



-

블록은 클로저를 제공한다.



-

블록이 올바르게 동작하는 데 필요한 런타임 컴포넌트는 맥 OS X 10.4 이후 버전과 iOS 4.0 이후의 모든 버전이다.

이 언어 기능은 기술적으로 C 레벨 기능이다.

그렇기 때문에 지원되는 컴파일러로 컴파일한 C, C++, 오브젝티브-C, 오브젝티브-C++, 코드에서 사용할 수 있다.

그리고 제공되는 블록 런타임(block runtime)으로 실행할 수 있다.




블록의 기본


-

블록은 함수와 비슷하지만 다른 함수에 인라인으로 정의된다.

그리고 정의된 곳에서 블록의 범위를 공유한다.

블록을 표시하는 심벌은 ^(caret)이다.

이어서 블록의 구현을 포함하는 범위 블록이 따라온다.

^{

     // 블록을 여기에 구현한다.

}



-

블록은 변수에 할당될 수 있다.

그러면 다른 변수들처럼 사용된다.

블록 타입을 위한 문법은 함수 포인터와 비슷하다.



-

파라미터가 없고 아무것도 반환하지 않는 블록은 다음과 같다.

void (^someBlock)() = ^{

     // 블록 구현

}


syntax 가 다음과 같다.

return_type (^block_name)(parameter) 



-

int 를 반환하고 두 개의 int 파라미터를 받는 블록을 정의하는 문법은 다음과 같다.

int (^addBlock) (int a, int b) = ^(int a, int b){

     return a + b;

};


이는 다음과 같이 사용할 수 있다.

int add = addBlock(2, 5);



-

블록이 제공하는 강력한 기능은 그것들이 선언된 곳의 범위를 잡을 수 있는 것이다. (capture)



-

기본적으로 블록이 잡은 어떠한 변수(즉 블록 외부에서 정의된 변수)도 변경할 수 없다.

그러나 변수에 __block 수식어를 붙이면 변경할 수 있게 된다.

__block NSInteger count = 0;



-

블록이 객체 타입의 변수를 잡았을 때 블록은 암묵적으로 그 객체를 리테인한다.

블록 자체가 릴리스되면 블록이 잡고 있는 객체도 릴리스될 것이다.

반드시 이해해야 하는 블록에 관한 핵심이다.


블록 자체를 객체로 여길 수 있다.

사실 블록은 다른 오브젝티브-C 객체가 하는 것처럼 수많은 선택자에 응답한다.

꼭 알아야 할 것은 블록이 다른 객체처럼 참조 세기를 한다는 것이다.

블록의 마지막 참조가 제거되면 블록은 할당 해제된다.

그러면 블록이 잡은 모든 객체는 릴리스된다.



-

블록이 오브젝티브-C 클래스의 인스턴스 메서드에 정의될 때 블록은 self 변수를 클래스의 다른 인스턴스 변수처럼 사용할 수 있다.

인스턴스 변수는 항상 쓰기가 가능하다.

그리고 명시적으로 __block 으로 선언할 필요가 없다.

그러나 인스턴스 변수를 블록이 읽기나 쓰기로 잡은 경우 self 변수 또한 암묵적으로 잡힌다.


블록에 의해 잡힌 self 는 쉽게 잊힐 수 있다.

명시적으로 코드에서 사용되지 않기 때문이다.

그러나 인스턴스 변수에 접근하는 것은 다음과 같이 동일하다.

self->_anInstanceVariable

self.anInstanceVariable 



-

self 가 객체이기 때문에 블록에 의해 잡혔을 때 리테인된다는 사실을 꼭 기억해야 한다.

이 상황에서 블록이 self 로 참조되는 동일한 객체에 의해 리테인되면 리테인 순환이 발생한다.






블록의 요점


-

블록은 객체다.

블록이 정의된 메모리 영역의 첫 번째 변수가 Class 객체에 대한 isa 포인터이다.

블록이 사용하는 메모리의 나머지는 블록이 올바르게 동작하기 위해 필요한 다양한 정보를 저장하는 데 사용한다.



-

메모리 배치에서 꼭 기억해야 할 것은 invoke 변수다.

이는 블록의 구현이 있는 곳을 가리키는 함수 포인터다.


블록이 단순한 함수 포인터를 대신하는 것을 기억하자.

이 함수 포인터는 불투명 void 포인터를 사용해 전달된다.

블록은 표준 C 언어 기능을 사용해 했던 것을 간결하고 사용하기 쉬운 인터페이스로 감싼다.



-

descriptor 변수는 블록이 가진 구조체에 대한 포인터다.

이 구조체는 블록 객체의 전체 크기와 copy, dispose 헬퍼에 대한 함수 포인터들을 선언한다.

이러한 헬퍼들은 블록이 복사되고 제거될 때 동작한다.

예를 들어 각각 잡은 객체를 리테인하고 릴리스할 때 사용한다.



-

마지막으로 블록은 잡은 모든 객체의 복사본을 포함한다.

이러한 복사본은 descriptor 변수 뒤에 저장된다.

그리고 모든 잡은 변수를 저장하는 데 필요한 공간만큼 차지한다.

이는 객체 자체를 복사하는 것을 의미하지 않음을 주목하라.

대신 오직 포인터 변수만 복사한다.

블록이 실행될 때 잡힌 변수는 이 메모리 영역으로부터 읽힌다.




전역, 스택, 힙 블록


-

블록이 정의되었을 때 그것들이 점유한 메모리 영역은 스택에 할당된다.

이는 블록이 오직 정의된 범위 내에서만 유효하다는 것을 의미한다.


예를 들어 다음 코드는 위험한 코드다.

void (^block)();

if ( /* condition */ ){

     block = ^{

          NSLog(@“Block A”);

     };

} else {

     block = ^{

          NSLog(@“Block B”);

     };

}

block();


if 문과 else 문에 정의된 이 두 블록은 스택 메모리에 할당된다.

각 블록이 스택 메모리에 할당된 이후에 컴파일러는 범위 끝에서 블록이 점유했던 메모리를 덮어쓸 수 있다.

그래서 각 블록은 각 if  문 부분 내에서만 유효하다.

이 코드는 에러 없이 컴파일되지만 실행 시간에 함수가 정확하게 동작할 수도 있고 아닐 수도 있다.



-

이 문제를 해결하기 위해 블록에 copy 메시지를 보내 블록을 복사할 수 있다.

void (^block)();

if ( /*condition*/ ){

     block = [^{

          NSLog(@“Block A”);

     } copy];

} else{

     block = [^{

          NSLog(@“Block B”);

     } copy];

}

block();


이렇게 하여 블록을 스택에서 힙으로 복사할 수 있다.

복사하고 난 후에는 블록은 블록이 정의된 범위 밖에서도 사용될 수 있게 된다.

또한 힙으로 복사되고 나면 블록은 참조 세기를 하는 객체가 된다.

이후에 일어나는 블록의 모든 복사는 실제 복사를 하지 않고 간단히 블록 참조 수만 증가시킨다.

힙 블록이 더는 참조되지 않으면 자동으로 릴리스될 필요가 있다.

ARC 를 사용하거나 명시적으로 release 를 호출하는 것 같이 수동 참조 세기를 사용한다면 참조 수가 0으로 떨어지면 힙 블록은 다른 객체와 마찬가지로 할당 해제된다.

스택 블록은 명시적으로 릴리스할 필요가 없다.

스택 메모리의 공간은 자동으로 회복되기 때문이다.



-

전역 블록은 스택, 힙 블록과 마찬가지로 또 다른 범주다.

전역 블록은 블록을 사용할 때마다 스택에 생성하는 대신 전역 메모리에 정의된다.

또한 전역 블록을 복사하면 아무런 동작도 하지 않는다.

그리고 전역 블록은 절대 할당 해제되지 않는다.

이러한 블록은 매우 효과적인 싱글턴이다.



기억할 점


-

블록은 C, C++ 오브젝티브-C 를 위한 렉시컬 클로저다.



-

블록은 선택적으로 파라미터를 받거나 값을 반환할 수 있다.



-

블록은 스택, 힙, 전역으로 할당할 수 있다.

스택으로 할당된 블록은 힙으로 복제할 수 있다.

이 지점에서 이 블록은 일반 오브젝티브-C  객체 같이 참조 수를 셀 수 있게 된다.




반응형

댓글