#ios #multithreading #uitableview #nsoperation
#iOS #многопоточность #uitableview #nsoperation
Вопрос:
Я пытаюсь обеспечить плавную UITableView
прокрутку при просмотре около 700 изображений, которые загружены из Интернета, кэшированы (во внутреннюю память) и отображаются в каждой ячейке таблицы. Мой код пока выглядит нормально с точки зрения производительности прокрутки. Однако я заметил, что иногда, если соединение дрянное или если я прокручиваю очень быстро, ячейка будет отображать неправильное изображение (изображение другой ячейки), возможно, около 1/2 секунды, а затем обновится до изображения, которое она должна отображать.
Пока я подозреваю 2 вещи:
A- У меня может возникнуть проблема с повторным входом с момента, когда мои NSInvocationOperation
вызовы возвращаются в основной поток с [self performSelectorOnMainThread:]
до момента, когда выполняется селектор в главном потоке. Хотя на самом деле я не вижу никаких общих переменных.
B- Какая-то гонка между основным потоком и NSInvocationOperation
? Нравится:
1 вызовы основного потока cacheImageFromURL
2 внутри этого вызова UIImage
охватывает рабочий поток
3 рабочий поток почти завершен и переходит к вызову performSelectorOnMainThread
4 на данный момент рассматриваемая ячейка исключена из очереди для повторного использования, поэтому основной поток снова вызывает cahceImageFromURL
новое изображение.
5 внутри этого вызова UIImage останавливает, NSOPerationQueue
что приводит к завершению предыдущего NSInvocationOperation
потока.
6 НО поток уже вызвал performSelectorOnMainThread
7 таким образом, селектор возбуждается, вызывая загрузку старого изображения.
8 сразу после этого недавно созданный поток завершает выборку нового изображения и вызывает performSelectorOnMainThread
снова, вызывая обновление до правильного изображения.
Если это так, я думаю, мне нужно было бы установить флаг при входе в cacheImageFromURL
метод, чтобы код рабочего потока не вызывался performSelectorOnMainThread
, если внутри уже есть другой поток (основной) cacheImageFromURL
?
Вот мой код для моего UIImageView
подкласса, который использует каждая ячейка в таблице:
@implementation UIImageSmartView
//----------------------------------------------------------------------------------------------------------------------
@synthesize defaultNotFoundImagePath;
//----------------------------------------------------------------------------------------------------------------------
#pragma mark - init
//----------------------------------------------------------------------------------------------------------------------
- (void)dealloc
{
if(!opQueue)
{
[opQueue cancelAllOperations];
[opQueue release];
}
[super dealloc];
}
//----------------------------------------------------------------------------------------------------------------------
#pragma mark - functionality
//----------------------------------------------------------------------------------------------------------------------
- (bool)cacheImageFromURL:(NSString*)imageURL
{
/* If using for the first time, create the thread queue and keep it
around until the object goes out of scope*/
if(!opQueue)
opQueue = [[NSOperationQueue alloc] init];
else
[opQueue cancelAllOperations];
NSString *imageName = [[imageURL pathComponents] lastObject];
NSString* cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *cachedImagePath = [cachePath stringByAppendingPathComponent:imageName];
/* If the image is already cached, load it from the local cache dir.
Else span a thread and go get it from the internets.*/
if([[NSFileManager defaultManager] fileExistsAtPath:cachedImagePath])
[self setImage:[UIImage imageWithContentsOfFile:cachedImagePath]];
else
{
[self setImage:[UIImage imageWithContentsOfFile:self.defaultNotFoundImagePath]];
NSMutableArray *payload = [NSMutableArray arrayWithObjects:imageURL, cachedImagePath, nil];
/* Dispatch thread*/
concurrentOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(loadURI:) object:payload];
[opQueue addOperation: concurrentOp];
[concurrentOp release];
}
return YES;
}
//----------------------------------------------------------------------------------------------------------------------
/* Thread code*/
-(void)loadURI:(id)package
{
NSArray *payload = (NSArray*)package;
NSString *imageURL = [payload objectAtIndex:0];
NSString *cachedImagePath = [payload objectAtIndex:2];
/* Try fetching the image from the internets.
If we got it, write it to disk. If fail, set the path to the not found again.*/
UIImage *newThumbnail = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]]];
if(!newThumbnail)
cachedImagePath = defaultNotFoundImagePath;
else
[UIImagePNGRepresentation(newThumbnail) writeToFile:cachedImagePath atomically:YES];
/* Call to the main thread - load the image from the cache directory
at this point it'll be the recently downloaded one or the NOT FOUND one.*/
[self performSelectorOnMainThread:@selector(updateImage:) withObject:cachedImagePath waitUntilDone:NO];
}
//----------------------------------------------------------------------------------------------------------------------
- (void)updateImage:(NSString*)cachedImagePath
{
[self setImage:[UIImage imageWithContentsOfFile:cachedImagePath]];
}
//----------------------------------------------------------------------------------------------------------------------
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
@end
И способ использования этого UIImage находится в контексте cellForRowAtIndexPath , вот так:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIImageSmartView *cachedImage;
// and some other stuff...
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.selectionStyle = UITableViewCellSelectionStyleGray;
// some labels and tags stuff..
cachedImage = [[UIImageSmartView alloc] initWithFrame:CGRectMake(5, 5, 57, 80)];
cachedImage.contentMode = UIViewContentModeCenter;
cachedImage.defaultNotFoundImagePath = [[NSBundle mainBundle] pathForResource:@"DefaultNotFound" ofType:@"png"];
cachedImage.tag = PHOTO_TAG;
[cell.contentView addSubview:cachedImage];
[cell.contentView addSubview:mainLabel];
[cell.contentView addSubview:secondLabel];
}
else
{
cachedImage = (UIImageSmartView*)[cell.contentView viewWithTag:PHOTO_TAG];
mainLabel = (UILabel*)[cell.contentView viewWithTag:MAINLABEL_TAG];
}
// Configure the cell...
NSString *ImageName = [[[self.dbData objectAtIndex:indexPath.row] objectAtIndex:2]
stringByReplacingOccurrencesOfString:@".jpg"
withString:@"@57X80.png"];
NSString *imageURL = [NSString stringWithFormat:@"www.aServerAddress.com/%@/thumbnail5780/%@",
self.referencingTable,
ImageName];
[cachedImage cacheImageFromURL:imageURL];
mainLabel.text = [[self.dbData objectAtIndex:indexPath.row] objectAtIndex:0];
return cell;
}
Ответ №1:
Проблема заключается в повторном использовании ячейки, одна ячейка запрашивает различные изображения одновременно и отображается при загрузке каждого из них, я знаю, что вы отменяете очередь операций, но поскольку вызывающий объект обработки синхронен, операция продолжает выполнение. Я предлагаю попытаться сохранить indexPath запроса и сопоставить его с индексным путем ячейки, прежде чем устанавливать UIImage.
Комментарии:
1. пытаюсь понять ваше предложение. Вы имеете в виду, сохранить indexPath при вводе cacheImageFromURL: , затем передать его в NSInvocationOperation и попросить NSInvocation передать его, в свою очередь, обратно в код основного потока, чтобы при updateImage, если текущий путь к индексу не совпадает с переданным, не загружать «обновленное» изображение?
2. было бы лучше просто забыть весь NSOperationQueue и, возможно, вместо этого перейти к обычной NSOperation? значит, несколько запросов не могут встать в очередь и не отменяются вовремя?
3. Хммм, позвольте мне объяснить, попробуйте добавить свойство с именем indexPath к вашему объекту UIImageSmartView и устанавливать его каждый раз в cellForRowAtIndexPath, создайте пользовательское синхронное соединение NSRLConnection (подкласс) со свойством indexPath, и когда вы выполняете запрос (NSOperationInvocation), установите для него текущее свойство indexPath UIImageSmartView, таким образом, у вас будет уникальное соединение для каждого индекса. Перед установкой изображения проверьте, совпадает ли indexPath UIImageSmartView с indexPath пользовательского NSURLConnection.
Ответ №2:
D33pN16h7 прав в том, что проблема заключалась в повторном использовании ячейки. Однако, вместо того, чтобы пытаться сделать indexPath потокобезопасным с помощью NSURLConnection, я решил переопределить все это, переместив NSOperationQueue в код UITableViewController и сделав параллельный класс ImageView фактически надлежащим подклассом NSOperation (поскольку я использовал NSOperationInvocation в первую очередь, чтобы попытаться избежать полноценного подкласса NSOperation ).
Итак, теперь контроллер таблицы управляет своим собственным NSOperationQueue, операции являются подклассами NSOperation, и я могу отменить их из кода контроллера таблицы, когда представление таблицы прокручивается мимо них. И все работает быстро и красиво.