CGImage/UIImage 在 UI 线程上延迟加载会导致卡顿

发布于 2024-08-12 13:46:16 字数 1416 浏览 9 评论 0原文

我的程序显示一个水平滚动表面,从左到右平铺有 UIImageViews。代码在 UI 线程上运行,以确保新可见的 UIImageView 分配有新加载的 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 实际上“预加载”是什么?问题3:解决这个问题的官方方法是什么?

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

My program displays a horizontal scrolling surface tiled with UIImageViews from left to right. Code runs on the UI thread to ensure that newly-visible UIImageViews have a freshly loaded UIImage assigned to them. The loading happens on a background thread.

Everything works almost fine, except there is a stutter as each image becomes visible. At first I thought my background worker was locking something in the UI thread. I spent a lot of time looking at it and eventually realized that the UIImage is doing some extra lazy processing on the UI thread when it first becomes visible. This puzzles me, since my worker thread has explicit code for decompressing JPEG data.

Anyway, on a hunch I wrote some code to render into a temporary graphics context on the background thread and - sure enough, the stutter went away. The UIImage is now being pre-loaded on my worker thread. So far so good.

The issue is that my new "force lazy load of image" method is unreliable. It causes intermittent EXC_BAD_ACCESS. I have no idea what UIImage is actually doing behind the scenes. Perhaps it is decompressing the JPEG data. Anyway, the method is:

+ (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();
}

And the EXC_BAD_ACCESS happens on the CGContextDrawImage line. QUESTION 1: Am I allowed to do this on a thread other than the UI thread? QUESTION 2: What is the UIImage actually "pre-loading"? QUESTION 3: What is the official way to solve this problem?

Thanks for reading all that, any advice would be greatly appreciated!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

始于初秋 2024-08-19 13:46:16

我也遇到了同样的口吃问题,在一些帮助下我在这里找到了正确的解决方案: iOS 中的非延迟图像加载

需要提及的两件重要事情:

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

-

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

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

I've had the same stuttering problem, with some help I figured out the proper solution here: Non-lazy image loading in iOS

Two important things to mention:

  • Don't use UIKit methods in a worker-thread. Use CoreGraphics instead.
  • Even if you have a background thread for loading and decompressing images, you'll still have a little stutter if you use the wrong bitmask for your CGBitmapContext. This are the options you have to choose (it's still a bit unclear to me why):

-

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

I've posted a sample project here: SwapTest, it has about the same performace as Apples' Photos app for loading/displaying images.

凡间太子 2024-08-19 13:46:16

我使用 @jasamer 的 SwapTest UIImage 类别在工作线程(使用 NSOperationQueue)中强制加载我的大 UIImage(大约 3000x2100 px)。这可以减少将图像设置到 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];
}

I used @jasamer's SwapTest UIImage category to force load my large UIImage (about 3000x2100 px) in a worker thread (with NSOperationQueue). This reduces the stutter time when setting the image into the UIImageView to an acceptable value (about 0.5 sec on iPad1).

Here is SwapTest UIImage category... thanks again @jasamer :)

UIImage+ImmediateLoading.h file

@interface UIImage (UIImage_ImmediateLoading)

- (UIImage*)initImmediateLoadWithContentsOfFile:(NSString*)path;
+ (UIImage*)imageImmediateLoadWithContentsOfFile:(NSString*)path;

@end

UIImage+ImmediateLoading.m file

#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

And this is how I create NSOpeationQueue and set the image on main thread...

// 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];
}
胡大本事 2024-08-19 13:46:16

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

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


我不确定您如何初始化 UIImage,但如果您使用 imageNamed:initWithFile: 进行初始化,那么您可能可以强制它加载自己加载数据,然后调用initWithData:。这种卡顿可能是由于惰性文件 I/O 造成的,因此使用数据对象初始化它不会为其提供从文件读取的选项。

The UIGraphics* methods are designed to be called from the main thread only. They are probably the source of your trouble.

You can replace UIGraphicsBeginImageContext() with a call to CGBitmapContextCreate(); it's a little more involved (you need to create a color space, figure out the right sized buffer to create, and allocate it yourself). The CG* methods are fine to run from a different thread.


I'm not sure how you're initializing UIImage, but if you're doing it with imageNamed: or initWithFile: then you might be able to force it to load by loading the data yourself and then calling initWithData:. The stutter is probably due to lazy file I/O, so initializing it with a data object won't give it the option of reading from a file.

顾忌 2024-08-19 13:46:16

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

@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

I had the same problem, even though I initialized the image using data. (I guess the data is loaded lazily, too?) I’ve succeeded to force decoding using the following category:

@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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文