【问题标题】:CGImage/UIImage lazily loading on UI thread causes stutterCGImage/UIImage 在 UI 线程上延迟加载会导致卡顿
【发布时间】:2010-12-21 09:22:15
【问题描述】:

我的程序显示一个水平滚动表面,从左到右平铺 UIImageViews。代码在 UI 线程上运行,以确保新可见的 UIImageViews 具有分配给它们的新加载的 UIImage。加载发生在后台线程上。

一切正常,除了每个图像变得可见时都会出现卡顿。起初我以为我的后台工作人员在 UI 线程中锁定了一些东西。我花了很多时间研究它,最终意识到 UIImage 在它第一次变得可见时在 UI 线程上做了一些额外的惰性处理。这让我很困惑,因为我的工作线程有用于解压缩 JPEG 数据的显式代码。

无论如何,我凭直觉写了一些代码来渲染到后台线程上的临时图形上下文中 - 果然,口吃消失了。 UIImage 现在正在我的工作线程上预加载。到目前为止一切顺利。

问题是我的新“强制延迟加载图像”方法不可靠。它会导致间歇性 EXC_BAD_ACCESS。我不知道 UIImage 在幕后实际上在做什么。也许它正在解压缩 JPEG 数据。反正方法是:

+ (void)forceLazyLoadOfImage: (UIImage*)image
{
 CGImageRef imgRef = image.CGImage;

 CGFloat currentWidth = CGImageGetWidth(imgRef);
 CGFloat currentHeight = CGImageGetHeight(imgRef);

    CGRect bounds = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);

 CGAffineTransform transform = CGAffineTransformIdentity;
 CGFloat scaleRatioX = bounds.size.width / currentWidth;
 CGFloat scaleRatioY = bounds.size.height / currentHeight;

 UIGraphicsBeginImageContext(bounds.size);

 CGContextRef context = UIGraphicsGetCurrentContext();
 CGContextScaleCTM(context, scaleRatioX, -scaleRatioY);
 CGContextTranslateCTM(context, 0, -currentHeight);
 CGContextConcatCTM(context, transform);
 CGContextDrawImage(context, CGRectMake(0, 0, currentWidth, currentHeight), imgRef);

 UIGraphicsEndImageContext();
}

EXC_BAD_ACCESS 发生在 CGContextDrawImage 行上。问题 1:我可以在 UI 线程以外的线程上执行此操作吗?问题 2: UIImage 实际上是“预加载”的是什么? QUESTION 3:官方解决这个问题的方法是什么?

感谢您阅读所有内容,任何建议将不胜感激!

【问题讨论】:

    标签: iphone uiimage lazy-loading exc-bad-access cgimage


    【解决方案1】:

    我也遇到过同样的口吃问题,在一些帮助下,我在这里找到了正确的解决方案:Non-lazy image loading in iOS

    要提两件重要的事情:

    • 不要在工作线程中使用 UIKit 方法。请改用 CoreGraphics。
    • 即使您有一个用于加载和解压缩图像的后台线程,如果您为 CGBitmapContext 使用了错误的位掩码,您仍然会有些卡顿。这是您必须选择的选项(我仍然不清楚为什么):

    -

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

    我在此处发布了一个示例项目:SwapTest,它的性能与 Apple 的照片应用程序用于加载/显示图像的性能大致相同。

    【讨论】:

    • 我无法理解为什么您的点赞数为 0。您的解决方案是唯一对我有用的解决方案。如果可以的话,我会给你 10 票。谢谢!!!
    【解决方案2】:

    我使用@jasamer 的 SwapTest UIImage 类别在工作线程(使用 NSOperationQueue)中强制加载我的大型 UIImage(大约 3000x2100 像素)。这样可以减少将图像设置到 UIImageView 中的卡顿时间(在 iPad1 上约为 0.5 秒)。

    这是 SwapTest UIImage 类别...再次感谢@jasamer :)

    UIImage+ImmediateLoading.h 文件

    @interface UIImage (UIImage_ImmediateLoading)
    
    - (UIImage*)initImmediateLoadWithContentsOfFile:(NSString*)path;
    + (UIImage*)imageImmediateLoadWithContentsOfFile:(NSString*)path;
    
    @end
    

    UIImage+ImmediateLoading.m 文件

    #import "UIImage+ImmediateLoading.h"
    
    @implementation UIImage (UIImage_ImmediateLoading)
    
    + (UIImage*)imageImmediateLoadWithContentsOfFile:(NSString*)path {
        return [[[UIImage alloc] initImmediateLoadWithContentsOfFile: path] autorelease];
    }
    
    - (UIImage*)initImmediateLoadWithContentsOfFile:(NSString*)path {
        UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
        CGImageRef imageRef = [image CGImage];
        CGRect rect = CGRectMake(0.f, 0.f, CGImageGetWidth(imageRef), CGImageGetHeight(imageRef));
        CGContextRef bitmapContext = CGBitmapContextCreate(NULL,
                                                           rect.size.width,
                                                           rect.size.height,
                                                           CGImageGetBitsPerComponent(imageRef),
                                                           CGImageGetBytesPerRow(imageRef),
                                                           CGImageGetColorSpace(imageRef),
                                                           kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little
                                                           );
        //kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little are the bit flags required so that the main thread doesn't have any conversions to do.
    
        CGContextDrawImage(bitmapContext, rect, imageRef);
        CGImageRef decompressedImageRef = CGBitmapContextCreateImage(bitmapContext);
        UIImage* decompressedImage = [[UIImage alloc] initWithCGImage: decompressedImageRef];
        CGImageRelease(decompressedImageRef);
        CGContextRelease(bitmapContext);
        [image release];
    
        return decompressedImage;
    }
    
    @end
    

    这就是我创建 NSOpeationQueue 并在主线程上设置图像的方式...

    // Loads low-res UIImage at a given index and start loading a hi-res one in background.
    // After finish loading, set the hi-res image into UIImageView. Remember, we need to 
    // update UI "on main thread" otherwise its result will be unpredictable.
    -(void)loadPageAtIndex:(int)index {
        prevPage = index;
    
        //load low-res
        imageViewForZoom.image = [images objectAtIndex:index];
    
        //load hi-res on another thread
        [operationQueue cancelAllOperations];  
        NSInvocationOperation *operation = [NSInvocationOperation alloc];
        filePath = [imagesHD objectAtIndex:index];
        operation = [operation initWithTarget:self selector:@selector(loadHiResImage:) object:[imagesHD objectAtIndex:index]];
        [operationQueue addOperation:operation];
        [operation release];
        operation = nil;
    }
    
    // background thread
    -(void)loadHiResImage:(NSString*)file {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        NSLog(@"loading");
    
        // This doesn't load the image.
        //UIImage *hiRes = [UIImage imageNamed:file];
    
        // Loads UIImage. There is no UI updating so it should be thread-safe.
        UIImage *hiRes = [[UIImage alloc] initImmediateLoadWithContentsOfFile:[[NSBundle mainBundle] pathForResource:file ofType: nil]];
    
        [imageViewForZoom performSelectorOnMainThread:@selector(setImage:) withObject:hiRes waitUntilDone:NO];
    
        [hiRes release];
        NSLog(@"loaded");
        [pool release];
    }
    

    【讨论】:

    • 在你的主线程之外的线程中仍然有对 UIImage 的调用,所以这个解决方案是不安全的。
    • 你可以使用 CGImageRef imageRef = CGImageCreateWithJPEGDataProvider(CGDataProviderCreateWithFilename([path UTF8String]), NULL, NO, kCGRenderingIntentDefault) 代替 UIImage
    • @JorisMans 如何在主线程外调用 UIImage 不安全?
    • UIImage = UIKit.你不能在另一个线程上使用 UIKit
    • 不,只有 UI 更新 不允许在另一个线程上。这里我只创建了一个 UIImage 实例,所以应该没问题。此外,在后台加载图像是相当普遍的做法。
    【解决方案3】:

    UIGraphics* 方法设计为只能从主线程调用。他们可能是你麻烦的根源。

    您可以将UIGraphicsBeginImageContext() 替换为对CGBitmapContextCreate() 的调用;它涉及更多一点(您需要创建一个色彩空间,找出要创建的正确大小的缓冲区,然后自己分配它)。 CG* 方法可以从不同的线程运行。


    我不确定你是如何初始化 UIImage 的,但是如果你使用 imageNamed:initWithFile: 进行初始化,那么你可以通过自己加载数据然后调用 @987654327 来强制它加载@。卡顿可能是由于懒惰的文件 I/O,所以用数据对象初始化它不会给它从文件读取的选项。

    【讨论】:

    • 您好,感谢您抽出宝贵时间帮助解决此问题。我最终意识到了这一点。我只能想象在那些 UI* 方法中发生了什么样的讨厌的非线程安全缓存。我的界面现在没有卡顿.....
    • 我很困惑...您是说 imageNamed: 导致延迟文件 IO 但是..创建数据对象不会?或者您是说您应该将数据对象汇集到某个地方以便在适当的时候从中提取?
    • Jasconius:如果你创建一个 UIImage 对象并给它一个文件的路径,它可能不会将整个图像加载到内存中,直到你绘制它。如果您使用 NSData 对象将文件加载到内存中,然后将其传递给 UIImage,它别无选择,只能将其保存在内存中。什么是最好的取决于具体情况。
    【解决方案4】:

    我遇到了同样的问题,即使我使用数据初始化了图像。 (我猜数据也是懒加载的?)我已经成功使用以下类别强制解码:

    @interface UIImage (Loading)
    - (void) forceLoad;
    @end
    
    @implementation UIImage (Loading)
    
    - (void) forceLoad
    {
        const CGImageRef cgImage = [self CGImage];  
    
        const int width = CGImageGetWidth(cgImage);
        const int height = CGImageGetHeight(cgImage);
    
        const CGColorSpaceRef colorspace = CGImageGetColorSpace(cgImage);
        const CGContextRef context = CGBitmapContextCreate(
            NULL, /* Where to store the data. NULL = don’t care */
            width, height, /* width & height */
            8, width * 4, /* bits per component, bytes per row */
            colorspace, kCGImageAlphaNoneSkipFirst);
    
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
        CGContextRelease(context);
    }
    
    @end
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-02-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多