Каждое iOS приложение состоит из одного или более потока. Каждое приложение начинается с одного потока и, затем может создавать еще дополнительные потоки.

Когда приложение создает дополнительный поток, он становится отдельной сущностью в пространстве процесса приложения. Каждый поток имеет свой стек и планируется на исполнение отдельно kernel’ом. Поток может общаться с другими потоками. Все потоки находятся в общем адресном пространстве приложения и делят одну и ту же виртуальную память и имеют те же права доступа что и процесс приложения.

Главный поток приложения имеет существенные отличия от остальных  по своему фнукционалу. Он выполняет функцию main приложения и отвечает за обработку событий от пользователя и обновление UI.

Поэтому, если, скажем нам нужно сделать чтото асинхронное или объемное, то желательно это сделать не в основном потоке. Если это делать на главном потоке – приложение перестанет реагировать на пользователя и с большой долей вероятности будет закрыто.

Теперь рассмотрим варианты многопоточности в iOS.

  • pthreads. Это самый древний и самый трудоемкий вариант. Расшифровывается ка Posix THREADS. Этот вариант, в отличие от остальных, кросплатформенный, но ИМХО, он актуален только для поддержки старых и портирования уже написанных приложений.
  • NSThread. Этот вариант проще чем pthread, но все же предполагает ручное создание потоков и управление их жизненным циклом. Его тоже рассматривать не будем.
  • — (void)performSelectorInBackground : (SEL)aSelector withObject:(id)arg. Это один из самых простых и распространенных вариантов. Он требует только указать метод, который будет исполнять этот поток.
  • NSOperationQueue. Это более новый метод. Смысл заключается в создании объекта NSOperation, который будет выполнять нужную задачу и помещения его в очередь NSOperationQueue. И очередь сама будет осуществлять выполнение операций в зависимости от указанных приоритетов, зависимостей операций,  синхронности/асинхронности.
  • GCD (Grand Central Dispatch…It’s Grand, and it’s Central.…). Это довольно новая технология для iOS, представлена в iOS 4.0, и использует для работы особую cи-структуру – блок (block, closure, lambda).

performSelectorInBackground

Рассмотрим один пример но на разных технологиях. Пример возьмем живой и часто встречающийся – загрузка контента UITableView  из сети. Причем в каждой ячейке таблицы – картинка. Для этого будем грузить xml feed (как именно – рассматривать не будем), парсить его с помощью GDataXMLNode  из библиотеки Google. И потом, подгружать для каждой ячейки иконку по необходимости. Кешированием пренебрежем. Рассмотрим первым вариант подгрузки картинки с помощью performSelectorInBackground.

Как известно, UITableView получает данные из datasource. Соответственно основной код будет находиться в

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

Приведем текст метода:

// Customize the appearance of table view cells.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
//cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
// Configure the cell.
//load image async using performSelectorInBackground
cell.imageView.hidden = YES;
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
cell.textLabel.text = [self.titles objectAtIndex:indexPath.row] ;
NSURL* imgUrl = [NSURL URLWithString:[self.thumbs objectAtIndex:indexPath.row]];
NSDictionary* params = [NSDictionary dictionaryWithObjectsAndKeys:imgUrl,@"url",cell,@"cell", nil];
[self performSelectorInBackground:@selector(loadImageWithParams:) withObject:params];//стартуем новый поток
return cell;
}

Вспомогательные методы. Первый выполняется на фоновом потоке. Второй вызывается из первого, но исполняется на главном потоке, потому что он

обновляет UI, а делать это в фоновом потоке просто не рекомендуется.

-(void)loadImageWithParams:(NSDictionary*)params{

NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];//обязательно нужен пул, т.к. новый поток который мы создали сами!
NSURL* url = [params objectForKey:@"url"];
UITableViewCell* cell = [params objectForKey:@"cell"];
NSLog(@"Start loading url:%@ for cell:%@",url, cell);
UIImage* thumb = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
NSDictionary* backParams = [NSDictionary dictionaryWithObjectsAndKeys:cell,@"cell",thumb,@"thumb", nil];
[self performSelectorOnMainThread:@selector(setImage:) withObject:backParams waitUntilDone:YES];//обновление UI только на главном потоке
[pool release];
}
-(void)setImage:(NSDictionary*)params{
UITableViewCell* cell = [params objectForKey:@"cell"];
UIImage* thumb = [params objectForKey:@"thumb"];
[cell.imageView setImage:thumb];
cell.imageView.hidden = NO;
[cell setNeedsLayout];//seems to be apple bug - not watching for image changes!
}

NSOperationQueue

Дальше приведем вариант с NSOperationQueue, так как он наиболее близок к performSelector. В этом варианте даже используются те же методы для загрузки.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
//cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
// Configure the cell.
//load image async using NSOperationQueue
cell.imageView.hidden = YES;
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
cell.textLabel.text = [self.titles objectAtIndex:indexPath.row] ;
NSURL* imgUrl = [NSURL URLWithString:[self.thumbs objectAtIndex:indexPath.row]];
NSDictionary* params = [NSDictionary dictionaryWithObjectsAndKeys:imgUrl,@"url",cell,@"cell", nil];
NSOperation* loadImgOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(loadImageWithParams:) object:params];
[self.queue addOperation:loadImgOp];
[loadImgOp release];
return cell;
}

GCD

Теперь рассмотрим код делающий то же самое, но реализованный с помощью GCD

#pragma mark GCD

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
//cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
// Configure the cell.
//load image async using GCD
cell.imageView.hidden = YES;
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
cell.textLabel.text = [self.titles objectAtIndex:indexPath.row] ;
NSURL* imgUrl = [NSURL URLWithString:[self.thumbs objectAtIndex:indexPath.row]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"Start loading url:%@ for cell:%@",imgUrl, cell);
UIImage* thumb = [UIImage imageWithData:[NSData dataWithContentsOfURL:imgUrl]];
dispatch_sync(dispatch_get_main_queue(), ^{
[cell.imageView setImage:thumb];
cell.imageView.hidden = NO;
[cell setNeedsLayout];//seems to be apple bug - not watching for image changes!
});
});
return cell;
}

Как видим, этот метод намного короче и понятнее чем 2 предыдущих.

Подведем итоги

Любое мало-мальское прилоежние для мобильного телефона нуждается в асинхронных задачах, наиболее явной из которых является взаимодействие с удаленными серверами. На данный момент существует несколько вариантов реализации многопоточности в iOS.

  • Самый простой и понятный «сходу» — performSelectorInBackground. Он подходит для простых задач. Его недостатки — нужно упаковывать все параметры для передачи, и бедные возможности по управлению очередностью, количеством одновременных задач, их приоритетом. Скажем, для данного примера загрузка будет происходить в 16ти потоках, т.к. на каждый вызов performSelectorInBackground будет создаваться отдельный поток. При быстром скроллировании большой таблицы можно довести число потоков до очень большой величины.
  • NSOperationQueue в этом смысле гораздо гибче и удобнее. Там можно для каждой очереди настраивать приоритет и количество одновременно выполняющихся операций. NSOperationQueue самостоятельно создает и поддерживает пул потоков, в которых исполняются NSOperation. Так же NSOperation предоставляет возможность отменять операции, приостанавливать всю очередь, запускать ее  снова и много чего прочего:)
  • И третий вариант — GCD. Визуально — он самый короткий и простой в реализации. Он возоможен с использованием блоков. Этот подход тоже очень гибкий(хотя отменять блок поставленный в очередь нельзя стандартными способами). В GCD можно настраивать приоритеты, блоки захватывают переменные из окружения блока. В общем тоже много разных вкусностей.

    В общем, что использовать — выбор каждого. Главное, что есть из чего выбирать:)

Готовый проект можно взять тут. Для включения конкретного варианта нужно задефайнить константу: PERFORM_SELECTOR_VARIANT, GCD_VARIANT или NSOPERATION_VARIANT. Проект создавался в XCode 4.2. Для пользования gcd нужно iOS 4.0 +.

Материал предоставлен компанией Softeq Development, FLLC

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *