在iOS中加载非延迟图像

jas*_*mer 39 objective-c ios

我正在尝试在后台线程中加载UIImages,然后在iPad上显示它们.但是,当我将imageViews的视图属性设置为图像时,会出现断断续续的情况.我很快发现iOS上的图像加载是懒惰的,并且在这个问题中找到了部分解决方案:

在UI线程上懒洋洋地加载CGImage/UIImage会导致口吃

这实际上会强制图像加载到线程中,但在显示图像时仍然存在断断续续的情况.

你可以在这里找到我的示例项目:http://www.jasamer.com/files/SwapTest.zip(编辑:修复版),检查SwapTestViewController.尝试拖动图片以查看口吃.

我创建的测试代码是断断续续的(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) {
        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];
    }
}];
Run Code Online (Sandbox Code Playgroud)

我知道可以避免口吃的原因有两个:

(1)Apple可以在照片应用中加载图像而不会出现断断续续的情况

(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];
    }
}];
Run Code Online (Sandbox Code Playgroud)

但是,我无法将所有图像保存在内存中.

这意味着forceLoad不能完成整个工作 - 在实际显示图像之前还会发生其他事情.有谁知道那是什么,我怎么能把它放到后台线程?

谢谢,朱利安

更新

使用了一些Tommys技巧.我想到的是它的CGSConvertBGRA8888到RGBA8888需要花费很多时间,因此它似乎是导致延迟的颜色转换.这是该方法的(反向)调用堆栈.

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         
Run Code Online (Sandbox Code Playgroud)

遗憾的是,他提出的最后一次掩码改变没有改变任何东西.

Tom*_*mmy 39

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);
Run Code Online (Sandbox Code Playgroud)

如果您使用的是iOS 4或更高版本,则无需执行malloc/free,您只需将NULL作为CGBitmapContextCreate的相关参数传递,并让CoreGraphics整理出自己的存储.

这与您发布的解决方案不同,因为它:

  1. 从PNG数据源创建CGImage - 延迟加载适用,因此这不一定是完全加载和解压缩的图像
  2. 创建与PNG大小相同的位图上下文
  3. 将PNG数据源中的CGImage绘制到位图上下文中 - 这应该强制完全加载和解压缩,因为实际的颜色值必须放在我们可以从C数组访问它们的地方.此步骤就是您链接到的forceLoad.
  4. 将位图上下文转换为图像
  5. 将图像发布到主线程的帖子,大概是成为UIImage

所以加载的东西和显示的东西之间没有对象的连续性; 像素数据通过一个C数组(因此,没有隐藏的恶作剧的机会),只有当它正确地放入数组中才有可能制作出最终的图像.

  • 顺便说一下, UIImage +imageWithContentsOfFile: 在 iOS4 之后是线程安全的。您可以在后台线程中使用此方法。[链接](https://devforums.apple.com/message/360659#360659) (2认同)

jas*_*mer 14

好吧,想通了 - 在汤米的帮助下.谢谢!

如果您使用创建上下文

        CGContextRef imageContext =
        CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace,
                              kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
Run Code Online (Sandbox Code Playgroud)

主运行循环不会再导致主线程上的任何转换.显示现在是黄油顺利.(标志有点违反直觉,有谁知道为什么你必须选择kCGImageAlphaPremultipliedFirst?)

编辑:

上传了固定的示例项目:http://www.jasamer.com/files/SwapTest-Fixed.zip.如果您的图像性能有问题,这是一个很好的起点!


Kaz*_*oto 10

怎么样的UIImage + ImmediateLoad.m

它立即解码图像.除了UIImage -initWithContentsOfFile:, - imageWithCGImage:和CG*在iOS4之后是线程安全的.