如何提高包含大量小图像的UCollectionView的性能?

时间:2015-08-03 10:57:37

标签: ios image performance memory uicollectionview

在我的iOS应用中,我UICollectionView显示大约1200个小(35x35点)图像。图像存储在应用程序包中。

我正在重复使用UICollectionViewCell但仍有性能问题因我处理图片加载的方式而异:

  • 我的应用是应用程序扩展,内存有限(本例为40 MB)。将所有1200个图像放入Assets目录并使用UIImage(named: "imageName")加载它们会导致内存崩溃 - 系统缓存的图像会填满内存。在某些时候,应用程序需要分配更大的内存部分,但由于缓存的图像,这些内存不可用。操作系统刚刚杀死了应用程序,而不是触发内存警告和清理缓存。

  • 我改变了方法以避免图像缓存。我将图像作为png文件放到我的项目中(而不是作为asssets目录),我现在正在使用NSBundle.mainBundle().pathForResource("imageName", ofType: "png")加载它们。由于内存错误,应用程序不再崩溃,但单个图像的加载需要更长时间,即使在最新的iPhone上,快速滚动也会滞后。

我可以完全控制图像,并可以将它们转换为.jpeg或优化它们(我已经尝试ImageOptim和其他一些选项但没有成功。)

如何立即解决这两个性能问题?

编辑1:

我也尝试在后台线程中加载图片。这是我的UICollectionViewCell的子类:

的代码
private func loadImageNamed(name: String) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { [weak self] () in
        let image = bundle.pathForResource(name, ofType: "png")?.CGImage
        if name == self?.displayedImageName {
            dispatch_async(dispatch_get_main_queue(), {
                if name == self?.displayedImageName {
                    self?.contentView.layer.contents = image
                }
            })
        }
    })
}

这使滚动变得平滑而不会消耗额外的内存来缓存以编程方式滚动到某个位置(例如当UICollectionView滚动到顶部时)会导致另一个问题:在滚动动画期间,图像不会更新(滚动速度太快,无法加载),滚动完成后,会显示错误的图像显示秒数 - 并且一个接一个地替换为正确的图像。这在视觉上非常令人不安。

编辑2:

我无法将小图像分组为更大的合成图像,并按照this answer的建议显示这些图像。

理由:

  • 考虑不同的屏幕尺寸和方向。必须为每个人提供预先组合的图像,这将使应用程序下载量巨大。
  • 小图像可以按不同顺序显示,其中一些可能在某些情况下隐藏。我完全不能为每种可能的组合和订单提供预先组合的图像。

7 个答案:

答案 0 :(得分:5)

我可以提出可能解决问题的替代方法:
考虑将图像块渲染为单个合成图像。这样大的图像应该覆盖app窗口的大小。对于用户来说,它看起来像是小图像的集合,但从技术上讲它将是大图像的表格。

您当前的布局:

 |      |      |      |
 | cell | cell | cell |  -> cells outside of screen
 |      |      |      |
************************
*|      |      |      |*
*| cell | cell | cell |* -> cells displayed on screen
*|      |      |      |*
*----------------------*
*|      |      |      |* 
*| cell | cell | cell |* -> cells displayed on screen
*|      |      |      |*
*----------------------*
*|      |      |      |*
*| cell | cell | cell |* -> cells displayed on screen
*|      |      |      |*
************************
 |      |      |      |
 | cell | cell | cell |  -> cells outside of screen
 |      |      |      |

建议布局:

 |                    |
 |     cell with      |
 |   composed image   |  -> cell outside of screen
 |                    |
************************
*|                    |*
*|                    |*
*|                    |* 
*|                    |* 
*|     cell with      |*
*|   composed image   |* -> cell displayed on screen
*|                    |*
*|                    |*
*|                    |* 
*|                    |* 
*|                    |*
************************
 |                    |
 |     cell with      |
 |   composed image   |  -> cell outside of screen
 |                    |

理想情况下,如果您预先渲染此类合成图像并在构建时将它们投影到项目中,但您也可以在运行时渲染它们。当然,第一个变体的工作速度要快得多。但无论如何,单个大图像会花费更少的内存,然后分离该图像。

如果您有可能预先渲染它们,请使用JPEG格式。在这种情况下,您的第一个解决方案(在主线程上加载[UIImage imageNamed:]的图像)可能会正常工作,因为使用的内存更少,布局更简单。

如果你必须在运行时渲染它们,那么你将需要使用你当前的解决方案(在后台工作),当你快速动画发生时你仍会看到图像错位,但在这种情况下它将是单个错位(一个图像覆盖窗框),所以它应该看起来更好。

如果您需要知道用户点击的图片(原始小图片35x35),您可以使用附加到单元格的UITapGestureRecognizer。识别手势后,您可以使用locationInView:方法计算小图像的正确索引。

我不能说它100%解决了你的问题,但尝试是有意义的。

答案 1 :(得分:4)

从PNG更改为JPEG无助于保存内存,因为当您将图像从文件加载到内存时,它会从压缩数据中提取到未压缩的字节。

对于性能问题,我建议您异步加载图像并使用delegate / block更新视图。并将一些图像保存在内存中(但不是所有图像,让我们说100)

希望这有帮助!

答案 2 :(得分:4)

  1. 每次出现单元格时都无需从文档目录中获取图像。
  2. 获取图像后,您可以在NSCache中保存图像,下次只需从NSCache获取图像而不是从文档目录中再次获取图像。
  3. 为NSCache objCache创建一个对象;
  4. 在你的cellForItemAtIndexPath中,只需写下

    即可
    UIImage *cachedImage = [objCache objectForKey:arr_PathFromDocumentDirectory[indexPath.row]];
    
    if (cachedImage) {
        imgView_Cell.image = cachedImage;
    } else {
        dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
        dispatch_async(q, ^{
            /* Fetch the image from the Document directory... */
            [self downloadImageWithURL:arr_PathFromDocument[indexPath.row] completionBlock:^(BOOL succeeded, CGImageRef data, NSString* path) {
                if (succeeded) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        UIImage *img =[UIImage imageWithCGImage:data];
                        imgView_Cell.image = img;
                        [objCache setObject:img forKey::arr_PathFromDocument[indexPath.row]];
                    });
                }
            }];
        });
    }
    
  5. 获取图像后,在NSCache中设置路径。下次检查是否已经下载,然后从缓存中设置。
  6. 如果您需要任何帮助,请告诉我。

    谢谢!

答案 3 :(得分:3)

您应该创建一个队列来异步加载图像。最好的选择是后进先出队列。您可以查看此LIFOOperationQueue。 一个重要的事情是防止显示需要单独处理的错误图像。为此,在创建加载图像的操作时,请将当前indexPath作为标识符。然后在回调函数中,检查给定的indexPath是否可见以更新视图

if (self.tableView.visibleIndexPath().containsObject(indexPath) {
    cell.imageView.image = img;
}

您还应该自定义LIFOOperationQueue以在队列中拥有最大数量的任务,以便它可以删除不必要的任务。最好将任务设置为1.5 * numberOfVisibleCell

最后一件事,您应该在willDisplayCell而不是cellForRowAtIndexPath

中创建加载图片操作

答案 4 :(得分:3)

你可以为此做些事情,

  1. 您应该只加载重新获取的图像,并在不重新获取时卸载它们,这将使您的应用程序使用更少 存储器中。
  2. 在背景中加载图像时的其他事情是在将其设置为图像视图之前在后台线程中对其进行解码...默认情况下 设置时图像将解码,并且通常会在main上发生 线程,这将使滚动平滑,您可以通过下面的代码解码图像。

    static UIImage *decodedImageFromData:(NSData *data, BOOL isJPEG)
    {
        // Create dataProider object from provided data
        CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
        // Create CGImageRef from dataProviderObject
        CGImageRef newImage = (isJPEG) ? CGImageCreateWithJPEGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault) : CGImageCreateWithPNGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault);
    
    
    
    // Get width and height info from image
    const size_t width = CGImageGetWidth(newImage);
    const size_t height = CGImageGetHeight(newImage);
    
    // Create colorspace
    const CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
    // Create context object, provide data ref if wean to store other wise NULL
    // Set width and height of image
    // Number of bits per comoponent
    // Number of bytes per row
    // set color space
    // And tell what CGBitMapInfo
    const CGContextRef context = CGBitmapContextCreate( NULL,width, height,8, width * 4, colorspace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
    
    //Now we can draw image
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), newImage);
    // Get CGImage from drwan context
    CGImageRef drawnImage = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    CGColorSpaceRelease(colorspace);
    
    // Create UIImage from CGImage
    UIImage *image = [UIImage imageWithCGImage:drawnImage];
    
    CGDataProviderRelease(dataProvider);
    CGImageRelease(newImage);
    CGImageRelease(drawnImage);
    return image;
    

    }

答案 5 :(得分:2)

查看本教程:

http://www.raywenderlich.com/86365/asyncdisplaykit-tutorial-achieving-60-fps-scrolling

它使用AsyncDisplayKit在后台线程上加载图像。

答案 6 :(得分:2)

要解决EDIT 1中描述的问题,您应该覆盖prepareForReuse子类中的UICollectionViewCell方法,并将图层的内容重置为nil:

- (void) prepareForReuse
{
    self.contentView.layer.contents = nil;
}

要在后台加载图片,您可以使用下一个:

- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSString* imagePath = #image_path#;
    weakself;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //load image
        UIImage *image = [UIImage imageWithContentsOfFile:imagePath];

        //redraw image using device context
        UIGraphicsBeginImageContextWithOptions(image.size, YES, 0);
        [image drawAtPoint:CGPointZero];
        image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        dispatch_async(dispatch_get_main_queue(), ^{
            strongself;
            if ([[self.collectionView indexPathsForVisibleItems] indexOfObject:indexPath] != NSNotFound) {
                cell.contentView.layer.contents = (__bridge id)(image.CGImage);
            } else {
                // cache image via NSCache with countLimit or custom approach
            }
        });
   });
}

如何改进:

  1. 使用NSCache或自定义缓存算法以便不加载 一直滚动的图像。
  2. 使用正确的文件格式。 JPEG解压缩对于像照片这样的图像工作得更快。将PNG用于平坦区域的图像 颜色,线条等。