UICollectionView 单元格图像在 GCD 进入视图时发生变化
Posted
技术标签:
【中文标题】UICollectionView 单元格图像在 GCD 进入视图时发生变化【英文标题】:UICollectionView Cell Image changing as it comes into view with GCD 【发布时间】:2013-05-22 13:43:39 【问题描述】:我需要调整本地存储的大图像(包含在self.optionArray
中)的大小,然后在 collectionView 中显示它。如果我只是显示它,ios 会在我快速滚动时尝试调整图像大小,从而导致与内存相关的崩溃。
在下面的代码中,collectionView 会平滑滚动,但有时如果我滚动得非常快,会显示一个不正确的图像,然后随着滚动减速而更改为正确的图像。为什么不将 cell.cellImage.image
设置为 nil
解决此问题?
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CustomTabBarCell" forIndexPath:indexPath];
cell.cellImage.image = nil;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^
cell.cellImage.image = nil;
UIImage *test = [self.optionArray objectAtIndex:indexPath.row];
UIImage *localImage2 = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
dispatch_sync(dispatch_get_main_queue(), ^
cell.cellImage.image = localImage2
cell.cellTextLabel.text = @"";
[cell setNeedsLayout];
);
);
return cell;
- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
编辑: 我添加了另一个 async 以首先缓存和 nil 并初始化 cell.image。我在最初的快速向下滚动时遇到了同样的问题。但是,在向上滚动时,它现在完美无缺。
我添加了这个:
-(void)createDictionary
for (UIImage *test in self.optionArray)
UIImage *shownImage = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
[localImageDict setObject:shownImage forKey:[NSNumber numberWithInt:[self.optionArray indexOfObject:test]]];
- (void)viewDidLoad
[super viewDidLoad];
if (!localImageDict)
localImageDict = [[NSMutableDictionary alloc]initWithCapacity:self.optionArray.count];
else
[localImageDict removeAllObjects];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^
[self createDictionary];
);
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CustomTabBarCell" forIndexPath:indexPath];
cell.cellImage.image = nil;
cell.cellImage.image = [[UIImage alloc]init];
if ([localImageDict objectForKey:[NSNumber numberWithInt:indexPath.row]])
cell.cellImage.image = [localImageDict objectForKey:[NSNumber numberWithInt:indexPath.row]];
cell.cellTextLabel.text = @"";
else
cell.cellImage.image = nil;
cell.cellImage.image = [[UIImage alloc]init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^
UIImage *test = [self.optionArray objectAtIndex:indexPath.row];
UIImage *shownImage = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
[localImageDict setObject:shownImage forKey:[NSNumber numberWithInt:indexPath.row]];
dispatch_sync(dispatch_get_main_queue(), ^
cell.cellImage.image = shownImage;
cell.cellTextLabel.text = @"";
[cell setNeedsLayout];
);
);
return cell;
【问题讨论】:
【参考方案1】:仔细查看您的代码示例,我可以看到您的内存问题的根源。跳出来的最重要的问题是您似乎将所有图像都保存在一个数组中。这需要大量的内存(我从您需要调整图像大小推断它们必须很大)。
为减少应用的占用空间,您不应维护 UIImage
对象数组。相反,只需维护图像的 URL 或路径数组,然后仅在 UI 需要时动态创建 UIImage
对象(称为延迟加载的过程)。一旦图像离开屏幕,您就可以释放它(UICollectionView
和 UITableView
一样,只要您不保持对图像的强引用,就会为您完成很多清理工作)。
应用程序通常应该只为当前可见的图像维护UIImage
对象。出于性能原因,您可能会缓存这些调整大小的图像(例如,使用 NSCache
),但是当您的内存不足时,缓存将被自动清除。
好消息是您显然已经精通异步处理。无论如何,实现可能如下所示:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CustomTabBarCell" forIndexPath:indexPath];
NSString *filename = [self.filenameArray objectAtIndex:indexPath.row]; // I always use indexPath.item, but if row works, that's great
UIImage *image = [self.thumbnailCache objectForKey:filename]; // you can key this on whatever you want, but the filename works
cell.cellImage.image = image; // this will load cached image if found, or `nil` it if not found
if (image == nil) // we only need to retrieve image if not found in our cache
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^
UIImage *test = [UIImage imageWithContentsOfFile:filename]; // load the image here, now that we know we need it
if (!test)
NSLog(@"%s: unable to load image", __FUNCTION__);
return;
UIImage *localImage2 = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
if (!localImage2)
NSLog(@"%s: unable to convert image", __FUNCTION__);
return;
[self.thumbnailCache setObject:localImage2 forKey:filename]; // save the image to the cache
dispatch_async(dispatch_get_main_queue(), ^ // async is fine; no need to keep this background operation alive, waiting for the main queue to respond
// see if the cell for this indexPath is still onscreen; probably is, but just in case
CustomTabBarCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell)
updateCell.cellImage.image = localImage2
updateCell.cellTextLabel.text = @"";
[updateCell setNeedsLayout];
);
);
return cell;
这假设您定义了thumbnailCache
的类属性,它是对您将在viewDidLoad
或任何地方初始化的NSCache
的强引用。缓存是一种两全其美的方法,将图像加载到内存中以获得最佳性能,但是当您遇到内存压力时它会被释放。
显然,我很高兴地假设“哦,只需将您的图像数组替换为图像文件名数组即可”,而且我知道您可能必须进入代码的许多不同部分才能使其正常工作,但这无疑是你内存消耗的来源。显然,您总是可能遇到其他内存问题(保留周期等),但在您发布的 sn-p 中没有类似的情况。
【讨论】:
所有图片都是本地的,没有从服务器下载。这会改变您的任何建议吗? @Eric 啊,抱歉我没有注意到。在那种情况下,我的第二点不太可能表现出来(尽管理论上可以)。第四点也不是什么大问题。但是第 1 点和第 3 点绝对是相关的,如果您使用缓存,您可能会看到关于第四点的性能略有提升。我鼓励您在尽可能慢的设备上测试您的代码,因为您不会在模拟器或更新的设备上看到其中一些问题。 @Eric 我详细查看了您修改后的答案(我的原始答案是今天早上在路上从我的 iPhone 发送的,当时我没有仔细查看您的代码;抱歉) .无论如何,我认为您仍然会遇到头号内存问题,将所有大图像加载到数组中。您希望将这些文件保存在持久存储中,并进行“延迟加载”,仅在 UI 需要时创建UIImage
对象。而且我担心您的编辑创建另一个数组只会使内存问题变得更糟,不是吗?您希望在数组中存储更少,而不是更多。
就我个人而言,我真的很喜欢您的“让我即时调整它的大小”解决方案。这是正确的想法。我们希望进一步扩展它,不仅可以动态调整大小,还可以动态“加载和调整大小”。您不仅可以使用NSCache
作为缩略图,还可以将它们写入持久存储,从而改进我的解决方案。但最重要的是不要将它们放在一个数组中。
感谢 Rob,这似乎是可用的两全其美(大小和动态负载)。【参考方案2】:
我遇到了类似的问题,但用了不同的方法。
我还遇到了“弹出”问题,因为异步加载的图像在进入时会闪烁,直到最终显示正确的图像。
发生这种情况的一个原因是最初出列的单元格的当前索引路径与您放入其中的图像的索引不匹配。
基本上,如果您从 0-19 快速滚动并且要更新的单元格是 #20,并且您希望它显示图像 #20,但它仍在异步加载图像 3、7、14。
为了防止这种情况,我所做的是跟踪两个索引; #1) 反映单元格实际位置的最新索引路径和 #2) 与实际异步加载的图像相对应的索引(在这种情况下,实际上应该是您传递给 cellforitematindexpath 的索引路径,它被保留为异步进程通过队列工作,因此实际上将是某些图像加载的“旧”数据)。
获取最新索引路径的一种方法可能是创建一个简单的方法,该方法仅返回单元格当前位置的 NSInteger。将此存储为 currentIndex。
然后我放了几个 if 语句,在实际填充图像之前检查两者是否相等。
所以如果 (currentIndex == imageIndex) 然后加载图像。
如果您在这些 if 语句之前放置一个 NSLog(@"CURRENT...%d...IMAGE...%d", currentIndex, imageIndex) ,那么当两者不匹配并且异步调用应该退出。
希望这会有所帮助。
【讨论】:
【参考方案3】:我发现 chuey101 所说的措辞令人困惑。我想出了一个办法,然后意识到 chuey101 的意思是一样的。
如果它可以帮助任何人,图像会因为正在运行的不同线程而闪烁和更改。因此,当您为图像操作生成线程时,它将为特定的单元格生成,例如 c1。但是,最后当您实际将图像加载到单元格中时,它将是您正在查看的当前单元格,即您滚动到的单元格 - 比如说 c2。因此,当您滚动到 c2 时,会在您滚动时生成 c2 线程,每个单元格前一个。据我了解,所有这些线程都将尝试将它们的图像加载到当前单元格 c2 中。所以,你有图像的闪光。
为避免这种情况,您需要实际检查是否正在将所需的图像加载到要加载到的单元格中。因此,在将图像加载到其中之前获取 collectionviewcell indexpath.row (loading_image_into_cell)。此外,在生成线程之前,即在主线程 (image_num_to_load) 中,获取生成线程的单元格。现在,在加载之前,检查这两个数字是否相等。
问题解决了:)
【讨论】:
以上是关于UICollectionView 单元格图像在 GCD 进入视图时发生变化的主要内容,如果未能解决你的问题,请参考以下文章
点击时将图像设置为 UICollectionView 单元格
下载内部图像后调整 UICollectionView 单元格的大小