쓰기 가능한 변수들

: 지역 변수는 블록안에서 값을 쓰지 못한다. 그러나 블록 안에서 쓰기가 가능한 변수들이 있다.

정적 변수나 전역 변수
- 정적 변수(정적 지역 변수)
- 정적 전역 변수
- 전역 변수

위 3가지 종류의 변수들은 블록안에서 값을 쓸 수 있다. 다만 블록 내부에서 처리하는 방법이 서로 다르다. 일단 블록이 C 함수로 변환될 때 블록을 선언한 메서드 내부에서 블록함수가 선언되지 않는다. 즉 블록을 선언한 메서드 내부에 선언된 정적 변수는 원칙적으로 블록함수에서 접근이 불가능 하다.
하지만 이러한 한계를 극복하기 위해서 블록이 선언될 때 블록 구조체 멤버 변수로 정적 지역 변수의 포인터를 할당한다. 블록 함수에서 블록 구조체 내부에 저장된 정적 지역변수의 포인터를 통해 값을 조작한다.

정적 전역 변수와 전역 변수는 블록 구조체에 따로 저장하지 않아도 접근이 가능하기 때문에 그대로 사용한다.

__block 지시어

예제 코드)

__block int val = 10;

void (^blk)(void) = ^{ val = 1; };

------> C, C++ 로 변환

struct __Block_byref_val_0
{
    void *__isa;
    __Block_byref_val_0 *__forwarding;
    int __flags;
    int __size;
    int val;
}

struct __main_block_impl_0
{
    struct __block_impl impl;
    struct __main_block_desc_0 *Desk;
    __Block_byref_val_0 *val;
}
// 생성자는 생략했다.

static void __main_block_func_0(struct __main_block_impl_0 *__self)
{
    __Block_byref_val_0 *val = __cself->val;
    
    (val -> _forwarding->val) = 1;
}

static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src)
{
    _Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

static void __main_block_dispose_0(struct __main_block_impl_0 *src)
{
    _Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

static struct __main_block_desc_0
{
    unsigned long reserved;
    unsigned long Block_size;
    void (*copy)(struct __main_block_impl_0 *);
    void (*dispose)(struct __main_block_impl_0 *);
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0),
    __main_block_copy_0,
    __main_block_dispose_0
};

int main()
{
    __Block_byref_val_0 val = {
        0,
        &val,
        0,
        sizeof(__Block_byref_val_0),
        10
    };

    blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val,     0x22000000);

    return 0;
}

__block 변수 val이 구조체로 바뀌었고 멤버변수로 10을 가진다. 그리고 블록을 위한 구조체의 멤버로 __block 변수 구조체의 포인터가 전달된다. 이는 여러개의 블록에서 하나의 __block 변수를 공유하기 위함이다.

void (^blk0)(void) = ^{ val = 0; };
void (^blk1)(void) = ^{ val = 1; };

-------> C, C++ 로 변환

__Block_byref_val_0 val = { 0, &val, 0, sizeof(__Block_byref_val_0), 10 };

blk 0 = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000 );
blk 1 = &__main_block_impl_1( __main_block_func_1, &__main_block_desc_1_DATA, &val, 0x22000000 );

위 두개의 블록은 __Block_byref_val_0 구조체의 동일한 인스턴스 val에 대한 포인터를 멤버변수로 가져서 __block 변수를 공유할 수 있는것이다.

블록에서 메모리 세그먼트
: 이전 챕터에서 블록은 Objective-C 객체라는 것을 배웠다. 그리고 블록의 클래스는 _NSConcreteStackBlock 이라고 배웠다. 다만, 블록이 저장되는 메모리 위치에 따라서 블록의 클래스가 달라질 수 있다.
- NSConcreteStackBlock : 스택 메모리에 저장되는 블록을 뜻한다.
- NSConcreteGlobalBlock : 데이터 섹션 메모리에 저장되는 블록을 뜻한다.
- NSConcreteMallocBlock : 힙 메모리에 저장되는 블록을 뜻한다.


NSConcreteGlobalBlock 클래스 객체 형태의 블록
: 글로벌 변수로 사용하는 블록 리터널이 있다면, 그 블록은 _NSConcreteGlobalBlock 클래스 객체로 생성된다.

void (^blk)(void) = ^{ printf("Global Block"); };

int main()
{
    ,,,,

이 코드에서 블록 리터럴은 _NSConcreteGlobalBlock 클래스 객체로 생성된다. 즉, 블록은 데이터 섹션에 저장되고 하나의 애플리케이션에서 하나의 인스턴스만 생성된다.  

다음은 지역변수로 블록 리터럴이 선언되었는데도, 데이터 섹션에 저장되는 경우를 살펴보자.
블록 인스턴스는 지역변수를 캡처하는 경우에만 변경된다. 예를들어 보자.
typedef int (^blk_t)(int);

for (int rate = 0; rate < 10; ++rate)
{
    blk_t blk = ^(int count){return rate * count; };
}
매 for문 반복시점마다 지역변수를 캡쳐하기 때문에 다른 인스턴스가 생성된다.

for (int rate = 0; rate < 10; ++rate)
{
    blk_t blk = ^(int count){return count; };
}
위 블록은 어떠한 지역변수도 캡쳐하지 않았기 때문에 데이터 섹션 메모리에 저장된다.

즉, 전역 변수로 블록이 선언되는 경우와 블록에서 지역변수를 캡쳐하지 않은경우에는 _NSConcreteGlobalBlock 클래스 객체가 되고, 나머지 방법으로 생성되면  _NSConcreteStackBlock 클래스 객체가 된다.

그러면 언제 _NSConcreteMallocBlock 클래스가 사용되는가? 
전역 변수처럼 데이터 섹션 메모리에 저장되는 블록은 변수 영역의 밖에서도 포인터를 이용해 안전하게 접근할 수 있다. 반면, 스택에 저장되어 있는 블록들은 블록의 영역을 벗어나면 폐기된다. 이 문제를 해결하기 위해 블록은 블록이나 __block 변수를 스택에서 힙으로 복사하는 기능을 제공한다.
블록이 힙으로 복사될 때, 블록 구조체의 isa 멤버 변수는 _NSConcreteMallocBlock으로 덥혀 써지고 힙 메모리 영역에 복사된다. 


블록을 힙 메모리로 복사하기
: ARC를 활성화하면 대부분의 경우 컴파일러가 자동으로 필요한 부분은 발견하고 블록을 스택에서 힙으로 복사한다. 다만, 컴파일러가 찾아내지 못하는 경우에는 블록을 수동으로 스택에서 힙으로 복사해야 한다. 그 작업을 수행하려면 copy 인스턴스 메서드를 사용하면 된다.


보통 블록을 메서드나 함수의 인자값으로 전달할 때 컴파일러가 찾아내지 못하여 수동으로 복사해야 한다. 
- (id)getBlockArray
{
    int val = 10;
    
    return [[NSArray alloc] initWithObjects:^{ NSLog(@"blk0:%d", val); },
                                                                   ^{ NSLog(@"blk1:%d", val); } ];
}

id obj = getBlockArray();
typedef void (^blk_t)(void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk();

위 코드는 blk();가 실행되면서 강제 종료된다. getBlockArray 함수에서 블록이 생성될때 블록은 스택 메모리 영역에 생성이 되기 때문에, NSArray의 initWithObjects메서드 안에서 매개변수로 넘어온 블록들을 retain하려고 해도 retain할 수 없기 때문이다. 그래서 매개변수로 블록을 넘길때는 명시적으로 블록을 힙 메모리 영역으로 복사해주는 작업이 필요하다. 이 작업은 두가지 방법을 통해 할 수 있다.

1) copy 메서드 사용.
- (id)getBlockArray
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:[^{ NSLog(@"blk0:%d", val)]; } copy],
                                                                   [^{ NSLog(@"blk1:%d", val); } copy] ];
}
// 블록에 copy 메서드를 사용하면 블록을 힙으로 복사하고 retainCount를 하나 늘린다. 그리고 매개변수로 넘어갈때 autorelease를 시켜줘서 initWithObjects 메서드 안에서 retain해도 retainCount는 1이 된다.

2) 지역변수로 retain한뒤 매개변수로 넘기기. 
- (id)getBlockArray
{
    int val = 10;
    typedef void (^blk_t)(void);
    blk_t blk1 = ^{ NSLog(@"blk1:%d", val)]; };
    blk_t blk2 = ^{ NSLog(@"blk2:%d", val); };

    return [[NSArray alloc] initWithObjects:blk1, blk2];
}
// 블록이 생성될 때는 스택 메모리 영역에 생성되지만 blk1, blk2에 대입될 때 ARC환경이기 때문에 block이 자동으로 retain된다. 블록을 retain하는 과정에서 스택 메모리 영역의 블록을 힙 영역으로 옮기고 그 뒤 retainCount를 하나 증가 시킨다. 그리고 initWithObjects 메서드의 매개변수로 전달될 때 autorelease가 된다. 결과적으로는 1)과 똑같은 동작을 하게 된다.


단 세가지의 경우는 컴파일러가 자동으로 복사한다.
1) usingBlock이란 이름을 포함하는 Cocoa 프레임워크 메서드
2) Grand Central Dispatch API
3) 함수, 메서드가 블록을 리턴하는 경우

위 세가지의 경우는 신경쓰지 않아도 컴파일러가 알아서 블록을 힙 메모리 영역으로 복사한다.

표) 블록복사

 블록 클래스

복사되는 곳 

복사 동작 방식 

_NSConcreteStackBlock 

스택 

스택에서 힙으로 복사 

_NSConcreteGlobalBlock 

데이터 섹션 

아무 변화 없음 

_NSConcreteMallocBlock 

힙 

객체의 참조 카운트 증가 

*데이터 섹션에 있는 블록을 복사해도 아무일도 생기지 않는다. 그리고 설상 힙 영역에 있는 블록을 복사한다고 해도 나쁜영향을 끼치치 않는다.

blk = [[[blk copy] copy] copy]

---->> 변환된다.

{
    blk_t tmp = [blk copy];
    blk = tmp;
}

{
    blk_t tmp = [blk copy];
    blk = tmp;
}

{
    blk_t tmp = [blk copy];
    blk = tmp;
}

이렇게 여러번 반복되도 결국 retainCount는 1이 된다. 


__block 변수의 메모리 세그먼트
: 블록 안에서 __block 변수를 사용하여 스택에서 힙으로 복사하면 __block 변수도 영향을 받는다.
__block 변수를 사용하는 블록이 스택에서 힙으로 복사 될 때 __block 변수들도 같이 스택에서 힙으로 복사된다. __block 변수가 __strong 속성의 객체라면 __block구조체의 멤버변수로 strongly하게 할당이 되고, __block변수가 블록에 캡쳐될 때도 strongly하게 할당된다. 


__ forwarding
: __block 변수 구조체의 __forwarding은 __block 변수가 스택 메모리 영역에 생성 될 때는 스택의 생성된 __block변수 구조체를 가리키고 있지만 __bock 변수가 힙영역으로 복사되면 스택 메모리 영역에 있는 __block 변수 구조체의 __forwarding은 힙 영역의 자신의 복사본인 __block 변수를 가리키고 있다. 
블록에서 또는 블록 밖에서 __block변수의 값을 사용할때는 무조건 __forwarding 포인터를 참조해서 값을 가져오기 때문에 __block변수가 스택에서 힙으로 복사되어 스택, 힙 두개의 영역에 중복 존재하더라도 사용하는 곳에서는 늘 똑같은 변수를 참조해서 사용하게 된다.


'IOS > Objective-C' 카테고리의 다른 글

Objective-C Runtime Programing Guide  (0) 2017.02.17
Block의 모든것(4)  (0) 2017.01.07
Block의 모든것(2)  (0) 2017.01.02
Block의 모든것(1)  (0) 2017.01.02
ARC 규칙  (0) 2017.01.01
Posted by 홍성곤
,