5

我需要调整本地存储的大型图像(包含在 中self.optionArray),然后在 collectionView 中显示它。如果我只是显示它,iOS 会在我快速滚动时尝试调整图像大小,从而导致与内存相关的崩溃。

在下面的代码中,collectionView 会平滑滚动,但有时如果我滚动得非常快,则会显示不正确的图像,然后随着滚动减速而更改为正确的图像。为什么不设置cell.cellImage.imagenil解决这个问题?

- (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;
}

编辑:我添加了另一个异步缓存首先和 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;
4

3 回答 3

6

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

为了减少应用程序的占用空间,您不应维护UIImage对象数组。相反,只需维护图像的 URL 或路径数组,然后仅UIImage在 UI 需要时动态创建对象(称为延迟加载的过程)。一旦图像离开屏幕,您就可以释放它(只要您不保持对图像的强引用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的 a 的强引用。缓存是一种两全其美的方法,将图像加载到内存中以获得最佳性能,但是当您遇到内存压力时它会被释放。NSCacheviewDidLoad

显然,我很高兴地假设“哦,只需用图像文件名数组替换你的图像数组”,我知道你可能必须进入代码的一堆不同部分才能使其工作,但是这个无疑是你内存消耗的来源。显然,您总是可能遇到其他内存问题(保留周期等),但在您发布的代码段中没有类似的情况。

于 2013-05-22T14:09:40.970 回答
1

我发现 chuey101 所说的措辞令人困惑。我想出了一个办法,然后意识到 chuey101 的意思是一样的。

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

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

问题解决了 :)

于 2015-04-16T00:16:38.707 回答
1

我遇到了类似的问题,但以不同的方式解决了这个问题。

我还遇到了“弹出”问题,因为异步加载的图像在进入时会闪烁,直到最终显示正确的图像。

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

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

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

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

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

所以如果 (currentIndex == imageIndex) 然后加载图像。

如果您在这些 if 语句之前放置一个 NSLog(@"CURRENT...%d...IMAGE...%d", currentIndex, imageIndex) ,那么当两者不匹配并且异步调用应该出口。

希望这可以帮助。

于 2014-01-15T15:11:25.443 回答