Каждое iOS приложение состоит из одного или более потока. Каждое приложение начинается с одного потока и, затем может создавать еще дополнительные потоки.
Когда приложение создает дополнительный поток, он становится отдельной сущностью в пространстве процесса приложения. Каждый поток имеет свой стек и планируется на исполнение отдельно kernel’ом. Поток может общаться с другими потоками. Все потоки находятся в общем адресном пространстве приложения и делят одну и ту же виртуальную память и имеют те же права доступа что и процесс приложения.
Главный поток приложения имеет существенные отличия от остальных по своему фнукционалу. Он выполняет функцию main приложения и отвечает за обработку событий от пользователя и обновление UI.
Поэтому, если, скажем нам нужно сделать чтото асинхронное или объемное, то желательно это сделать не в основном потоке. Если это делать на главном потоке – приложение перестанет реагировать на пользователя и с большой долей вероятности будет закрыто.
- 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, а делать это в фоновом потоке просто не рекомендуется.
обновляет 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 +.