2015-08-03 123 views
17

在我的iOS应用程序中,我有UICollectionView,它显示大约1200个小(35x35点)图像。这些图像存储在应用程序包中。如何提高包含大量小图片的UCollectionView的性能?

我正确地重用UICollectionViewCell秒,但仍然有取决于我如何处理图像加载性能问题:

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

  • 我改变了避免图像缓存的方法。我把图像放到我的项目中(不是作为目录),我现在用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的建议显示的那些。

原因:

  • 考虑不同的屏幕大小和方向。必须为它们中的每一个预先制作图像,这将使应用下载巨大。
  • 小图像可以按不同顺序显示,其中一些可能在某些情况下隐藏。我完全无法为每种可能的组合和顺序预先分解图像。
+1

你需要某种形式的缓存可以从您的需求,标准的图像填充,但随后快速访问一次的事情是建立和运行,从而滚动等是光滑的。以下链接似乎提供了这样的机制。看起来它使用内存映射文件并避免了大部分开销。也许值得一瞧? https://开头github上。com/path/FastImageCache –

回答

4

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

而对于性能问题,我建议您异步加载图像并使用委托/块更新视图。并保留一些图像内存(但不是所有的,比方说100)

希望这有助于!

+0

请参阅我的编辑。 – drasto

+0

@drasto为你的“编辑1”问题应该是你没有设置内容的图像为零之前分派一个新的任务获取图像。 – Kevin

2

检查本教程:

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

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

+0

请参阅我的编辑。 – drasto

+0

为什么不试试这个教程?它完全解决了你的问题。 –

+0

我正在阅读它。但它似乎并不是我所需要的:它使用AsyncDisplayKit在后台加载图像,并在加载时显示一些占位符。但占位符在我的情况下看起来非常糟糕 - 想象一下,在收集视图中有1200个35x35图像,您在底部,按一个按钮滚动到顶部。大约1100个占位符将滚动。这不会让你对滚动的内容有太多的感觉...... – drasto

5

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

您目前的布局:

|  |  |  | 
| 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%可以解决你的问题,但它是有道理的尝试。

+0

这是一个好主意,但我不幸的是不能这么做 - 有很多原因让我必须将它显示为许多小图片。首先,图像每次可能以不同的顺序出现,有些可能会丢失等。其次,对于不同的屏幕尺寸,特定屏幕上会显示不同的图像。例如,在iPhone 6 Plus上,更多图像适用于iPhone 5S屏幕。我将不得不为每个屏幕大小和方向分别预先制作图像,这将使应用程序的下载规模巨大。有更多的原因,我为什么不能做你的建议... – drasto

3

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

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

你也应该需要定制的LIFOOperationQueue有队列中的任务的最大数量,以便它可以消除不必要的任务。最好设置任务的最大数量为1.5 * numberOfVisibleCell

最后一件事,你应该在willDisplayCell代替cellForRowAtIndexPath

+0

谢谢你有趣的答案。但是,我还有一些其他问题:通过编辑1中的代码进行异步加载,LIFOOperationQueue是否有优势?它将在编辑1中描述的编程式滚动时解决视觉问题吗?为什么我应该在'willDisplayCell'而不是'cellForRowAtIndexPath'中加载图像? – drasto

+1

如果你使用队列,你可以取消不必要的任务,而你的答案不能这样做。它仍然无法解决视觉问题,所以你必须自己动手,正如我在答案中所描述的那样。当然,willDisplay总是在cellForRow之前调用,所以它会更快 – sahara108

2

要解决在EDIT 1描述问题创造负荷图像操作,你应该重写prepareForReuse方法在UICollectionViewCell子类,图层的内容复位到零:

- (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图像为与 颜色的平坦区域,锋利的线条,等
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。下一次它将检查是否已经下载,然后只从缓存设置。

如果您需要任何帮助,请告诉我。

谢谢!

+0

这是一个很好的答案,可以解决您的问题。另一个需要研究的东西是一个像https://github.com/path/FastImageCache这样的开源项目,但它使用相同的原理。您仍然可能遇到的问题是扩展名为40MB的上限。 – mclaughlinj

+0

@mclaughlinj我不认为缓存是一个可行的选择,因为可用内存非常有限:40MB用于*所有*。这是针对所有视图,动画,数据结构。另外,键盘扩展存在非常奇怪的内存管理 - 在应用程序因内存限制死亡之前,您并不总是收到内存警告。我有*可能* 10MB保留在那里,所以我可以使用5MB进行缓存,但这太少了,无法使用。 – drasto

3

夫妇的事情,你可以为此做,

  1. 你应该只加载这些图像在其中获取的,并卸载它们时不重新获得这会让你的应用程序使用较少的内存 。
  2. 当你在后台加载图像时,在后台线程中解码它,然后将其设置为图像视图...在默认情况下 图像将在您设置时解码并且通常会在主线程上发生 线程,这将使滚动平滑,您可以通过下面的代码解码图像。

    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; 
    

    }