【问题标题】:How to work with big images in iOS?如何在 iOS 中处理大图像?
【发布时间】:2013-12-01 02:27:08
【问题描述】:

我有一个UICollectionView,它显示了来自 CoreData 实体的一些数据。这个实体有很多属性,其中之一就是image(可变形类型)。

我的UICollectionView 滚动不流畅。我认为原因是 image 属性中的大图片(从 300 到 500k)。

这是我cellForItemAtIndexPath的代码:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    AppDelegate *d = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    static NSString *CellIdentifier = @"cellRecipe";

    RecipeCell *cell = (RecipeCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];

    cell.image.layer.borderColor = [UIColor blackColor].CGColor;
    cell.image.layer.borderWidth = 1.0f;

    Recipes *recipe = [recipesArray_ objectAtIndex:indexPath.item];

    if ([recipe.isPaid integerValue] == 0)
        [cell.freeImage setHidden:NO];
    else [cell.freeImage setHidden:YES];

    NSObject *object = [[NSUserDefaults standardUserDefaults] objectForKey:@"com.________.fullhomechef"];
    if ([object isEqual:[NSNumber numberWithBool:YES]]) {
        [cell.freeImage setHidden:YES];
    }

    if ([recipe.isFavorite integerValue] == 0) 
        [cell.favImage setImage:[UIImage imageNamed:@"toFavorite.png"]];
    else [cell.favImage setImage: [UIImage imageNamed:@"toFavorite_.png"]];

    if ([recipe.isFitness integerValue] == 1)
        [cell.fitImage setHidden:NO];
    else [cell.fitImage setHidden:YES];

    if ([d.currLang isEqualToString:@"ru"]) 
        [cell.recipeName setText: recipe.nameru];
    else [cell.recipeName setText: recipe.nameen];

    [cell.image setImage:recipe.image];

    int difficulty = [recipe.difficulty intValue];
    [cell.difficultyImage setImage: [mainTabController imageForRating:difficulty]];

    return cell;
}

我已尝试评论行[cell.image setImage:recipe.image];,但滚动仍在
间歇性的。

我做错了什么?可能有一些方法可以处理大图像吗?因为在以前的版本中我使用了大小为 50-70k 的图像。

附:如果我耐心地向下滚动到最后,滚动会变得更加流畅。

Guto Araujo 的回答之后

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"cellRecipe";

    RecipeCell *cell = (RecipeCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];

    cell.image.layer.borderColor = [UIColor blackColor].CGColor;
    cell.image.layer.borderWidth = 1.0f;

    Recipes *recipe = [fetchRecipes objectAtIndexPath:indexPath];

    if ([recipe.isPaid integerValue] == 0) {
        [cell.freeImage setHidden:NO];
    }
    else [cell.freeImage setHidden:YES];

    if (wasPaid == YES) {
        [cell.freeImage setHidden:YES];
    }

    if ([recipe.isFavorite integerValue] == 0) 
        [cell.favImage setImage:[UIImage imageNamed:@"toFavorite.png"] ];
    else [cell.favImage setImage: [UIImage imageNamed:@"toFavorite_.png"]];

    if ([recipe.isFitness integerValue] == 1)
        [cell.fitImage setHidden:NO];
    else [cell.fitImage setHidden:YES];

    if ([d.currLang isEqualToString:@"ru"]) {
        [cell.recipeName setText: recipe.nameru];
    } else [cell.recipeName setText:recipe.nameen];

    __weak typeof(self) weakSelf = self;
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        UIImage *image = recipe.image;

        dispatch_async(dispatch_get_main_queue(), ^{
            // Set via the main queue if the cell is still visible
            if ([weakSelf.collectionView.indexPathsForVisibleItems containsObject:indexPath]) {
                RecipeCell *cell =
                (RecipeCell *)[weakSelf.collectionView cellForItemAtIndexPath:indexPath];
                cell.image.image = image;
            }
        });
    }];
    [thumbnailQueue addOperation:operation];

    int difficulty = [recipe.difficulty intValue];
    [cell.difficultyImage setImage: [mainTabController imageForRating:difficulty]];

    return cell;
}

【问题讨论】:

  • 为什么每次都分配一个新的MainTabController
  • 所以你将图片存储在数据库本身?如果是这样,请不要这样做;)只需在您的数据库中存储一个路径并将图像存储在一个文件夹中。
  • 您可以尝试衡量性能;)这不是数据库的用途;)。您甚至应该能够创建一个文件夹结构,在其中保存带有路径和名称的图像,这样您就不需要路径属性(例如,如果您选择配方名称,您可以像[UIImage imageNamed:[[NSString stringWithFormat:@"%@", recipe.name]]]; 一样动态加载它)抱歉任何语法错误,但我在移动设备上……
  • @HAS 非常感谢!我会试着给你写结果!
  • Romowski,很高兴听到它成功了! @HAS 这是一个很好的辩论:存储。据我了解,Core Data 实际上在选择该选项的情况下将图像存储在数据库之外。我想缺点是它是信仰的飞跃而不是直接管理它。无论如何,我认为这两点都是公平的。感谢您的精彩辩论!

标签: ios uiimageview uicollectionview uicollectionviewcell


【解决方案1】:

一种方法是使用操作队列在后台加载图像,如 UICollectionView tutorial by Bryan Hansen 中所述,具体步骤 #45-47:

为操作队列添加一个属性:

@property (nonatomic, strong) NSOperationQueue *thumbnailQueue;

viewDidLoad中初始化并配置maxConcurrentOperationCount

self.thumbnailQueue = [[NSOperationQueue alloc] init];
self.thumbnailQueue.maxConcurrentOperationCount = 3; // Try different values for this variable

使用collectionView:cellForItemAtIndexPath:中的操作队列从Core Data异步加载图片:

// Change [cell.image setImage:recipe.image] for the code below

__weak typeof(self) weakSelf = self;
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    UIImage *image = recipe.image;

    dispatch_async(dispatch_get_main_queue(), ^{
        // Set cell.image via the main queue if the cell is still visible
        if ([weakSelf.collectionView.indexPathsForVisibleItems containsObject:indexPath]) {
            RecipeCell *cell =
                (RecipeCell *)[weakSelf.collectionView cellForItemAtIndexPath:indexPath];
            cell.image = image;
        }
    });
}];

[self.thumbnailQueue addOperation:operation];

更新 1:

此外,您可以做一些事情来优化图像加载:

  1. 在Recipe 实体中创建缩略图属性以存储要在Collection View 单元格中使用的图像的较小版本。使该图像不大于 RecipeCell 的最大宽度和高度。然后,在上面的块中使用该属性:

     UIImage *image = recipe.thumbnail;
    
  2. 为主要图像使用外部存储。我建议您尝试从图像属性中选择“存储在外部记录文件中”:

    使用此选项将使 Core Data 为您管理外部文件存储。或者,您可以按照@HAS 的建议仅存储图像路径并将文件存储在磁盘上。这是一个讨论这些选项的问题:Storing UIImage in Core Data with the new External Storage flag

  3. 如果您使用“存储在外部记录文件中”选项,请考虑将主图像移动到与配方相关的单独照片实体:配方 照片。在这种情况下,image 属性将位于 Photo 实体中。这样在遵循关系之前它们不会被加载到内存中。

    这里还有关于在 Core Data 中存储图像的最佳方法的更多讨论:How to store an image in core data

    更新 2:

  4. 如果滚动仍然不流畅,请考虑按照@DavidH 的建议使用 NSCache。这里有一个示例实现:Caching an Image and UICollectionView。事实上,即使滚动已经很顺利,这似乎也是正确的做法。

希望这会有所帮助。

【讨论】:

  • 谢谢,但__weak typeof(self) *weakSelf = self;一行有问题
  • 请根据 cmets 中关于图像存储的讨论查看答案的更新
【解决方案2】:

这里有很多事情要做,但让我为您提供一种使这项工作更好的方法的高级想法。

首先,如果可以,请预取 collectionView 所需的第一组图像,以便在该视图首次显示时将它们放在手边。

一旦你得到这些,你可以尝试在后台开始获取更多,或者在集合视图完成显示第一组时(你必须在这里进行试验)。

这个想法是,一旦你有东西要显示给用户,你会尝试立即预取“难以获取”的项目,并缓存它们,希望你能比用户滚动更快地获取它们。

一旦你在后台得到一些东西,把它放到一个 NSCache 中,然后在你出售单元格的例程中,在 NSCache 中寻找图像,如果有的话将它应用到单元格中。如果它不存在,那么只需将 UIImageView 的背景设置为灰色(或某种颜色)。

如果用户滚动的速度快于您检索图像的速度,您可以在从后台(和主线程)接收图像的方法中查看当前可见单元格,如果有任何缺少您刚刚检索到的图像,将其应用于单元格。

如果用户滚动缓慢,您可能会保持领先。如果他们滚动得非常快,他们会看到空的图像视图,但是当他们向后滚动时,这些视图会被填满。

使用 NSCache 效果非常好,因为系统会在可用时为您提供大量内存,如果它需要内存,它只会从缓存中删除项目,要求您再次获取它们。

【讨论】:

  • 那么你会将图像存储在数据库中吗?我仍然认为保存 URL 会更好(请参阅 this link 并在末尾写着 It is better, however, if you are able to store BLOBs as resources on the filesystem, and to maintain links (such as URLs or paths) to those resources. You can then load a BLOB as and when necessary. 你觉得呢?这不是处理图像的最有效(和最高效)的方式吗?跨度>
  • @HAS 很抱歉错过了这个。我所做的只是在存储库中保存小缩略图,对于较大的问题,它们存储在磁盘上,并且存储库只有图像的 filePath。我描述的方法对于两种技术都是相同的。另外,我强制执行了严格的排序,因此 tableView 中较低的图像直到前一个才显示。它看起来非常漂亮并且完成了,但是要做好很多工作(我可能不会再这样做了!)
  • 感谢您的解释,大卫 :)
  • @DavidH 我根据你的回答添加了一个更新:NSCache,因为这似乎是我错过的一个非常重要的点,它会对其他人有所帮助。希望你没事。感谢您提供的所有其他见解(例如预取)。我下次试试。
  • @GutoAraujo 没问题 - 一切都是为了找到好的解决方案。
猜你喜欢
  • 2013-04-09
  • 1970-01-01
  • 2012-07-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多