UICollectionView Cell Image随着GCD进入视图而发生变化

时间:2013-05-22 13:43:39

标签: iphone ios ipad grand-central-dispatch uicollectionview

我需要调整大型本地存储的图像(包含在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;

3 个答案:

答案 0 :(得分:6)

仔细查看您的代码示例,我可以看到您的内存问题的根源。跳出来的最重要的问题是您似乎将所有图像保存在一个数组中。这需要非常大的内存(我推断你需要调整它们必须大的图像)。

要减少应用的占用空间,您不应该维护一组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的类属性,该属性是NSCache的强引用,您将在viewDidLoad或任何地方初始化。缓存是一种充分利用两个世界的方法,在内存中加载图像以获得最佳性能,但是当您遇到内存压力时它将被释放。

很明显,我很乐意假设“哦,只需用一系列图像文件名替换你的图像阵列”,我知道你可能需要进入代码的一些不同部分才能使其工作,但这无疑是你记忆消耗的来源。显然,你总是会遇到其他内存问题(保留周期等),但是在你发布的代码片段中没有类似内容。

答案 1 :(得分:1)

我遇到了类似的问题,但是采取了不同的方式。

我也遇到了“pop-in”的问题,因为加载异步的图像在进入时闪烁,直到最后显示正确的图像。

发生这种情况的一个原因是,最初出列的单元格的当前索引路径与您放入其中的图像的索引不匹配。

基本上,如果您从0-19快速滚动并且要更新的单元格是#20,并且您希望它显示图像#20,但它仍然异步加载图像3,7,14。

为了防止这种情况,我所做的是追踪两个指数; #1)反映单元实际位置的最新索引路径和#2)实际加载异步的图像对应的索引(在这种情况下实际应该是你传入cellforitematindexpath的索引路径,它保留为异步过程通过队列工作,因此实际上是某些图像加载的“旧”数据。)

获取最新索引路径的一种方法可能是创建一个简单的方法,只返回单元格当前位置的NSInteger。将其存储为currentIndex。

然后在实际填写图像之前,我发了几个if语句来检查两者是否相等。

所以if(currentIndex == imageIndex)然后加载图片。

如果你把NSLog(@“CURRENT ...%d ... IMAGE ...%d”,currentIndex,imageIndex)放在那些if语句之前你可以很清楚地看到当两者不匹配时异步调用应退出。

希望这有帮助。

答案 2 :(得分:1)

我发现了chuey101所说的措辞令人困惑。我找到了一种方法,然后意识到chuey101意味着相同。

如果要帮助任何人,由于正在运行的线程不同,图像会闪烁并更改。因此,当您为图像操作生成线程时,它将针对特定单元格生成,例如c1。但是,最后当您实际将图像加载到单元格中时,它将成为您正在查看的当前单元格,即您滚动到的单元格 - 例如c2。因此,当您滚动到c2时,在滚动时会生成c2个线程,每个单元格前面有一个。根据我的理解,所有这些线程都会尝试将其图像加载到当前单元格c2中。所以,你有闪烁的图像。

为避免这种情况,您需要实际检查是否要将所需的图像加载到要加载到的单元格中。因此,在将图像加载到其中之前获取collectionviewcell indexpath.row(loading_image_into_cell)。另外,在产生线程之前,即在主线程(image_num_to_load)中,获取从线程中产生的单元格。现在,在加载之前,检查这两个数字是否相等。

问题解决了:)