iOS中的非懒加载图片加载

39
我正在尝试在后台线程中加载UIImages,然后在iPad上显示它们。但是,当我将imageView的视图属性设置为图像时会出现卡顿。
我很快发现iOS上的图像加载是懒加载的,并在这个问题中找到了部分解决方案:CGImage/UIImage lazily loading on UI thread causes stutter
这实际上强制在线程中加载图像,但在显示图像时仍会出现卡顿。
您可以在此处找到我的示例项目:http://www.jasamer.com/files/SwapTest.zip(编辑:修复版),请查看SwapTestViewController。尝试拖动图片以查看卡顿。
我创建的测试代码如下(forceLoad方法是从我上面发布的stackoverflow问题中获取的):
NSArray* imagePaths = [NSArray arrayWithObjects:
                       [[NSBundle mainBundle] pathForResource: @"a.png" ofType: nil], 
                       [[NSBundle mainBundle] pathForResource: @"b.png" ofType: nil], nil];

NSOperationQueue* queue = [[NSOperationQueue alloc] init];

[queue addOperationWithBlock: ^(void) {
    int imageIndex = 0;
    while (true) {
        UIImage* image = [[UIImage alloc] initWithContentsOfFile: [imagePaths objectAtIndex: imageIndex]];
        imageIndex = (imageIndex+1)%2;
        [image forceLoad];

        //What's missing here?

        [self performSelectorOnMainThread: @selector(setImage:) withObject: image waitUntilDone: YES];
        [image release];
    }
}];

我知道为什么卡顿可以被避免的两个原因:

(1) 苹果能够在照片应用程序中无卡顿地加载图像

(2) 在上述代码的修改版本中,placeholder1和placeholder2被显示一次后,此代码不会导致卡顿:

    UIImage* placeholder1 = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource: @"a.png" ofType: nil]];
[placeholder1 forceLoad];
UIImage* placeholder2 = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource: @"b.png" ofType: nil]];
[placeholder2 forceLoad];

NSArray* imagePaths = [NSArray arrayWithObjects:
                       [[NSBundle mainBundle] pathForResource: @"a.png" ofType: nil], 
                       [[NSBundle mainBundle] pathForResource: @"b.png" ofType: nil], nil];
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock: ^(void) {
    int imageIndex = 0;
    while (true) {
        //The image is not actually used here - just to prove that the background thread isn't causing the stutter
        UIImage* image = [[UIImage alloc] initWithContentsOfFile: [imagePaths objectAtIndex: imageIndex]];
        imageIndex = (imageIndex+1)%2;
        [image forceLoad];

        if (self.imageView.image==placeholder1) {
            [self performSelectorOnMainThread: @selector(setImage:) withObject: placeholder2 waitUntilDone: YES];
        } else {
            [self performSelectorOnMainThread: @selector(setImage:) withObject: placeholder1 waitUntilDone: YES];
        }                

        [image release];
    }
}];

然而,我不能将所有的图片都保存在内存中。

这意味着forceLoad不能完成所有工作——在图片实际显示之前还有其他事情发生。 有没有人知道那是什么,以及我如何将其放入后台线程?

谢谢,朱利安

更新

使用了Tommy的一些技巧。我发现问题出在CGSConvertBGRA8888toRGBA8888上,所以似乎是颜色转换导致了延迟。 以下是该方法的(反向)调用堆栈。

Running        Symbol Name
6609.0ms        CGSConvertBGRA8888toRGBA8888
6609.0ms         ripl_Mark
6609.0ms          ripl_BltImage
6609.0ms           RIPLayerBltImage
6609.0ms            ripc_RenderImage
6609.0ms             ripc_DrawImage
6609.0ms              CGContextDelegateDrawImage
6609.0ms               CGContextDrawImage
6609.0ms                CA::Render::create_image_by_rendering(CGImage*, CGColorSpace*, bool)
6609.0ms                 CA::Render::create_image(CGImage*, CGColorSpace*, bool)
6609.0ms                  CA::Render::copy_image(CGImage*, CGColorSpace*, bool)
6609.0ms                   CA::Render::prepare_image(CGImage*, CGColorSpace*, bool)
6609.0ms                    CALayerPrepareCommit_(CALayer*, CA::Transaction*)
6609.0ms                     CALayerPrepareCommit_(CALayer*, CA::Transaction*)
6609.0ms                      CALayerPrepareCommit_(CALayer*, CA::Transaction*)
6609.0ms                       CALayerPrepareCommit_(CALayer*, CA::Transaction*)
6609.0ms                        CALayerPrepareCommit
6609.0ms                         CA::Context::commit_transaction(CA::Transaction*)
6609.0ms                          CA::Transaction::commit()
6609.0ms                           CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*)
6609.0ms                            __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
6609.0ms                             __CFRunLoopDoObservers
6609.0ms                              __CFRunLoopRun
6609.0ms                               CFRunLoopRunSpecific
6609.0ms                                CFRunLoopRunInMode
6609.0ms                                 GSEventRunModal
6609.0ms                                  GSEventRun
6609.0ms                                   -[UIApplication _run]
6609.0ms                                    UIApplicationMain
6609.0ms                                     main         

他提出的最后一个位掩码更改很遗憾地没有改变任何东西。

非常感谢您,我已经打开了大约1000个Chrome窗口来解决这个问题,但是没有人有任何想法。 - Sheepdogsheep
此外,NSBundle 不是线程安全的。 - Ken
3个回答

40

UIKit 只能在主线程中使用。因此,您的代码从除主线程之外的其他线程中使用 UIImage 是技术上无效的。您应该仅使用 CoreGraphics 在后台线程中加载(且非懒加载解码)图形,将 CGImageRef 发布到主线程并在那里将其转换为 UIImage。尽管您当前的实现可能表面上似乎可以工作(虽然会出现不希望看到的卡顿),但不能保证其可靠性。似乎在这个领域周围存在很多迷信和不良做法,因此您可能已经找到了一些错误的建议...

推荐在后台线程上运行:

// get a data provider referencing the relevant file
CGDataProviderRef dataProvider = CGDataProviderCreateWithFilename(filename);

// use the data provider to get a CGImage; release the data provider
CGImageRef image = CGImageCreateWithPNGDataProvider(dataProvider, NULL, NO, 
                                                    kCGRenderingIntentDefault);
CGDataProviderRelease(dataProvider);

// make a bitmap context of a suitable size to draw to, forcing decode
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
unsigned char *imageBuffer = (unsigned char *)malloc(width*height*4);

CGColorSpaceRef colourSpace = CGColorSpaceCreateDeviceRGB();

CGContextRef imageContext =
    CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace,
                  kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);

CGColorSpaceRelease(colourSpace);

// draw the image to the context, release it
CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), image);
CGImageRelease(image);

// now get an image ref from the context
CGImageRef outputImage = CGBitmapContextCreateImage(imageContext);

// post that off to the main thread, where you might do something like
// [UIImage imageWithCGImage:outputImage]
[self performSelectorOnMainThread:@selector(haveThisImage:) 
         withObject:[NSValue valueWithPointer:outputImage] waitUntilDone:YES];

// clean up
CGImageRelease(outputImage);
CGContextRelease(imageContext);
free(imageBuffer);
如果你正在使用 iOS 4 或更高版本,那么在创建 CGBitmapContextCreate 函数的相关参数时,可以将 NULL 作为参数传递,让 CoreGraphics 自己处理存储。这样就不需要进行 malloc/free。
与你发布的解决方案不同之处在于:
1. 它从 PNG 数据源创建 CGImage,采用了懒加载方式,因此这不一定是一个完全加载和解压缩的图像。 2. 它创建了与 PNG 大小相同的位图上下文。 3. 它将来自 PNG 数据源的 CGImage 绘制到位图上下文中 - 这应该强制执行完整的加载和解压缩,因为实际的颜色值必须放置在我们可以从 C 数组中访问的地方。该步骤与你链接的 forceLoad 的步骤相同。 4. 将位图上下文转换为图像。 5. 将该图像发送到主线程,预计成为 UIImage。
因此,在加载和显示的对象之间没有连续性;像素数据经过 C 数组(因此不存在隐藏的诡计),只有正确放置到数组中才能生成最终图像。

1
谢谢你提供的信息;UIImage 真的很方便 ;-) 我用你的代码替换了我的方法,但是卡顿仍然存在。 - jasamer
抱歉,注释有点混乱。我猜这可能是一个快速的颜色空间转换和/或上传。我找不到一个明确的参考来证明CALayers是否线程安全;也许值得在NSOperationQueue落地时设置CGImageRef,并在主线程上进行快速的setNeedsDisplay操作? - Tommy
哦,那将会忙于预乘alpha通道 - 这也暴露了我代码中的另一个缺陷; 它不能保留从提供的PNG文件中获取的alpha通道!等一下,我会尝试修复它。 - Tommy
2
顺便提一下,自iOS4以后,UIImage +imageWithContentsOfFile:是线程安全的。您可以在后台线程中使用此方法。链接 - Kazuki Sakamoto
1
@Kazuki:这很有道理;UIImage对用户交互一无所知,因此您可以预计它是UIKit中最容易去除线程限制的部分之一。@Julian:答案已编辑。 - Tommy
显示剩余16条评论

14

好的,我搞清楚了——在Tommy的帮助下。谢谢!

如果你使用以下代码创建你的上下文:

        CGContextRef imageContext =
        CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace,
                              kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);

主运行循环不再在主线程上执行任何转换。现在显示非常流畅。(这些标志有点令人困惑,有人知道为什么必须选择kCGImageAlphaPremultipliedFirst吗?)

编辑:

已上传修复后的示例项目:http://www.jasamer.com/files/SwapTest-Fixed.zip。如果您遇到图像性能问题,这是一个很好的起点!


如果我从互联网下载大图像,该怎么办?您的方法是 - (UIImage*) initImmediateLoadWithContentsOfFile:(NSString*)path; 是从本地加载。 - pengwang

10

您觉得UIImage+ImmediateLoad.m怎样?

它可以立即解码图片。除了UIImage -initWithContentsOfFile:,-imageWithCGImage:和CG*在iOS4后是线程安全的。


很高兴知道这些方法现在是线程安全的。有了像这样的UIImage类别,代码看起来更加美观,我会更新我的示例项目。不过需要调整位掩码选项以消除卡顿。 - jasamer
我从你的代码中仅仅一次加载图像就获得了足够的好处,以至于我现在只是加载图像并放弃它们,然后像往常一样继续我的工作。我稍微改变了方法,将其命名为imageImmediateLoadWithName,因此使用[UIImage imageImmediateLoadWithName:@"myImage.png"];即可使对图像的后续引用更快。 - Ben Flynn
你有没有关于“除了UIImage -initWithContentsOfFile:-imageWithCGImage:和CG *之外,在iOS4之后是线程安全的”的参考资料?在UIImage文档或iOS更改日志(https://developer.apple.com/library/ios/#releasenotes/General/WhatsNewIniPhoneOS/Articles/iPhoneOS4.html#//apple_ref/doc/uid/TP40009559-SW1)中没有相关内容。 - Tommy
我在一个包含很多UIImageView的UICollectionView上遇到了这个问题。在iPhone 5上,滚动效果非常糟糕。现在我已将它作为UIImage的一个类别实现,并把加载过程放到了另一个分派队列中。现在,滚动是流畅的。谢谢! - Trenskow

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