使用UIImage从URL异步加载图像时出现显著的延迟

5

我正在尝试编写一个iPad应用程序,从URL加载图像。我使用以下图像加载代码:

    url = [NSURL URLWithString:theURLString];
    NSData *data = [NSData dataWithContentsOfURL:url];
    img = [[UIImage alloc] initWithData:data];
    [imageView setImage:img];
    [img release];
    NSLog(@"Image reloaded");

所有的代码都被作为操作添加到NSOperationQueue中,以便异步加载,如果图像的网络服务器较慢,不会导致我的应用程序锁定。我添加了NSLog行,以便在控制台中查看此代码何时执行完毕。
我已经注意到一致性的问题是:在代码完成执行后大约5秒钟,我的应用程序才更新图像。但是,如果我单独使用此代码而不将其放入NSOperationQueue中,则似乎几乎立即更新图像。
这种滞后并不完全是由于网络服务器缓慢...我可以在Safari中加载图像URL,只需要不到一秒钟的时间,或者我可以使用相同的代码而不使用NSOperationQueue来更快地加载它。
有没有办法在显示我的图像之前减少延迟,同时继续使用NSOperationQueue?
2个回答

6
根据文档,您编写的代码无效。 UIKit对象只能在主线程上调用。我敢打赌,您所做的在大多数情况下都可以正常工作,但未能成功更改显示,屏幕是因为某种其他原因而被更新。
如果您想保持电池效率,苹果强烈建议不要使用线程执行异步URL提取。相反,您应该使用NSURLConnection并允许运行循环组织异步行为。编写一个快速的方法来累积数据到NSData中,并在连接完成时将整个内容发布到委托可能并不难,但是假设您宁愿坚持您已经拥有的,我建议:
url = [NSURL URLWithString:theURLString];
NSData *data = [NSData dataWithContentsOfURL:url];
[self performSelectorOnMainThread:@selector(setImageViewImage:) withObject:data waitUntilDone:YES];

...

- (void)setImageViewImage:(NSData *)data
{
    img = [[UIImage alloc] initWithData:data];
    [imageView setImage:img];
    [img release];
    NSLog(@"Image reloaded");
}
performSelectorOnMainThread按照名称所示功能执行操作——将对象发送到主线程安排所请求的选择器,以对象作为单个参数,尽快在运行循环中处理。 在这种情况下,“data”是由NSOperation隐式创建的线程池中的已释放对象。 因为需要保持其有效性直到使用它,所以我使用了waitUntilDone:YES。 另一种方法是使数据成为您明确拥有并且主线程方法将其释放的内容。
此方法的主要缺点是,如果图像以压缩形式返回(例如JPEG或PNG),则会在主线程上对其进行解压缩。 为避免这种情况,而不是对UIImage行为进行经验性猜测,并超出了文档中记录的安全范围,需要降至C级别并使用CoreGraphics。 但是,我认为这超出了本问题的范围。

谢谢,汤米!我今晚会看一下你告诉我的内容,并尝试着去理解。其实,今天早上我在研究Cocoa中下载图片的不同方法时,我自己发现当我使用NSUrlRequest和NSURLConnection重新编写整个图像处理代码时,它似乎按照我期望的方式加载了图像。我仍然不确定我是否会使用我编写的新方法或者你刚刚向我展示的代码,但是有多种选择很好,而且用两种方式编写它也是一个很好的学习经验。再次感谢你的帮助,也感谢你容忍像我这样的新手! :) - Jackson
顺便说一句,如果有其他人阅读这个问题,我发现这篇文章在实现基于NSUrlRequest的图像加载器方面非常有帮助。 http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/URLLoadingSystem/Tasks/UsingNSURLConnection.html - Jackson

1
Tommy关于需要在主线程上执行所有UIKit操作是正确的。但是,如果您正在后台操作队列上运行获取操作,则无需使用NSURLConnection异步加载。此外,通过将图像解码工作保留在后台操作中,您将防止主线程在解码图像时被阻塞。
您应该能够按原样使用您的原始代码,只需将[imgView setImage:img]更改为:
[imageView performSelectorOnMainThread:@selector(setImage:)
                          withObject:img
                       waitUntilDone:NO];

哇,问题解决了。非常感谢!这么简单!虽然我还不完全确定为什么。我得去看看相关资料。现在我已经有了一个完全实现和工作的NSUrlConnection方法(它本身是异步的),以及一个使用NSObjectQueue进行异步处理的NSData/dataWithContentsOfURL方法,那么你建议我使用哪个?使用NSUrlConnection和NSData的优势分别是什么?我听说NSUrlConnection在缓存方面更灵活,并且能更好地处理错误。这是真的吗? 再次感谢! - Jackson
确实,NSURLConnection 提供了更多的灵活性。但在后台 NSOperation 中使用 NSURLConnection 的异步加载是相当棘手的,因为你需要设置自己的运行循环。你可以使用 -[NSURLConnection sendSynchronousRequest:returningResponse:error:] 方法来获得 NSURLConnection 提供的一些灵活性(见 NSMutableURLRequest),同时保持同步。这可能是我会做的,除非你有复杂的 HTTP 重定向/身份验证/缓存要求。 - Daniel Dickison
这个解决方案为什么有效的原因是,运行在主线程上的视图绘制代码需要知道你刚刚改变了图像视图的图像。通过在后台线程中调用setImage:,大概率情况下,主线程没有被通知到有新内容需要进行绘制(除非有其他东西引起视图刷新)。如果您在后台线程执行此操作,则可能会导致更糟糕的行为,例如,在主线程正在尝试绘制图像时更改图像。 - Daniel Dickison
只是澄清一下 - 我的NSData方法使用NSOperationQueue(而不是NSObjectQueue...哎呀)。但是,我的NSUrlRequest方法不使用NSObjectQueue,我理解它默认是异步的。至少在执行完成之前,它不会阻止我的应用程序运行。也许我的同步与异步的理解还有限。我基于苹果文档编写的代码提到,如果要使代码同步,需要使用特殊的函数,这让我认为它默认以某种方式异步执行。 - Jackson
糟糕——我是指NSOperationQueue(据我所知,没有NSObjectQueue这样的东西)。请参阅我早期的评论,了解如何使用NSURLConnection同步执行请求(不过它是一个类方法——“-”是一个打字错误)。 - Daniel Dickison
哈哈,我也很困惑。一开始我写成了NSOperationQueue,然后是NSObjectQueue,但我想我实际上是指的<i>NSOperationQueue</i>。 - Jackson

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