UIImage解压引起滚动卡顿

28

我有一个使用全屏tableView的应用程序,它显示了一堆小图片。这些图片从网络中获取,通过后台线程进行处理,然后使用类似以下方式保存到磁盘:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
    // code that adds some glosses, shadows, etc 
    UIImage *output = UIGraphicsGetImageFromCurrentImageContext();

    NSData* cacheData = UIImagePNGRepresentation(output);
    [cacheData writeToFile:thumbPath atomically:YES];

    dispatch_async(dispatch_get_main_queue(), ^{
        self.image = output; // refreshes the cell using KVO
    });
});

这段代码仅在单元格第一次显示时执行(因为此后图像已经存在于磁盘上)。在这种情况下,单元格是使用以下方式加载的:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    UIImage *savedImage = [UIImage imageWithContentsOfFile:thumbPath];

    if(savedImage) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.image = savedImage; // refreshes the cell using KVO
        });
    }
});
我的问题是在第一种情况下,滚动非常顺畅。但是在第二种情况下(直接从磁盘读取图像时),即使图像已加载,滚动也会非常卡顿。绘制是导致延迟的原因。使用Instruments,我看到copyImageBlockSetPNGpng_read_nowinflate占用了大部分CPU(当将self.image分配给UIGraphicsGetImageFromCurrentImageContext()时它们不会这样)。
我认为这是因为在第一种情况下,UIImage是绘图的原始输出,而在第二种情况下,它必须每次绘制时解压缩PNG。我尝试使用JPG代替PNG,结果类似。
有没有方法来解决这个问题?也许只在第一次绘制时解压PNG?
3个回答

24
您的问题在于+imageWithContentsOfFile:被缓存和延迟加载。如果您想要做类似的操作,可以尝试在后台队列中使用以下代码:
// Assuming ARC
NSData* imageFileData = [[NSData alloc] initWithContentsOfFile:thumbPath];
UIImage* savedImage = [[UIImage alloc] initWithData:imageFileData];

// Dispatch back to main queue and set image...

现在,使用这段代码,图像数据的实际解压仍然是惰性的并且会花费一点时间,但是不会像您代码示例中的惰性加载那样影响文件访问速度。
由于您仍然遇到性能问题,您还可以强制UIImage在后台线程上解压缩图像:
// Still on background, before dispatching to main
UIGraphicsBeginImageContext(CGSizeMake(100, 100)); // this isn't that important since you just want UIImage to decompress the image data before switching back to main thread
[savedImage drawAtPoint:CGPointZero];
UIGraphicsEndImageContext();

// dispatch back to main thread...

这确实使滚动更加流畅,但远不及第一种情况那么顺畅... - samvermette
@samvermette 对的,在代码块末尾看到我的评论。在第一种情况下,您已经拥有了完全解压缩的图像,而在第二种情况下,您还没有解压缩的图像(UIImage是惰性的)。如果您有很多这些小图像,请按行分批加载它们并一次性加载所有图像可能会有所帮助。 - Jason Coco
@samvermette 我编辑了答案,并提供了一些示例代码,你可以将其放入后台队列中... - Jason Coco
1
好问题,好答案。如果你还没有这样做,你也可以将解压后的图像放入NSCache中,以避免再次访问磁盘。 - Ryder Mackay
谢谢你提供解压缩的提示。我发现使用imageNamed比initWithData表现更好。 - Johnny Z
显示剩余2条评论

16

Jason提到的预先绘制图像以解压缩是关键,但如果复制整个图像并丢弃原始图像,则性能将更好。

iOS上运行时创建的图像似乎比从文件加载的图像更适合绘制,即使您已经强制它们解压缩。因此,您应该像这样加载图像(将解压缩的图像放入NSCache中,这样您就不必一直重新加载它也是一个好主意):

- (void)loadImageWithPath:(NSString *)path block:(void(^)(UIImage *image))block
{
    static NSCache *cache = nil;
    if (!cache)
    {
        cache = [[NSCache alloc] init];
    }

    //check cache first
    UIImage *image = [cache objectForKey:path];
    if (image)
    {
        block(image);
        return;
    }

    //switch to background thread
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{

        //load image
        UIImage *image = [UIImage imageWithContentsOfFile:path];

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

        //back to main thread
        dispatch_async(dispatch_get_main_queue(), ^{

            //cache the image
            [cache setObject:image forKey:path];

            //return the image
            block(image);
        });
    });
}

此外,这些图像有多大?如果它们在Retina iPad上是全屏的,无论如何加载它们都会有延迟。诀窍在于首先加载低分辨率版本,然后在滚动停止时切换到Retina图像。 - Nick Lockwood
我曾经使用缩略图,并在“NSOperationQueue”上异步加载全尺寸图片。虽然是异步的,但当分配到图像视图(在主队列上)时,它仍然会出现延迟(如果用户正在拖动)。问题在于,这些图片非常大(iPhone 5照片的全尺寸为30MB解压缩)。最终,我决定不显示全尺寸照片。 - Tricertops
2
根据我的实验结果,在上述代码中,对于我的情况,UIGraphicsBeginImageContextWithOptions 中的比例参数应该设置为1。 - Erik
2017年是否仍然如此? - BarrettJ
@BarrettJ 它仍然可以工作,但我建议现在使用ImageIO。 - Nick Lockwood
显示剩余4条评论

6

图片解压缩的另一种方法:

 NS_INLINE void forceImageDecompression(UIImage *image)
 {
  CGImageRef imageRef = [image CGImage];
  CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
  CGContextRef context = CGBitmapContextCreate(NULL, CGImageGetWidth(imageRef), CGImageGetHeight(imageRef), 8, CGImageGetWidth(imageRef) * 4, colorSpace,kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
  CGColorSpaceRelease(colorSpace);
  if (!context) { NSLog(@"Could not create context for image decompression"); return; }
  CGContextDrawImage(context, (CGRect){{0.0f, 0.0f}, {CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}}, imageRef);
  CFRelease(context);
}

使用:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
  UIImage *image = [UIImage imageWithContentsOfFile:[NSString stringWithFormat:@"%u.jpg", pageIndex]];
  forceImageDecompression(image);

  dispatch_async(dispatch_get_main_queue(), ^{ 
    [((UIImageView*)page)setImage:image];
  });
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接