Naming Conventions

IOS/Objective-C 2017. 2. 18. 17:26

Category의 메서드를 만들때는 prefix를 붙여야 한다. 특히, Foundation 클래스의 카테고리를 만들때는....
ex) 내가 NSString의 a라는 메서드를 카테고리로 선언했는데, 다른 프레임워크에서 똑같이 NSString 카테고리로 a라는 메서드를 만들었으면 두 메서드는 충돌이 나고, runtime시 하나의 메서드만 NSString 클래스에 더해질 것이다.

ex2) 내가 iOS8 버전에서 NSString의 a라는 메서드를 만들었는데 iOS9버전에 애플이 NSString에 a라는 메서드를 추가했을때 역시 충돌이 일어난다.



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

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

objective-c 프로그램은 런타임 시스템과 상호작용하기 위한 3가지 레벨이 있다.

1. Objectitve-C Source Code
- message call

2. NSObject Methods
- class, isKindOfClass, isMemberOfClass, respondsToSelector, conformsToProtocol, methodForSelector 등등

3. Runtime Functions
- 런타임 시스템은 dynamic shared library이다. 헤더파일들/usr/include/objc 디렉토리 안에 위치해 있다.


Messaging

objc_msgSend(receiver, selector, arg1, arg2, ...)

NSObject, NSProxy클래스로부터 상속받은 모든 클래스는 isa 변수(슈퍼클래스를 가리키는 포인터)와 dispatch table을 가진다.
dispatch table은 자신의 message name과 매칭되는 실제 함수 구현체의 포인터를 가지고 있는 map table이라고 보면된다.

objc_msgSend가 하는일은 receiver의 dispatch table에서 selector의 이름으로 함수 구현체를 찾고, 없으면 isa포인터를 통해 슈퍼클래스에 접근한 후 다시 dispatch table을 뒤진다.(이런식으로 recursive하게 root class까지 넘어간다.)

위 내용이 동적 바인딩이다. 


Using hidden Arguments

objc_msgSend가 결국 메세지에 맞는 함수를 찾아서 호출해주는데 프로그래머가 메세지에 넘겨주는 파라미터 이외에 두개의 숨겨진 파라미터 2개를 같이 넘겨준다.
하나는 receiver, 하나는 selector이다. 


Getting a Method Address

동적바인딩을 피하고 싶은경우, methodForSelector: 메서드를 사용해서 함수의 주소를 얻어와서 직접 함수를 call하면 된다. 하나의 메서드가 연속적으로 여러번 불려야 할경우 퍼포먼스를 조금이나마 개선하기 위해 해당 방식을 사용할 수 있다.

methodForSelector:은 런타임시스템에 의해 제공된다. Objective-C 언어가 제공하는것이 아니다. 


Dynamic Method Resolutions

프로퍼티를 @dynamic 지시어를 지정하면 dynamic 메소드 콜을 할 수 있다.
예를들어 appdelegate의 window 프로퍼티를 dynamic 메소드 콜로 동작하게 하려면

UIWindow *dynamicMethodIMP(id self, SEL _cmd) {

    return [[UIWindow alloc] initWithFrame:CGRectZero];

}

@dynamic window;

+ (BOOL)resolveInstanceMethod:(SEL)aSel

{

    if (aSel == @selector(window))

    {

        class_addMethod([self class], aSel, (IMP)dynamicMethodIMP, "aaa");

        

        return YES;

    }

    

    return [super resolveInstanceMethod:aSel];

}

이런식으로 구현 하면 됨. resolveClassMethod:를 사용하여 클래스 메소드도 dynamic 메소드 콜 적용 가능.


Dynamic Loading

NSBundle를 통해서 Objective-C 모듈을 동적으로 로딩할 수 있다. 


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

Naming Conventions  (0) 2017.02.18
Block의 모든것(4)  (0) 2017.01.07
Block의 모든것(3)  (0) 2017.01.02
Block의 모든것(2)  (0) 2017.01.02
Block의 모든것(1)  (0) 2017.01.02
Posted by 홍성곤
,

객체 캡쳐하기
: 객체는 블록에서 캡쳐되면 블록 구조체의 멤버 변수로 id __strong object;형태로 선언하고 객체를 소유한다.
원래 C구조체는 __strong 속성이 부여된 멤버 변수를 가질 수 없다. 그 이유는 컴파일러가 구조체를 언제 초기화하고 언제 폐기하는지의 적절한 메모리 관리 방식을 가지고 있지 않기 때문이다. 그러나, Block 구조체는 Objective-C객체로 취급되기 때문에 컴파일러가 적절한 메모리 관리 방식을 가지고 있다. 그래서 Block 구조체는 멤버 변수로 __strong 속성이 부여된 멤버변수를 가질 수 있다.

다만, __weak 속성의 객체가 캡쳐될 경우 블록 구조체는 id _weak object; 형태 멤버변수를 선언하고 객체를 가리킨다. 밖에서 객체가 release되면 블록 구조체의 id _weak object는 nil이 된다. 

__block id __weak object 객체를 캡쳐하면 __block 구조체 변수는 가리키고 블록이 힙으로 복사될 때 __block변수는 소유를 하지만 __weak 객체가 block 구조체의 할당될 때 __weak속성으로 할당되기 때문에 객체가 release되면 nil이 할당된다.

블록 순환 참조
: __strong 속성의 객체형 지역 변수가 블록에서 사용되면 블록은 해당 객체의 소유권을 갖는다. 하지만 이 때문에 순환 참조가 발생할 수 있다. 

@interface MyObject : NSObject
{
    blk_t blk;
}
@end

@implementation MyObject

- (instancetype)init
{
    self = [super init];

    if (self)
    {
        blk =^{ NSLog(@"self = %@", self); };
    }

    return self;    

}

- (void)dealloc
{
    NSLog(@"dealloc");
}

@end

int main()
{
    {
        id o = [[MyObject alloc] init];
    }   
}
이 경우 순환참조가 발생하여 "dealloc" 메세지는 절대 발생하지 않는다.
blk가 self를 strongly하게 캡쳐하면서 MyObject 인스턴스의 retainCount는 2가 되고, MyObject 인스턴스 지역 변수의 범위를 벗어난다고 해도 retainCount는 1이 되어 dealloc되지 않는다.

self를 캡쳐할 때,

id __weak sWeakSelf = self;

if (self)
{
    blk =^{ NSLog(@"sWeakSelf = %@", sWeakSelf); };
}

이렇게 캡쳐해야 순환참조 발생을 예방할 수 있다. 

그러나, self를 블록에서 사용하지 않더라도 순환 참조가 발생할 수 있다. 다음 코드를 보자.

@interface MyObject : NSObject
{
    blk_t blk;
    id obj;
}
@end

@implementation MyObject

- (instancetype)init
{
    self = [super init];

    if (self)
    {
        blk =^{ NSLog(@"
obj = %@", obj); };
    }

    return self;    

}

- (void)dealloc
{
    NSLog(@"dealloc");
}

@end

이 경우도 순환 참조가 발생한다. init메서드에서 obj란 self->obj이기 때문에 self가 캡쳐된다.
순환 참조를 피하기 위해서 블록안에서 self 캡쳐를 못하게 하는 방법이 있다.

id sObj = obj;

if (self)
{
    blk =^{ NSLog(@"sO
bj = %@", sObj); };
}

이러면 블록 안에 코드에서는 self가 들어가지 않는다. 그러므로 self를 캡쳐하지 않는다.

* ARC를 사용하지 않을 때의 블록 순환 참조에 대한 방법은 생략하겠다. 


Copy/Release

블록은 C 언어에서 확장된 것이기 때문에 C언어 에서도 블록을 사용할 수 있다. 이 경우 Objective-C의 copy, release메서드 대신에 Block_copy와 Block_release메서드를 사용한다. Objective-C의 런타임은 스택 메모리 영역에 있는 블록을 retain할 때, Block_copy메서드를 사용한다.(스택 메모리 영역의 있는 객체에 retain 메세지를 보내봤자 아무일도 일어나지 않기 때문이다.) Block_copy메서드는 스택 메모리 영역에 있는 객체를 힙 영역으로 복사하고 retainCount를 하나 증가 시킨다.
C언어에서 Block을 retain, release 할 때, Block_copy, Block_release 메서드를 사용하기 바란다.

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

Naming Conventions  (0) 2017.02.18
Objective-C Runtime Programing Guide  (0) 2017.02.17
Block의 모든것(3)  (0) 2017.01.02
Block의 모든것(2)  (0) 2017.01.02
Block의 모든것(1)  (0) 2017.01.02
Posted by 홍성곤
,

쓰기 가능한 변수들

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

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

위 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 홍성곤
,

[블록 구현]
여기서 설명되는 코드는 ARC 환경이라고 가정한다.

블록의 내부
: 블록구문은 뭔가 특별하다고 생각할지 모르지만, 컴파일러는 블록을 평범한 C 언어 코드로 변환한 다음 똑같이 컴파일한다.

소스코드 변환하기
: 블록 구문을 포함하는 소스코드는 -rewrite-objc 컴파일러 옵션을 사용해서 표준 c++ 소스코드로 변환할 수 있다. 변환된 소스코드가 C++로 쓰여졌기는 하지만, 구조체를 위해 생성자를 사용할 때를 제외하고는 거의 C로 쓰여졌다.
(clang -rewrite-objc file_name_of_the_source_code)

int main()
{
    void (^blk)(void) = ^{ printf("Block\n"); };    
    blk();
    return 0;
}

---> C++소스로 변환

struct __block_impl 
{
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __main_block_impl_0 //실제 블록 구조체 라고 보면 됨.
{
    struct __block_impl impl;
    struct __main_block_desc_0 *Desk;
    __main_block_impl_0 (void *fp, struct __main_block_desc_0 *desk, int flags = 0)
    {
        impl.isa = &NSConcreteStatckBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    printf("Block\n");
}

static struct __main_block_desc_0
{
    unsigned long reserved; //여유 공간
    unsigned long Block_size; //블록 크기
} __main_block_desc_0_DATA =  {
        0,
        sizeof(struct __main_block_impl_0)
    };

int main()
{
    void (*blk)(void) = 
        (void) (*)(void)&__main_block_impl_0(
            (void *)__main_block_func_), &__main_block_desc_0_DATA);

    ((void *)(struct __block_impl *)
        (struct __block_impl *)blk) -> FuncPtr)((struct __block_impl *) blk);

    return 0;
}

엄청나게 코드가 늘어났다! -_- 추후에 하나하나 자세히 들여다 볼것이다. 일단 선행되는 개념부터 알아보겠다.


Objective-C 에서 self

MyObject의 인스턴스 메서드라고 하자.
- (void)method:(int)arg
{
    NSLog(@"%p %d\n", self, arg);
}

------> 컴파일러가 C함수로 변환한다.

void _I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg)
{
    NSLog(@"%p %d\n", self, arg);
}

이제 메서드를 호출하는 경우를 보자.

MyObjecdt *obj = [[MyObject alloc] init];
[obj method:10];

------> 
컴파일러가 C함수로 변환한다.

MyObject *obj = objc_msgSend( objc_getClass("MyObject"), self_registerName("alloc"));
obj = objc_msgSend(obj, sel_registerName("init"));
objc_msgSend(obj, sel_registerName("method:", 10));

세번째 줄에서 MyObject의 인스턴스 메서드로 "mehod"라는 이름으로 등록되 있는 함수 포인터를 찾아 호출한다. 그러면 
void _I_MyObject_method_(struct MyObject *self, SEL _smd, int arg) 함수가 호출될 것이다. 이때 첫번째 인자로 넘어온 obj를 함수의 첫번째 인자 self에 대입한다.

static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    printf("Block\n");
}
를 보면 __cself에 블록 구조체인 
struct __main_block_impl_0의 포인터를 인자로 받고 있다. 이 함수에서는 __cself가 사용되지 않았지만 블록에 변수가 캡쳐되어 블록안에서 사용된다면 캡쳐된 변수를 __cself를 통해 가져올 것이다. 이해가 안된다면 밑에 설명을 계속 보자.

생성자를 제외한 블록 구조체이다.
struct __main_block_impl_0
{
    struct __block_impl impl;
    struct __main_block_desc_0 *Desk;
};
블록 구조체는 블록에 대한 정보를 가지고 있는 2개의 구조체를 멤버변수로 가진다.

main 함수에서 호출되는 부분을 살펴보자.

void (*blk)(void) = 
        (void) (*)(void)&__main_block_impl_0(
            (void *)__main_block_func_), &__main_block_desc_0_DATA);

----> 형변환 코드를 제외해 보자.

struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;

코드를 보면 블록 구조체가 적절한 값(함수 포인터, desc 구조체)으로 초기화 되어 변수에 할당 되었다.

이제 __block_impl 부분을 블록 구조체에 넣어서 확장해보면 다음과 같이 쓸 수 있다.

struct __main_block_impl_0
{
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
    struct __main_block_desc_0 *Desk;
}

이 구조체는 다음과 같이 초기화 되었다.

isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;

그리고 실제 블록이 호출되는 부분을 살펴보자.

((void *)(struct __block_impl *)
        (struct __block_impl *)blk) -> impl.FuncPtr)((struct __block_impl *) blk);

-----> 형 변환 코드를 제거해보자.

(*blk->impl.FuncPtr
)(blk);

블록 구조체의 저장되어 있는 함수포인터를 호출 하면서 자신(블록 구조체)을 인자로 넘겨주고 있다. 이는 __cself로 전달된다.

결국 Block의 구조를 보면 Objective-C 객체의 구조랑 똑같다. 
struct __main_block_impl_0은 우리가 objective-C객체의 구조체라고 알고있는 struct objc_object의 확장형이라고 보면된다. 즉, 블록 역시 Objective-C 객체라고 볼 수 있다. 블록 구조체의 isa값 _NSConcreteStackBlock 역시 Objective-C 클래스 구조체라고 알려져있는 class_t 구조체의 인스턴스를 가리킨다. 즉, _NSConcreteStackBlock은 블록의 클래스라고 보면되고, 블록을 Objective-C 객체로 인식하고 처리하기 위한 정보를 담고 있다.


지역 변수 캡쳐
: 블록이 생성될 때, 블록의 매개변수로 지역변수가 전달되면 블록 구조체의 멤버변수가 생성되고 매개변수로 넘어온 값이 저장된다. 즉, 생성된 시점에 값이 블록 구조체의 멤버변수로 저장되기 때문에 블록 생성 이후에 값이 변경된다 하더라도 블록 구조체의 멤버변수의 값이 바뀌지 않는다.


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

Block의 모든것(4)  (0) 2017.01.07
Block의 모든것(3)  (0) 2017.01.02
Block의 모든것(1)  (0) 2017.01.02
ARC 규칙  (0) 2017.01.01
GCD(Grand Central Dispatch)(2)  (0) 2016.12.19
Posted by 홍성곤
,

개요

: C 언어에 새로 추가된 확장 기능이다.
: "자동(지역) 변수와 함께 동작하는 익명 함수"라고 할 수 있다.


블록 변수 선언과 사용
int (^blk)(int); 
: 지역 변수, 함수 인자, 정적 지역 변수, 정적 전역 변수, 전역 변수로 선언이 가능하다.
typedef int (^blk_t)(int);로 복잡성을 피할 수 있다. 이렇게 하면 blk_t 타입으로 변수를 선언할 수 있다.
: 블록 포인터로도 사용이 가능하다.
typedef int (^blk_t)(int);
blk_t blk = ^(int count){return count + 1};
blk_t *blkptr = &blk;
(*blkptr)(10);


자동 변수 캡처

int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{ printf(fmt, val); };

val = 2;
fmt = "These values were changed. val = %d\n";
blk();

// 결과: "val = 10"

fmt, val은 블록 리터럴이 선언될 때 이미 캡쳐되어 있어서, 블록 리터럴 선언 이후에 값이 변경되더라도 블록 리터럴 내부의 값은 절대 영향을 받지 않는다.


__block 지시어 
: __block 지시어를 사용하면 지역변수를 캡처하지 않고 블록 리터럴 내부에서 수정도 가능하다.

int val = 0;
void (^blk)(void) = ^{val = 1;};
// 컴파일 에러 발생! 블록 리터럴 밖에서 선언한 지역 변수에 값을 할당했기 때문!

__block int val = 0; 으로 코드를 변경해야 한다.


캡처된 자동 변수

그렇다면 이 코드도 컴파일 에러가 발생할까? 

id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
    [array addObject:@2];
};
// 컴파일 에러가 발생하지 않는다. 이 경우 캡쳐된 것은 NSMutableArray 클래스 객체에 대한 구조체 인스턴스의 포인터이다.

id array = [[NSMutableArray alloc] init];
void (^blk)(void)=^{
    array = [[NSMutableArray alloc] init];
};
// 컴파일 에러 발생! 

C 배열은 블록에 의해 자동으로 캡쳐될 수 없다.
const char text[] = "hello";
void (^blk)(void) = ^{
    printf("%c\n", text[2]);
};
// 컴파일 에러 발생. 

그러므로 포인터를 사용해야 한다.
const char *text = "hello";
void (^blk)(void) = ^{
    printf("%c\n", text[2]);
}
// 정상동작 한다.

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

Block의 모든것(3)  (0) 2017.01.02
Block의 모든것(2)  (0) 2017.01.02
ARC 규칙  (0) 2017.01.01
GCD(Grand Central Dispatch)(2)  (0) 2016.12.19
GCD(Grand Central Dispatch)(1)  (0) 2016.12.19
Posted by 홍성곤
,

ARC 규칙

IOS/Objective-C 2017. 1. 1. 16:03

개요
: 요약하면 프로그래머가 더 이상 "retain", "release"를 호출할 필요가 없다는 것이다. 다음 조건에서 여러분의 소스코드는 자동으로 ARC가 활성화된 상태로 컴파일 된다.
- clang(LLVM 컴파일러) 3.0 이상
- 컴파일 옵션 "-fobjc-arc"
* xCode 4.2부터는 Clang이 기본 컴파일러이고, -fobjc-arc 옵션도 기본으로 켜져있다. 따라서, Xcde 4.2 이상에서 작업하면 추가설정이 필요없다.


1) 소유권 수식어
- Objective-C는 id나 각 객체 타입을 사용하여 객체의 데이터 형을 지정한다.

객체 데이터 형은 NSObject *와 같이 Objective-C 클래스의 포인터 형이다. id은 클래스 이름을 숨길 때 사용한다. id는 C언어에서 void*와 동일하다.
ARC에서 id와 객체 데이터형 변수는 다음 네 가지 소유권 수식어 중 하나를 가져야 한다.

- __strong
- __weak
- __unsafe_unretained
- __autoreleasing

*_weak vs __unsafe_unretained 수식어
: __unsafe_unretained는 이름이 암시하듯 전혀 안전하지 않다. 이 속성을 갖는 변수를 ARC의 자동 메모리 관리에서 제외되기 때문에 직접 변수를 관리해 주어햐 한다. 

id __weak obj1 = nil;

{
    id __strong obj0 = [[NSObject alloc] init];
    obj1 = obj0;
    NSLog(@"A: %@", obj1);
}

NSLog(@"B: %@", obj1);
// 결과
// A: <NSObject: 0x753e180>
// B: (null)
// B를 찍을때는 obj0 변수가 변수범위를 벗어나서 강한참조가 사라지면서 이 객체가 제거될 때, 약한참조도 제거되어 obj1에 자동으로 nil이 대입된다. 

만약 위 코드에서 id __unsafe_unretained obj1 = nil;가 사용되면 B를 찍을때 obj1에 nil이 대입되는 로직이 적용되지 않아서 B를 찍는시점에는 ojb1에는 허상 포인터가 들어가 있을 것이다. 이 허상포인터에 접근하면 크래쉬를 발생시킨다. 

id __unsafe_unretained obj = [[NSObject alloc] init];

__unsafe_unretained 변수는 __weak과 마찬가지로 객체의 소유권을 갖지 않고, 위의 예제에서 객체는 생성되자마자 릴리스 된다. 


2) 컴파일러가 알아서 __autoreleasing을 처리해준다.
- 객체를 생성하지 않고 얻어오려면 alloc/new/copy/mutableCopy 메서드 그룹이 아닌 다른 메서드를 사용해야 한다. 위 4가리 그룹 메서드가 아닌 메서드가 반환하는 객체는 자동으로 오토릴리스 풀에 등록시킨다. 예외적으로 init으로 시작하는 메서드는 반환값을 오토릴리스 풀에 등록시키지 않는다.

- __weak 속성을 갖는 변수가 사용되면 객체는 언제나 오토릴리스 풀에 등록된다. 
id __weak obj1 = obj0;
NSLog(@"class =%@", [obj1 class]);

위 소스코드는 아래와 동일하다.

id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);

컴파일러가 첫번째 코드를 두번째 코드로 자동변환하는 이유는 __weak으로 수식되면 강한참조가 없기 때문에 어느 시점에서든 제거될 수 있기 때문이다. 만일 객체가 오토릴리스 풀에 등록되면 @autoreleasepool 블록이 끝나기 전에는 그 객체가 계속 존재하게 된다. 따라서 __weak 변수를 안전하게 사용하기 위해 객체가 자동으로 오토릴리스 풀에 등록된다.

- 더블 포인터 변수는 __autoreleasing 소유권 수식어를 갖는다.(id*, NSObject**)

NSError *error = nil;
NSError **pError = &error;
// 컴파일 오류를 발생시킨다. 객체 포인터를 대입하려면 두 변수의 소유권 수식어가 동일해야 한다.

컴파일 오류를 피하려면 다음과 같이 바꿔줘야 한다.
NSError *error = nil;
NSError *__strong *pError = &error;

코코아 프레임워크에는 수행결과를 인자로 반환하는 메서드들이 많이 있다.
performOperationWithError:(NSError **)error;
같은 메서드 들이다. ARC환경에서는 컴파일러가 다음과 같이 변환한다.
-> perfromOperationWithError:(NSError * __autoreleasing *)error;

NSError *error = nil;
BOOL result = [obj performOperationWithError:*error];
는 컴파일러가 어떻게 변환할까?

NSError __strong *error = nil;
NSError __autoreleasing *tmp = error;
BOOL result = [obj performOperationWithError:&tmp];
로 변환한다.


3) 객체 형의 변수들은 C언어의 구조체 또는 공용체의 멤버가 될 수 없다.

struct Data
{
    NSMuttableArray *array;
}
// 컴파일 에러 발생.

심지어 LLVM 컴파일러 3.0에서는 C 언어 명세의 제한 때문에 C구조체의 생명주기를 관리할 방법이 없다. ARC에서는 컴파일러가 메모리를 관리하기 위해 객체의 생명주기를 알고 관리해야 한다. 만약 C구조체에 객체를 넣길 원한다면 객체를 "void *"로 캐스팅하거나 __unsafe_unretained 소유권 수식어를 사용해서 할 수 있다.

컴파일러는 __unsafe_unretained 소유권 수식어를 사용한 변수를 관리하지 않는다. 메모리 누수 또는 애플리케이션이 강제 종료되지 않게 하려면 소유권을 직접 관리해야 한다.


4) 'id' 그리고 'void *'는 명시적으로 형 변환되어야 한다.

non-ARC 환경에서 'id'에서 'void *'로의 형 변환은 다음과 같이 아무 문제없이 작동한다.
그리고 void*에서 다시 대입한 id 변수를 통해서 메서드 호출도 문제없다.
/* non-ARC */
id obj = [[NSObject alloc] init];
void *p = obj;
id o = p;
[o release];

하지만 위 소스는 ARC 환경에서 컴파일 에러가 발생한다. ARC 환경에서는 id 또는 객체 타입들과 'void *' 사이에서 형 변환을 하기 위해서는 특별한 종류의 형 변환을 사용해야 한다.
단지 할당만 하기 위해서는 __bridge 형 변환을 사용할 수 있다.

- __bridge case
id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
id o = (__bridge id)p;

void *를 위한 __bridge 형 변환을 사용(void *p = (__bridge void *)obj;)하면 __unsafe_unretained 수식어 변수보다 더 위험하다. 객체의 소유권을 주의깊게 관리하지 않으면 댕글링 포인터 때문에 크래쉬가 난다.

__bridge_retained cast
id obj = [[NSObject alloc] init];
void *p = (__bridge_retained void *)obj;

위의 소스는 non-ARC 환경에서 다음과 같이 재작성할 수 있다.
id obj = [[NSObject alloc] init];
void *p = obj;
[(id)p retain];

__bridge_transfer cast
: 할당이 된 직후에 객체를 릴리즈하게 된다.

id obj = (__bridge_transfer id)p;

위 소스는 non-ARC 환경에서 다음과 같이 재작성할 수 있다.

id obj = (id)p;
[obj retain];
[(id)p release];
// obj 변수는 __strong으로 수식되었기 때문에 retain 되었다.

*Objective-C 객체와 Core Foundation 객체
- Core Foundation 객체는 Core Foundation 프레임워크에서 사용하는 객체로, 대부분 C 언어로 작성되었고 레퍼런스 카운트를 갖고있다. Core Foundation 프레임워크에서 사용하는 CFRetain, CFRelease 함수는 Objective-C에서 non-ARC 일때 사용하는 retain, release 메서드와 동일하다. Core Foundation 객체와 Objective-C 객체는 어느 프레임워크에서 객체를 생성하느냐에 따라 생성하는 방식이 다를 뿐 거의 같다. 일단 객체를 생성하고 나면 두 가지 프레임워크 모두에서 동일하게 사용할 수 있다. 예를 들어 Foundation 프레임워크 API로 생성한 객체여도 Core Foundation 프레임워크 API로 릴리즈할 수 있으며, 그 반대로 가능하다.

엄밀히 얘기해서 Core Foundation 객체와 Objective-C 객체는 동일한 객체다. 따라서 ARC 환경이 아닌 경우에 C 스타일 형 변환 방식으로 객체를 서로 변환할 수 있다. 이런 객체 형 변환 방식은 CPU 비용이 발생하지 않기 때문에 Toll-Free Bridge라고 부른다. 

ARC를 활성화한 환경에서 Objective-C와 Core Foundation 객체 사이의 Toll-Free Bridge 변환을 위해 다음과 같은 함수들이 제공된다.

CFTypeRef CFBridgingRetain(id X)
{
    return (__bridge_retained CFTypeRef)X;
}

id CFBridgingRelease(CFTypeRef X)
{
    return (__bridge_transfer id)X;
}

CFBridgingRetain Function
CFMutableArrayRef cfObject = NULL;

{
    id obj = [[NSMutableArray alloc] init];
    cfObject = CFBridgingRetain(obj);
    CFShow(cfObject);
    printf("retain count = %d\n", CFGetRetainCount(cfObject));
}

printf("retain count after the scope = %d\n", CFGetRetainCount(cfObject));
CFRelease(cfObject);
// 결과
// () . 빈 배열을 의미한다.
// retain count = 2
// retain count after the scope = 1

위 소스에서 cfObject = CFBridgingRetain(obj); 대신 cfObject = (__bridge_retained CFMutableArrayRef)obj;를 사용할 수 있다. 선호하는 것을 사용하면 된다.

CFBridgingRelease function
CFMutableArraytRef cfObject = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
printf("retain count = %d\n", CFGetRetainCount(cfObject));

id obj = CFBridgingRelease(cfObject);
printf("retain count after the cast = %d\n", CFGetRetainCount(cfObject));
//결과
// retain count = 1;
// retain count after the cast = 1;

위 소스에서 id obj = CFBridgingRelease(cfObject); 대신 id obj = (__bridge_transfer id)cfObject;로 대체할 수 있다. 선호하는 것을 사용하면 된다.

Property
ARC 환경에서 여러가지 Property 수식어들을 사용할 수 있다.

- assign: __unsafe_unretained 소유권 수식어와 대응.
- copy: __strong 소유권 수식어와 대응.
- retain: __strong 소유권 수식어와 대응.
- strong: __strong 소유권 수식어와 대응.
- unsafe_unretained: __unsafe_unretained 소유권 수식어와 대응.
- weak: __weak 소유권 수식어와 대응.
프로퍼티에 대입하는 것은 소유권 수식어에 해당되는 변수에 대입하는 것과 동일하다.

프로퍼티를 위해 인스턴스 변수를 선언했을 때 이는 프로퍼티와 같은 소유권 수식을 가져야 한다.
@interface SomeObject:NSObject
{
    id obj;
}
@end

@property (nonatomic, weak) id obj;
// 이것은 컴파일 에러를 발생 시킴.
// 멤버 변수를 __weak으로 수식하던가 property를 strong 속성으로 지정하여야 한다.

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

Block의 모든것(2)  (0) 2017.01.02
Block의 모든것(1)  (0) 2017.01.02
GCD(Grand Central Dispatch)(2)  (0) 2016.12.19
GCD(Grand Central Dispatch)(1)  (0) 2016.12.19
Objective-C의 동적 바인딩, 그리고 Message dispather와 Runtime  (0) 2016.12.18
Posted by 홍성곤
,

디스패치 큐 제어하기
: GCD는 디스패치 큐에서 작업을 제어할 수 있는 많은 유용한 API를 제공한다.

dispatch_set_target_queue
: target을 세팅하기 위한 큐이다. 이것은 주로 새로 생성된 큐에 중요도를 설정하는데 사용된다.
: 시리얼과 콘커런트 디스패치 큐 모드 dispatch_queue_create함수를 사용해서 생성될 때, 스레드의 중요도는 글로벌 디스패치 큐의 기본 중요도와 같다. 생성된 이후 중요도를 수정하기 위해서는 이 함수를 사용할 수 있다.

dispatch_queue_t serialQueue = dispatch_queue_create("com.hsg2510.serial", NULL);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(serialQueue, globalQueue)
// serialQueue의 중요도는 globalQueue의 중요도와 같아진다.
// dispatch_set_target_queue함수의 첫번인자 값으로 메인 큐, 글로벌 큐를 넣으면 안된다.

: 또한 디스패치 큐의 계층 구조를 만들수도 있다. 이는 관련자료를 찾아보기 바란다.

 dispatch_after
: 큐에 작업의 시작 타이밍을 설정하는 것이다. 지정된 시간 이후에 작업을 실행하려고 할 때 사용한다.

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), {
    NSLog(@"hi ~~ ");
 })
// dispatch_time은 첫번째 인자로 들어온 시간부터 2번째 인자의 시간만큼 지난시간 리턴한다.
// dispatch_after의 첫번째 인자는 해당 블록이 큐에 추가될 시간을 지정한다.
// 지정된 블록을 3초 후에 메인 디스패치 큐에 추가한다.
// "ull"은 C언어 문법에서 지정된 타입이다. unsigned long long
// RunLoop가 1/60초 간격으로 실행하는 경우, 블록은 3초후와 3초 + 1/60초 사이에서 실행된다. 많은 작업이 메인 디스패치 큐에 추가되거나 메인 스레드에 지연이 있으면, 그 뒤에 실행될 수 있다. 따라서 정확한 타이머 개념으로 사용하는 것은 문제지만, 대략적인 작업의 지연을 유도할 때 매우 유용하게 사용된다.


디스패치 그룹
: 디스패치 큐의 여러개의 작업들이 비동기적으로 실행될 때 모든 작업이 끝난뒤에 해야되는 작업이 있다면 디스패치 그룹을 사용하면 된다.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue, ^{ NSLog(@"blk0"); });
dispatch_group_async(group, queue, ^{ NSLog(@"blk1"); });
dispatch_group_async(group, queue, ^{ NSLog(@"blk2"); }); 

dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"end"); });
dispatch_release(group);
//3개의 블록작업이 aync하게 동작되고 모든 작업이 끝나면 "end"가 출력된다.

또한 단순하게 디스패치 그룹과 관련된 모든 작업을 기다릴 수 있다.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORTY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue, ^{ NSLog(@"blk0"); });
dispatch_group_async(group, queue, ^{ NSLog(@"blk1"); });
dispatch_group_async(group, queue, ^{ NSLog(@"blk2"); });

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);
//dispatch_group_wait 함수의 두번쨰 인자값을 대기 시간을 지정하는 일시적 중단이다. FOREVER로 지정되어 있으니 그룹에 대한 작업이 끝날때까지 영원히 기다린다.

dispatch_after 함수로 배운바와 같이 1초를 기다리게 할 수 있다.

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);

if (result == 0)
{
    // 1초 뒤에 디스패치 그룹과 연관된 모든 작업이 종료됨.
}
else
{
    // 1초 뒤에도 디스패치 그룹과 연관된 작업이 모두 종료되지 않음.
}
// dispatch_group_wait 함수에 지정된 시간이 지나가는 동안 또는 그룹에 관련된 작업이 모두 종료될 때 까지 현재 스레드가 중지된다. 

DISPATCH_TIME_NOW를 사용해서 현재 그룹과 연관된 작업이 모드 종료되었는지 확인하는 용도로 쓸 수도 있다.

long result = dispatch_group_wait(group, DISPATCH_TIME_NOW);
// 예를들면, 메인 스레드에서 실행되는 여러 RunLoop의 각각 루프에서 모든 작업이 끝났는지 확인하는 용도로 사용할 수 있다. 


dispatch_barrier_async
: 큐에 다른 작업을 기다리는 함수이다.
: 데이터베이스 작업을 할 때, select작업은 serial로 실행될 필요가 없기 때문에 concurrent로 돌려도 된다. 하지만 도중에 update, insert작업이 들어가면 무조건 concurrent로 실행하면 안된다. 이때 유용하게 사용할 수 있는 함수이다. 

dispatch_queue_t queue = dispatch_queue_create("com.hsg2510.example", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_async(queue, blk4_for_reading);

dispatch_release(queue);
// 읽기 작업 5개가 비동기로 실행된다.

만약 위 5개 작업 도중 update작업이 들어가야 될 경우 곤란하다. 예를들어 첫번째, 두번째 블럭은 update 이전 데이터가 조회되야 되고 3~5번째 블럭은 update이후 데이터가 조회되야 한다면 어떻게 할것인가? 이를 위해 dispatch_barrier_async함수를 사용할 수 있다.

dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_barrier_async(queue, blk_for_update);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_async(queue, blk4_for_reading);
// dispatch_barrier_async함수를 위 두개의 작업이 다끝나기를 기다렸다가 그때 작업을 큐에 추가한다. 그리고 밑 3개의 작업은 update작업이 끝난뒤에 큐에 추가된다.

효과적인 데이터베이스 또는 파일 엑세스를 구현하기 위해 콘커런트 디스패치 큐와 dispatch_barrier_async함수를 사용하길 바란다.


dispatch_sync
: 디스패치 큐에 동기적으로 블록을 추가한다. 그리고 dispatch_sync가 실행된 큐는 해당 작업이 끝날때까지 멈춘다.

dispatch_queue_t_ queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{ /*task*/ });
// task가 끝날때까지 dispatch_sync를 실행한 큐는 멈춤. dispatch_sync가 호출된 뒤에 지정된 작업이 끝나기전까진 함수는 리턴을 하지 않는다.

다음 코드를 메인 스레드에서 실행할 때, 데드락이 발생할 수 있다.
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{NSLog(@"Hello?");});
//메인 큐에 블록을 추가하는데 블록이 실행되기전 블록에 대한 작업이 끝날때까지 큐를 기다리게 한다. 즉, 블록이 실행될 리가 없으므로 데드락을 일으킨다.

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
       dispatch_sync(queue, ^{ NSLog(@"Hello"); });
 });
//이것 역시 데드락을 일으킨다. 

물론 같은 일이 시리얼 디스패치 큐에도 발생한다.
dispatch_queue_t queue = dispatch_queue_create("com.hsg2510.serialDispatchQueue", NULL);
dispatch_async(queue, ^{
    dispatch_sync(queue, ^{ NSLog(@"Hello?"); });
 });
// 이것 역시 데드락을 일으킨다.

*dispatch_barrier_async에 대응하는 dispatch_barrier_sync가 있다. 디스패치 큐의 모든 작업이 완료된 후 dispatch_sync함수처럼 지정된 작업이 완료되기를 기다린다.
*dispatch_sync와 같은 sync API를 사용할 때 각별히 주의해야하고, 꼭 사용해야 할때만 사용하기를 바란다.


dispatch_apply
: dispatch_sync와 dispatch_barrier_async를 합쳐놨다고 생각하면 된다.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
     NSLog(@"%zu", index);
});
NSLog(@"done");

//결과
//4 1 0 3 5 2 6 8 9 7 done
//첫 번째 인자값은 횟수이다. 두 번째는 타깃 디스패치 큐이고, 세 번째는 큐에 추가될 작업이고 인자로 index를 받는다.

NSArray의 각 항목에 대해 뭔가를 하기를 원할때 for-loop를 작성할 필요가 없다.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply([array count], queue, ^(size_t index) {
    NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
});

글로벌 디스패치 큐의 배열에 모든 항목에 대한 블록을 실행하는 것은 매우 쉽다.
dispatch_apply 함수는 dispatch_sync 함수처럼 모든 작업의 실행을 기다린다.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(queue, ^{
    //글로벌 디스패치 큐에서 dispatch_apply 함수는 모든 작업이 완료되기를 기다린다.
    dispatch_apply([array count], queue, ^(size_t index) {
        //NSArray 객체의 각 항목에 대해 동시에 뭔가를 한다.
        NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
    });
    // dispatch_apply 함수에 의해 모든 작업이 완료된다.
    
    // 메인 디스패치 큐에서 비동기적으로 실행된다.
    dispatch_async(dispatch_get_main_queue(), ^{
        //userface 업데이트와 같은 것 등을 메인 디스패치 큐에서 실행.
    });
});



dispatch_suspend/dispatch_resume
: 이 함수들은 큐의 실행을 일시 중지하거나 재개한다. 디스패치 큐에 많은 작업을 추가할 때, 때때로 그것들의 모든 것을 추가하는 것을 완료할 때까지 작업의 실행을 원하지 않을 수 있다.(블록이 다른 작업에 의해 영향을 받을 값을 캠쳐할 때 이러한 작업을 원할 수 있다)
이미 실행중인 작업들에는 영향을 주지 않는다. 디스패치 큐에는 있지만 아직 시작하지 않은 작업실행을 방지할 수 있다.

dispatch_suspend(queue);
dispatch_resume(queue);
//사용방법은 간단하다.


Dispatch Semaphore
: dispatch semaphore는 시리얼 디스패치 큐 또는 dispatch_barrier_async 함수보다 작은 부분에 대한 동시성 제어가 필요한 경우 유용하다.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [[NSMutableArray alloc] init];

for (int i = 0; i < 100000; ++i)
{
    dispatch_async(queue, ^{
        [array addObject:[NSNumber numberWithInt:i]];
    });
}
이 소스코드에서 NSMutableArray 클래스는 멀티스레딩을 지원하지 않기 때문에 많은 스레드에서 객체가 업데이트 될 때 크래시가 발생될 수 있다. 이 문제는 전형적인 레이스 컨디션이다. 이런 상황에 디스패치 세마포어를 사용하면 된다.
흔히 멀티스레딩 프로그래밍에서 말하는 세마포어 개수를 다루는 방식과 동일하게 디스패치 세마포어도 흐름 제어를 위한 플래그 개념이다. 플래스는 계속 진행할 수 있을 때 up되고, 진행할 수 없을 때 플래그는 down된다.

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
//세마포어를 생성한다. 인자값은 카운터의 초기값이다. 이름에 "create"가 포함되어 있으므로 ARC가 아닌 환경의 경우 dispatch_release로 그것을 release해야 한다. 또한 dispatch_retaion으로 소유권을 가질수 있다.

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//이 함수는 세마포어의 카운터가 1개 이상이 될 때 까지 기다린다. 1개 이상이 되면 카운터를 감소시키고 함수를 종료한다. 두번째 인자값은 대기시간이다. 함수 리턴값은 dispatch_group_wait함수의 값과 동일하다.

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_semaphore_wait(semaphore, time);

if (result == 0)
{
    //디스패치 세마포어의 카운터는 1개 이상이다.
    //또는 지정된 시간이전에 1개 이상이 된다.
    // 카운터는 자동적으로 하나 감소된다.
    // 여기에서 동시 제어가 필요한 작업을 실행할 수 있다.
}
else
{
    //디스패치 세마포어의 카운터가 0이기 때문에 지정된 시간까지 기다린다.
}

작업이 완료되고 나서는 디스패치 세마포어의 카운터를 1개 증가시키는 dispatch_semaphore_signal 함수를 호출해야 한다.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [NSMutableArray array];

for (int i = 0; i < 100000; ++i)
{
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
       
        [array addObject:[NSNumber numberWithInt:i];
        
        dispatch_semaphore_signal(semaphore);
    });
}

dispatch_release(semaphore);


dispatch_once
: 지정된 작업이 애플리케이션 라이프타임 동안 오직 한  번 실행되는지 확인하는데 사용된다.

static int initialized = NO;

if (initialized == NO)
{
    // 초기화
    initialized = YES;
}

위 코드를 dispatch_once함수로 더 우아하게 구현할 수 있다.

static dispatch_once_t pred;

dispatch_once(&pred, ^{
    // 초기화
});

두 소스코드는 별 차이가 없으나 멀티스레드 환경에서  첫번째 소스코드는 initialized값이 덮여쓰여지는 동시에 읽힐 수도 있는 약간의 기회가 있어서 100프로 안전하지 않다. 그러나 dispatch_once 함수로 구현을 하면 더는 걱정할 필요가 없다.

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

Block의 모든것(1)  (0) 2017.01.02
ARC 규칙  (0) 2017.01.01
GCD(Grand Central Dispatch)(1)  (0) 2016.12.19
Objective-C의 동적 바인딩, 그리고 Message dispather와 Runtime  (0) 2016.12.18
Key-Value Coding(1)  (0) 2016.12.07
Posted by 홍성곤
,

GCD는 작업을 비동기적으로 실행하기 위한 기술 중 하나다. 이 기술은 보통 개발자들이 응용프로그램을 만들 때 직접 작성하는 스레드 관리 코드를 GCD자체가 관리하기 때문에 전통적인 멀티스레드 프로그래밍 방법보다 훨씬 쉽게 멀티 스레딩을 지원할 수 있도록 하였다.

GCD가 만들어지기 전에는 NSObject 인스턴스 메서드인 performSelectorInBackground:withObject, performSelectorOnMainThread등을 사용했다. 
performSelector계열이 NSThread 클래스를 사용한 코드보다는 쉽지만, GCD를 이용한 코드가 확실히 가장 쉽다. 게다가 훨씬 효율적이다.


CPU가 응용프로그램을 실행하는 방식
- 소스코드는 실행되기전 CPU 바이트코드로 변환된다. 응용프로그램은 맥이나 아이폰에 설치될 데이터와 바이트코드를 하나로 묶는다. 운영체제가 바이트 코드를 메모리에 할당하고 응용프로그램에 명시된 특정 주소를 시작으로 CPU가 바이트 코드를 한줄 한줄 실행한다.

하나의 CPU는 한 순간에 하나의 명령을 수행할 수 있다. 즉 한 순간에는 하나의 스레드만 작업할 수 있다는 얘기이다.
그렇다면 응용프로그램은 어떻게 멀티 스레딩을 지원할 수 있을까? 그것은 커널이 일정 간격으로 각 스레드를 스위치시키고, 시스템 콜 같은 OS 이벤트가 발생하면 실행중인 스레드의 실행상태를 각 스레드에 할당된 메모리 블록 중 하나에 저장한다. 즉, 이를 반복해서 CPU가 여러 경로를 실행하는 것처럼 동작한다. 이런 방식을 문맥 전환(context switch)라고 한다.
멀티 스레드 프로그램은 컨텍스트 스위치를 끊임없이 반복한다. 그러므로, 하나의 CPU가 가상으로 여러 스레드를 실행할 수 있게 된다. 여러개의 CPU 코어가 있을 때는 각 코어가 동시에 스레드를 실행할 수 있다. 즉, 실제로 스레드가 동시에 실행될 수 있는 개수가 CPU 코어의 개수이다.


GCD  기본
디스패치 큐(Dispatch Queues)
- 디스패치 큐는 실행할 작업을 저장하는 큐이다. dispatch_async 함수를 사용하여 작업들을 디스패치 큐에 추가할 수 있다.
- 디스패치 큐는 두가지 종류가 있다. 하나는 시리얼 디스패치 큐(serialDispatchQueue)이고 나머지 하나는 콘커런트 디스패치 큐(concurrentDispatchQueue)이다. 시리얼 디스패치 큐는 작업을 큐에 들어온 순서대로 동작시키고 콘커런트 디스패치 큐는 들어온 작업들을 비동기적으로 실행시킨다. 즉, 시리얼 디스패치 큐는 단일 스레드를 사용하고 콘커런트 디스패치 큐는 멀티 스레드를 사용한다.
다만 콘커런트 디스패치 큐는 작업 하나마다 스레드 하나를 할당하는 것이 아니라 커널이 스레드의 갯수를 정해주고 각 스레드가 작업을 분담해서 나눠서 처리한다.

디스패치 큐 얻기
- dispatch_queue_create
: 새로운 디스패치 큐를 생성한다.

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.hsg2510.MySerialDispatchQueue", NULL);
// 디스패치 큐의 이름은 프로그램 내에서 유일해야 되기 때문에 도메인 역순 표기법을 추천한다. 

시리얼 디스패치 큐가 만들어지고 작업이 추가될 때, 시스템은 각각의 시리얼 디스패치 큐를 위해 하나의 스레드를 생성한다. 
디스패치 큐는 Objective-c 객체로 취급되지 않기 때문에 수동으로 릴리스하기 위해 dispatch_release를 호출해 줘야 한다.

dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.hsg2510.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(myConcurrentDispatchQueue, ^{ NSLog(@"hi~") });
dispatch_release(myConcurrentDispatchQueue);
// 디스패치 큐에서 작업이 실행되고 작업이 끝나기를 기다리지 않고 바로 release한다. 괜찮은가? 괜찮다. 작업 블록이 dispatch_async 함수에 의해 디스패치 큐에 추가 되었을 때 작업블록이 dispatch_retain함수에 의해서 디스패치 큐의 소유권을 갖는다. 그리고 블록의 실행이 종료되었을 때, 블록은 dispatch_release함수에 의해서 디스패치 큐가 릴리즈된다.

- 메인 디스패치 큐 / 글로벌 디스패치 큐
: 디스패치 큐를 얻는 다른 방법은 시스템이 이미 제공하고 있는 디스패치 큐를 가져오는 것이다. 시스템은 당신이 생성 못하는 메인 디스패치 큐와 글로벌 디스패치 큐를 갖고있다.
메인 디스패치 큐는 메인 스레드에서 작업을 실행하는 큐이다. 이것은 시리얼 디스패치 큐이고 이 큐에 할당된 작업들은 메인 스레드의 RunLoop에서 실행된다.
글로벌 디스패치 큐는 콘커런트 디스패치 큐이다. 실제로 정말 특별한 경우가 아니라면 dispatch_queue_create함수를 사용하여 콘커런트 디스패치 큐를 생성할 필요가 없다. 이미 시스템은 4개의 글로벌 디스패치 큐가 있고 그것들은 각기 다른 중요도(high, default, low, background)를 가지고 있다. 


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

ARC 규칙  (0) 2017.01.01
GCD(Grand Central Dispatch)(2)  (0) 2016.12.19
Objective-C의 동적 바인딩, 그리고 Message dispather와 Runtime  (0) 2016.12.18
Key-Value Coding(1)  (0) 2016.12.07
Collections의 weak reference  (0) 2016.10.02
Posted by 홍성곤
,

정적 언어(C / C ++)와의 차이

- C / C++은 주로 "컴파일러"에 의존하는 정적인 언어라면, Objective-C는 "런타임"에 의존하는 동적인 언어이다.
- 동적인 언어의 특징은 Dynamic typing, Dynamic binding, Dynamic loading을 지원한다는 것이다.
   1) Dynamic typing : 코드에서 다루는 오브젝트의 type이 무엇인지 실행 도중에 결정됨.
   2) Dynamic binding : 코드상의 구문 statement가 어떤 method를 동작시킬 것인지 실행 도중에 결정됨
   3) Dynamic loading : 실행 도중에 동적으로 실행될 모듈을 메모리에 loading할 수 있음.

*물론, C / C++ 도 Dynamic loading을 지원하며 특히 Dynamic binding은 다형성을 지원하는 객체지향 언어라면 반드시 필요로 하는 기능이기 때문에 C++에서도 핵심적인 부분이다. 하지만 이러한 기능들을 사용하는 메커니즘이나 방법이 objective-c가 훨씬 더 유연하게 사용한다는 것이다.


동적 바인딩?

- 정적 바인딩은 컴파일러 혹은 링커가 프로그램 실행전에 프로그래머가 작성한 method call(또는 message sending)에 대해 실행될 함수를 미리 연결시켜 놓는것이다. 
- 동적 바인딩은 실행할 method call에 대한 함수를 프로그램 실행 도중 Runtime이 동적으로 결정한다는 것이다.


Objective-C의 Object 구조

- isa 포인터와 클래스 Object
: Objective-C의 모든 "인스턴스 Object"들은 isa라는 멤버 변수를 가지고 있다. isa의 타입은 Class타입인데 이것은 Objective-C의 "클래스 Object"를 가리키는 포인터이다. 그리고 id는 "인스턴스 Object"를 가리키는 포인터이다.

typedef struct objc_class* Class
typedef struct objc_object 
{
    Class isa;
} *id;

Objective-C에서는 클래스의 인스턴스 뿐 아니라 클래스 자체도 하나의 Object로서 존재한다. 
인스턴스 Object가 만들어지는 과정을 보면, 클래스 Object의 alloc메서드가 호출되면 인스턴스 Object에 대한 메모리를 할당하고 자신(클래스 Object)을 참조하고 있는 isa포인터를 멤버변수로 세팅한다. 


Dispatch table과 슈퍼 클래스

클래스 Object는 자신의 클래스가 정의하는 모든 method(인스턴스 method, 클래스 method)의 정보를 가지고 있다. 이를 Dispatch table이라 한다. 그리고 클래스 Object는 슈퍼클래스에 대한 포인터 isa를 멤버변수로 가진다.

Runtime은 인스턴스 Object가 메세지를 받으면 인스턴스 Object의 멤버변수 isa를 참조하여 자신의 클래스 Object를 찾는다. 그리고 클래스 Object의 Dispatch table을 참조해서 해당 메세지에 연결된 method 구조체를 참조해서 실제 함수를 실행한다.(method 구조체와 실제 함수 실행에 대한 부분은 추후 설명하겠다.) 만약 메세지에 해당하는 method 구조체를 찾지 못했을 경우 슈퍼클래스 Object를 가리키는 isa를 통해 슈퍼클래스의 Dispatch_table을 검색해서 매칭되는 method 구조체를 찾는다. 계속 적절한 method 구조체를 찾지 못하면 이 과정이 루트클래스까지 반복된다. 



루트 클래스와 메타 클래스

- Cocoa 프로그래밍 환경에서 루트 클래스는 항상 NSObject 또는 NSProxy이다.(대부분 NSObject)
- 클래스 Object도 isa포인트를 가지고 있다. isa포인트는 항상 자신을 생성한 클래스 Object를 가리킨다. 클래스 Object를 생성하는 클래스는 메타 클래스라고 불리는데 이것에 대한 정보는 아직 신경쓰지 않아도 된다.(추후 설명하겠음.)



Selector(SEL), Method, 구현(IMP)의 정체

- Selector는 Method의 이름이라고 생각하면 된다. Method는 Selector와 실제 구현(IMP)을 연결시켜주는 구조체이다.

typedef struct objc_method
{
    SEL method_name; //SEL은 보통 문자열의 포인터
    char *method_types; //인수의 개수와 타입, 반환값에 따라 정해지는 메서드 타입이다.
    IMP method_imp; //함수 포인터
}


- 위 그림에서 보다시피 메세지와 함수는 분리되어 있고 이를 Method 구조체가 연결하는 구조이다. 
- 클래스 Object의 Dispatch table이 이 Method 구조체들을 포함하고 있을것이다.



method 관련 Runtime함수들


- Method method = class_getInstanceMethod(Class class, SEL sel)
: Method 구조체를 얻어내는 함수

- SEL sel = method_getName(Method method)
: SEL를 얻어내는 함수

- IMP imp = method_getImplementation(Method method)
: IMP를 얻어내는 함수

- method_setImplementation(Method method, IMP imp)
: 해당 Method에 IMP를 설정

Selector에 대응되는 IMP변경 코드
void killEmployee(id self, SEL _cmd)
{
    NSLog(@"I'd rather kill you than pay you");
}

method_setImplementation(method, (IMP)killEmployee);



*참조
- http://pole2win.tistory.com/entry/Objective-C-objects
- http://pole2win.tistory.com/entry/Objective-C-dispatch


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

GCD(Grand Central Dispatch)(2)  (0) 2016.12.19
GCD(Grand Central Dispatch)(1)  (0) 2016.12.19
Key-Value Coding(1)  (0) 2016.12.07
Collections의 weak reference  (0) 2016.10.02
NSString과 NSMutableString의 copy  (0) 2016.02.18
Posted by 홍성곤
,