티스토리 뷰

iOS 개발/ObjectiveC

GCD 기본

beankhan 2016. 7. 24. 14:03

Dispatch Queue

디스패치 큐는 실행할 작업을 저장하는 큐이다.
애플리케이션 프로그래머들은 블록 구문으로 작업을 작성할 수 있고
dispatch_async 함수를 사용하여 그것들을 디스패치 큐에 추가할 수 있다.


시리얼 디스패치 큐

시리얼 디스패치 큐는 작업이 끝나기를 기다렸다가 다음 작업이 실행된다.
한 번에 딱 하나의 작업만 실행된다.
단일 스레드를 사용한다.


콘커런트 디스패치 큐

앞의 작업이 끝날 때를 기다리지 않는다. 
동시에 여러 개의 작업이 실행된다.
동시에 실행되는 작업의 수는 현재 시스템의 상태 (CPU 코어 수, CPU 사용 레벨) 에 따라 달라진다.
멀티 스레드를 사용한다.

XNU 커널은 스레드 개수를 결정하고 작업을 실행하는 스레드를 생성한다.
작업이 끝나거나 실행 중인 작업의 수가 줄어들 때 XNU 커널은 필요없는 스레드를 종료한다.
콘커런트 디스패치 큐를 사용하여 XNU 커널은 동시에 작업을 실행하는데 완벽하게 멀티스레드를 관리한다.

콘커런트 디스패치 큐를 사용하는 경우 실행 순서는 작업 자체, 시스템 상태에 따라 달라진다.
순서가 중요하거나 작업을 동시에 실행하지 말아야할 때는
시리얼 디스패치 큐를 사용해야한다.


Dispatch Queue 얻기

dispatch_queue_create, 메인 디스패치 큐 / 글로벌 디스패치 큐로 총 세가지가 있다.


dispatch_queue_create

디스패치 큐를 새로 생성하기 위한 함수이다.


- Serial Dispatch Queue
: dispatch_queue_t serialQueue = dispatch_queue_create("com.example.gcd.MySerialQueue", NULL);

- Concurrent Dispatch Queue
: dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.gcd.MyConcureentQueue", DISPATCH_QUEUE_CONCURRENT);

시리얼 디스패치 큐가 만들어지고 작업이 추가될 때,
시스템은 각각의 시리얼 디스패치 큐를 위해 하나의 스레드를 생성한다.
즉, 2000개의 시리얼 큐를 생성하면 2000개의 스레드가 생성된다.
많은 스레드는 많은 메모리를 소모하고 많은 콘텍스트 스위치는 시스템이 느려지게 하는 원인이다.

시리얼 디스패치 큐의 수는 필요한 것과 같은 수여야한다.
예를 들어, 데이터베이스를 업데이트 할 때 각각의 테이블에 대해 하나의 시리얼 디스패치 큐를 생성해야한다. (??????, 알아보기!!)
파일을 업데이트할 때, 파일 또는 각각의 분리된 파일 블록에 대해 시리얼 디스패치 큐를 생성해야한다.

만약 작업이 일관성 없은 데이터로 문제를 발생하지 않고 동시에 실행하기 원한다면
콘커런트 디스패치 큐를 사용해야한다.

생성된 디스패치 큐는 블록과 달리 Objective-C 객체로 취급되지 않기 때문에
사용이 끝나면 수동으로 릴리즈를 해줘야만 한다.

dispatch_release(serialQueue);
dispatch_release(concurrentQueue);

release 와 함께 dispatch_retain 도 존재한다.

dispatch_queue_t concurrentDispatchQueue = dispatch_queue_create("com.example.gcd.MyConcurrentDispatchQueue", DISPATCH_QEUEU_CONCURRENT);
dispatch_async(concurrentDispatchQueue, ^{
     NSLog(@"block on concurrentQueue");
});
dispatch_release(concurrentQueue);

콘커런트 디스패치 큐는 블록이 dispatch_async 로 추가되고 나서 dispatch_release 에 의해 릴리스된다.
하지만 안전하게 작동하는데, 블록이 dispatch_async 함수에 의해 디스패치 큐에 추가되었을 때
블록은 dispatch_retain 함수에 의해서 디스패치 큐의 소유권을 갖는다.
블록의 실행이 종료되었을 때 블록은 dispatch_release 함수에 의해 디스패치 큐가 릴리즈된다.

"create" 로 시작하는 GCD API instance 를 얻었을 때 필요가 없어지면 dispatch_release 를 사용하여 릴리즈 해야한다.


메인 디스패치 큐 / 글로벌 디스패치 큐

디스패치 큐를 얻는 다른 방법은 시스템이 이미 제공한 디스패치 큐를 가져오는 것이다.
메인 스레드는 하나만 존재하듯이 메인 디스패치 큐는 시리얼 디스패치 큐이다.
메인 디스패치 큐에 할당된 작업들은 메인 스레드의 런루프에서 실행된다.

글로벌 디스패치 큐애플리케이션 어디에서나 사용할 수 있는 콘커런트 디스패치 큐이다.
만약, 특별한 이유가 없다면 대부분의 케이스에서는 콘커런트 디스패치 큐를 생성할 필요가 없다.
글로벌 디스패치 큐를 이용하면 된다.

4개의 글로벌 디스패치 큐가 있고 그것들은 각기 다른 중요도(high, default, low, background) 를 가진다.
XNU 커널은 글로벌 디스패치 큐를 위한 스레드를 관리하고
각각의 스레드가 해당 글로벌 디스패치 큐의 중요도와 동일한 중요도를 갖도록 설정한다.
XNU 커널은 스레드의 실시간성을 보장하지 않는다. 중요도는 단지 참고용으로 사용할 뿐이다.

- Main Dispatch Queue
: dispatch_queue_t mainQueue = dispatch_get_main_queue();

- Global Dispatch Queue
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

메인 디스패치 큐, 글로벌 디스패치 큐를 retain, release 를 해도 아무런 일이 발생하지 않는다.





디스패치 큐 제어하기

dispatch_set_target_queue

디스패치 큐가 dispatch_queue_create 함수를 사용해서 생성될 때
스레드의 중요도는 그것의 글로벌 디스패치의 기본 중요도가 같다.
이를 수정하기 위해서 사용할 수 있다.

dispatch_queue_t serialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialQueue", NULL);
dispatch_queue_t globalDispatchBackgroundQueue = dispatch_get_global_queue(DISPATCH_QEUEU_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(serialDispatchQueue, globalDispatchBackgroundQueue);


dispatch_after

dispatch_after 는 큐에 작업 시작 타이밍을 설정하는 것이다.

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSIC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
     NSLog(@"waited at least three seconds");
});

ull (unsigned long long) 은 C언어 문법에서 지정된 타입이다.
dispatch_after 는 지정된 시간 후에 작업을 실행하지 않는다.
시간이 지나면 디스패치 큐에 작업을 추가한다.

만약, RunLoop 가 1/60 초 간격으로 실행하는 경우, 블록은 3초 + 1/60 초 후 사이에서 실행된다.
많은 작업이 메인 디스패치 큐에 추가되어있거나 메인 스레드에 지연이 있으면 그 이후에도 실행될 수 있다.

첫번째 인자 값은 dispatch_time_t  이다.
두번째 인자 값은 queue, 
세번째 인자 값은 실행될 블록이다.

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
위 time 은 나노 초 단위로 시간을 만든다. (NSEC_PER_SEC)
밀리 초 단위로 만드려면 NSEC_PER_MSEC 를 사용하면된다.
dispatch_time 함수는 상대적 시간을 생성하는데 주로 사용한다.
대조적으로 dispatch_walltime 함수는 절대 시간을 생성할 수 있는데, 아래와 같이 만들 수 있다.

dispatch_time_t getDispatchTimeByDate(NSDate *date) {
     NSTimeInterval interval;
     double second, subsecond;
     struct timespec time;
     dispatch_time_t milestone;

     interval = [date timeIntervalSince1970];
     subsecond = modf(interval, &second);
     time.tv_sec = second;
     time.tv_nsec = subsecond * NSEC_PER_SEC;
     milestone = dispatch_walltime(&time, 0);

     return milestone;
}


디스패치 그룹

디스패치 큐의 모든 작업이 종료되고 어떤 것을 완료하기 위해서 작업의 시작을 원할 수도 있다.
모든 작업이 하나의 시리얼 디스패치 큐에 있을 때 단지 큐의 마지막에 완료 작업을 추가하면 된다.
콘커런트 디스패치 큐 또는 멀티 디스패치 큐를 사용하면 복잡한 것 처럼 보인다.
이런 경우 디스패치 그룹을 사용할 수 있다.

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(@"1")});
dispatch_group_async(group, queue, ^{NSLog(@"2")});
dispatch_group_async(group, queue, ^{NSLog(@"3")});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"done")});
dispatch_release(group);
2
3
1
done

콘커런트 디스패치 큐인 글로벌 디스패치 큐에 있기 때문에
작업의 실행 순서는 일정하지 않다.
작업은 동시에 여러개의 스레드에서 실행된다.

디스패치 그룹은 디스패치 큐의 타입에 관계없이 완료할 작업을 모니터 할 수 있다.

모든 작업이 완료되었음을 감지하면, 마지막 작업 ("done") 은 디스패치 큐에 추가된다.

dispatch_group_async 는 dispatch_async 와 같이 큐에 블록을 추가한다.
dispatch_group_notify 는 디스패치 큐에 추가할 수 있는 블록을 지정한다.
블록은 디스패치 그룹에 모든 작업이 완료될 때 실행된다.

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

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(@"1")});
dispatch_group_async(group, queue, ^{NSLog(@"2")});
dispatch_group_async(group, queue, ^{NSLog(@"3")});

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);

dispatch_group_wait 의 두번째 인자값은 대기 시간을 지정하는 일시적 중단이다. 값은 dispatch_time_t 이다.
예를 들면, 
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);
if (result == 0) {
     // 디스패치 그룹이 연관된 모든 작업이 종료됨
} else {
     // 디스패치 그룹과 연관된 몇몇 작업들이 아직 동작중
}

wait 는 dispatch_group_wait 함수가 호출될 때 이 함수에서 리턴을 하지 않는다는 것을 의미한다.
dispatch_group_wait 함수를 실행하는 동안 스레드는 중지된다.
dispatch_group_wait 함수에 지정된 시간이 통과하는 동안에
디스패치 그룹과 연관된 모든 작업이 완료할 때까지
dispatch_group_wait 함수를 실행하는 스레드는 중지된다.

long result = dispatch_group_wait(group, DISPATCH_TIME_NOW);
로 메인 스레드에서 실행되는 여러 RunLoop 의 각각 루프에서 모든 작업이 끝났는지 확인하는 용도로 사용할 수 있다.
하지만 이것보다는 dispatch_group_notify 를 사용해서 코드를 좀더 우아하게 관리할 수 있다.


dispatch_barrier_async

dispatch_barrier_async  는 큐에 다른 작업을 기다리는 함수다.
레이스 컨디션을 피하기 위해서 시리얼 디스패치 큐를 사용할 수 있다.

읽기 작업은 콘커런트 디스패치 큐에 추가되어야하고
업데이트 작업이 실행하지 않는 동안만 업데이트 작업은 시리얼 디스패치 큐에 있어야한다.
읽기 작업은 업데이트 작업이 끝나지 않을 때까지 시작되지 않도록 해야한다.

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.ForBarrier", 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_async(queue, blk5_for_reading);
dispatch_release(queue);

blk3, blk4 사이의 데이터를 쓰는 것을 가정해보면
blk4 와 나중의 작업은 업데이트 된 데이터를 읽어야한다.

이때 dispatch_async 로 쓰기 작업을 했을 때 예기치 않게
쓰기 작업 전에 추가된 작업이 업데이트 된 데이터를 읽을 수도 있다.

dispatch_barrier_async 함수를 사용하면 큐의 모든 작업이 완료되는 시간에
디스패치 큐에 태스크를 추가할 수 있다. 
dispatch_barrier_async 함수로 추가된 작업이 종료될 때 
콘커런트 디스패치 큐는 정상으로 돌아갈 것이다. (dispatch_barrier_async 로 쓰기 작업이 진행된 후 큐에 async 작업이 추가될 것이다.)

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.ForBarrier", 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_barrier_async(queue, blk_for_writing);
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_release(queue);


dispatch_sync

dispatch_sync 는 작업이 추가되는 것을 기다리고 있다.
dispatch_queue 에 동기적으로 블록을 추가한다.
dispatch_sync 함수는 추가된 블록이 종료되기를 기다린다.

wait 는 현재 스레드가 중지되었다는 것을 의미한다.
예를 들면 메인 디스패치 큐에서 다른 스레드의 글로벌 디스패치 큐에
실행된 작업의 결과를 사용하기 원할 수 있다. 이 때 sync 를 사용한다.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{ NSLog(@"heavy task")});

dispatch_sync 함수가 호출된 후에는 지정된 작업이 끝날 때까지 함수는 리턴을 하지 않는다.
dispatch_group_wait 함수의 간단한 버전과 같다.


dispatch_sync 를 사용할 때는 데드락에 유의해야한다.

1. 
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{ NSLog(@"Hello?") });
이 코드는 메인 디스패치 큐에 블록을 추가한다.
블록은 메인 스레드에서 실행될 것이다. 동시에, 블록이 종료되기를 기다린다. (메인 스레드의 작업이 종료되어야 블록이 실행될 수 있다.)
메인 스레드에서 실행되기 때문에 메인 디스패치 큐의 블록이 절대로 실행되지 않는다.

2. 
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
     dispatch_sync(queue, ^{ NSLog(@"Hello?"); });
});

메인 디스패치 큐에서 실행되고 있는 블록은
또한 메인 디스패치 큐에서 실행될 다른 블록이 종료하기를 기다린다.

3. 
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.Test", NULL); //SerialDispatchQueue
dispatch_async(queue, ^{
     dispatch_sync(queue, ^{ NSLog(@"Hello?"); });
});

dispatch_barrier_sync 도 존재하는데, 디스패치 큐의 모든 작업이 완료된 후 디스패치 큐에 지정된 작업이 추가된다.
dispatch_sync 처럼 지정된 작업이 완료되기를 기다린다.
작업이 완료되기를 기다리는 dispatch_sync 와 같은 동기 API 들을 사용할 때 동기 API 들을 사용하는 이유를 물어야만 한다.


dispatch_apply

dispatch_apply 는 여러 번에 디스패치 큐에 블록을 추가하는데 사용된다.
그리고 모든 작업이 완료될 때까지 기다린다.

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

dispatch_apply 는 모든 작업이 완료되길 기다리기 때문에 done은 항상 마지막이여야만 한다.

첫번째 인자값은 횟수이다.
두번째는 타깃 디스패치 큐이고,
세번째는 큐에 추가될 작업이다.
이 예에서 블록은 여러 번 추가되기 때문에 세번째 인자값에서 블록은 각각의 블록을 구별한다.

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


dispatch_suspend / dispatch_resume

이 함수들은 큐의 실행을 일시 중지하거나 재개한다.
디스패치 큐에 많은 작업을 추가할 때 때때로 그것들의 모든 것을 추가하는 것을
완료할 때까지 작업 실행을 원치 않을 수 있다. (블록이 따른 작업에 의해 영향을 받을 때)
이때 일시적으로 중지 (suspend) 할 수 있고 재개 (resume) 할 수 있다.

이미 실행중인 어떤 작업에는 영향을 주지 않는다.
디스패치 큐에는 있지만 아직 시작하지 않는 작업의 시작을 방지할 수 있다.

dispatch_suspend(queue);
dispatch_resume(queue);


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 클래스의 객체가 글로벌 디스패치 큐에서 업데이트 된다.
NSMutableArray 클래스는 멀티스레딩을 지원하지 않기 때문에
많은 스레드에서 객체가 업데이트 될 때 객체는 손상될 수 있다.

이런 상황에서 디스패치 세마포어를 사용하면 된다.
디스패치 세마포어는 객체보다 더 작은 단위로 사용하게 된다.
플래그는 계속 진행할 수 있을 때 up 되고, 진행할 수 없을 때 플래그는 down 된다.

카운터가 0 일 때 실행을 기다린다.
카운터가 0 이상일 때 카운터를 감소 후 유지한다.

dispatch_semaphore_t semahore = dispatch_semaphore_create(1);
인자값은 카운터의 초기값이다.
함수명에 create 가 있으므로 디스패치 큐 또는 디스패치 그룹에서처럼 dispatch_release 를 해줘야한다.
dispatch_retain 함수를 호출하여 소유권을 가질수도 있다.

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
wait 함수는 디스패치 세마포어의 카운터가 1개 이상이 될 때까지 기다린다.
카운터가 1개 이상이 될 때나 카운터가 그것을 기다릴 때 1개 이상이 되면
카운터를 감소시키고 dispatch_semaphore_wait 함수에서 리턴한다.
두번 째 인자값은 dispatch_wait 에서 대기하는 시간을 지정한다.
dispatch_semaphore_wait 의 리턴값은 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) {
     //dispatch_semaphore 의 카운터는 1개 이상이다. 또는 지정된 time 이전에 1개 이상이 된다.
     //카운터는 자동적으로 하나 감소된다.
     //여기서 동시 제어가 필요한 작업을 실행할 수 있다.
} else {
     //디스패치 세마포어의 카운터가 0이기 때문에 지정된 시간까지 기다린다.
}

dispatch_semaphore_wait 함수가 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 alloc] init];
for (int i = 0; i < 100000; i ++) {
     dispatch_async(queue, ^{
          dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
          //디스패치 세마포어 카운터가 1개 이상 있기 때문에 카운터는 1개 감소하고
          //프로그램 흐름은 dispatch_semaphore_wait 함수에서 리턴했다.
          //카운터는 여기서 항상 0이다.
          //오직 한개의 스레드만 NSMutableArray 클래스 객체에 액세스 할 수 있기 때문에 안전하게 업데이트 할 수 있다.
          [array addObject:[NSNumber numberWithInt:i]];

          //콘커런트 제어를 필요하는 작업이 완료되었기 때문에 dispatch_semaphore_signal 함수를 호출한다.
          //일부 스레드가 dispatch_semaphore_wait 에서 증가된 dispatch_semaphore 카운터를 기다리면 첫 번째 스레드가 실행된다.
          dispatch_semaphore_signal(semaphore);
     });
}
dispatch_release(semaphore);


dispatch_once

dispatch_once 는 thread safe 하다.
singleton pattern, singleton 객체를 생성할 때 유용하다.


dispatch I/O

대용량의 파일을 로드할 때, 글로벌 디스패치 큐를 사용하여 동시에 작은 블록으로 로드하는 경우가 빠르다고 생각할 수 있다.
하나의 스레드에서 로드하는 것보다 동시에 로드하는 것이 빠를 수도 있다.
이것은 디스패치 I/O 와 Dispatch Data 를 사용할 수 있다.
dispatch I/O 로 파일을 읽거나 쓸 때에 하나의 파일은
글로벌 디스패치 큐에 액세스 할 수 있는 특정 크기로 나뉜다.

dispatch_io_create 함수는 디스패치 I/O 를 생성한다.
dispatch_io_set_low_water 함수는 각각의 읽기 사이즈를 설정한다.
dispatch_io_read 함수는 글로벌 디스패치 큐에서 읽기를 시작한다.
분할된 데이터 블록 중 하나를 읽을 때마다 디스패치 데이터는 dispatch_io_read 에 읽기 완료를 위한 콜백처럼 세팅된 블록의 파라미터 값으로 전달된다.
그래서 블록은 디스패치 데이터를 스캔 또는 병합할 수 있다.
만약, 기존 방법보다 조금 더 빨리 파일을 읽고 싶을 때는 디스패치 I/O 를 사용하면 된다.



요약

- 동기적 블록과 비동기적 블록 실행하는 방법
- 레이스 컨디션을 피하는 방법






참고자료

http://padgom.tistory.com/entry/iOS-%EA%B8%B0%EB%B3%B8-GCDGrand-Central-Dispatch-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
http://www.letmecompile.com/gcd-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC/


'iOS 개발 > ObjectiveC' 카테고리의 다른 글

delegate, block 에서의 Retain Cycle (ARC)  (0) 2016.11.09
NSArray, NSMutableArray, NSString, NSNumber  (0) 2016.07.24
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함