【问题标题】:How to asynchronously load an image in an UIImageView?如何在 UIImageView 中异步加载图像?
【发布时间】:2013-10-11 07:57:31
【问题描述】:

我有一个带有 UIImageView 子视图的 UIView。我需要在不阻塞 UI 的情况下在 UIImageView 中加载图像。阻塞调用似乎是:UIImage imageNamed:。这是我认为解决此问题的方法:

-(void)updateImageViewContent {
    dispatch_async(
        dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        UIImage * img = [UIImage imageNamed:@"background.jpg"];
        dispatch_sync(dispatch_get_main_queue(), ^{
            [[self imageView] setImage:img];
        });
    });
}

图片很小 (150x100)。

但是,加载图像时 UI 仍然被阻止。我错过了什么?

这是一个展示这种行为的小代码示例:

基于 UIImageView 创建一个新类,将其用户交互设置为 YES,在一个 UIView 中添加两个实例,并实现其 touchesBegan 方法,如下所示:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if (self.tag == 1) {
        self.backgroundColor= [UIColor redColor];
    }

    else {
    dispatch_async(dispatch_get_main_queue(), ^{
    [self setImage:[UIImage imageNamed:@"woodenTile.jpg"]];
  });
    [UIView animateWithDuration:0.25 animations:
        ^(){[self setFrame:CGRectInset(self.frame, 50, 50)];}];
    }
}

将标签 1 分配给这些 imageView 之一。

当您几乎同时点击两个视图(从加载图像的视图开始)时,究竟会发生什么? UI 是否因为等待[self setImage:[UIImage imageNamed:@"woodenTile.jpg"]]; 返回而被阻止?如果是这样,我该如何异步执行此操作?

这是一个project on github,带有ipmcc 代码

使用长按然后拖动以在黑色方块周围绘制一个矩形。据我了解他的回答,理论上白色选择矩形不应该在第一次加载图像时被阻止,但实际上是。

项目中包含两张图片(一张小的:woodenTile.jpg 和一张大的:bois.jpg)。结果是一样的。

图片格式

我不太明白这与第一次加载图像时 UI 被阻塞的问题有什么关系,但是 PNG 图像在不阻塞 UI 的情况下解码,而 JPG 图像确实阻塞了 UI .

大事记

UI 的阻塞从这里开始..

.. 到此结束。

AFNetworking 解决方案

    NSURL * url =  [ [NSBundle mainBundle]URLForResource:@"bois" withExtension:@"jpg"];
    NSURLRequest * request = [NSURLRequest requestWithURL:url];
    [self.imageView setImageWithURLRequest:request
                          placeholderImage:[UIImage imageNamed:@"placeholder.png"]
                                   success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
                                       NSLog(@"success: %@", NSStringFromCGSize([image size]));
                                   } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
                                       NSLog(@"failure: %@", response);
                                   }];

// this code works. Used to test that url is valid. But it's blocking the UI as expected.
if (false)       
if (url) {
        [self.imageView setImage: [UIImage imageWithData:[NSData dataWithContentsOfURL:url]]]; }

大多数时候,它会记录:success: {512, 512}

它偶尔也会记录:success: {0, 0}

有时:failure: <NSURLResponse: 0x146c0000> { URL: file:///var/mobile/Appl...

但图像永远不会改变。

【问题讨论】:

  • 你什么都没有。 UI 更改始终必须是同步的。这是你能做的最好的。
  • 图片有多大?您可能会发现阻塞调用不是加载图像,而是在图像视图显示在屏幕上时渲染图像的内容。您是否尝试过分析应用程序以查看瓶颈在哪里,您是否只是盲目地假设图像加载是问题?
  • 我最初是在类方法中加载图像,对于类的所有实例只加载一次。阻塞只是第一次发生,这让我认为它是由 UIImage imageNamed 引起的。
  • 使用仪器找到瓶颈。
  • 看我的问答here

标签: ios objective-c uiimageview grand-central-dispatch


【解决方案1】:

问题在于UIImage 直到第一次实际使用/绘制图像时才真正读取和解码图像。要强制这项工作在后台线程上进行,您必须在执行主线程-setImage: 之前在后台线程上使用/绘制图像。这对我有用:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    UIImage * img = [UIImage imageNamed:@"background.jpg"];

    // Make a trivial (1x1) graphics context, and draw the image into it
    UIGraphicsBeginImageContext(CGSizeMake(1,1));
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), [img CGImage]);
    UIGraphicsEndImageContext();

    // Now the image will have been loaded and decoded and is ready to rock for the main thread
    dispatch_sync(dispatch_get_main_queue(), ^{
        [[self imageView] setImage: img];
    });
});

编辑:用户界面没有阻塞。您已将其专门设置为使用 UILongPressGestureRecognizer,默认情况下,它会在执行任何操作之前等待半秒。主线程仍在处理事件,但在 GR 超时之前什么都不会发生。如果你这样做:

    longpress.minimumPressDuration = 0.01;

...您会注意到它变得更加敏捷。图片加载不是这里的问题。

编辑 2:我查看了发布到 github 上的代码,该代码在 iPad 2 上运行,但我根本不明白您所描述的问题。事实上,它非常顺利。这是在 CoreAnimation 工具中运行代码的屏幕截图:

如上图所示,FPS 最高可达 ~60FPS,并在整个手势过程中保持不变。在底部的图表中,您可以在大约 16 秒处看到第一次加载图像的时间点,但您可以看到帧速率没有下降。仅通过目视检查,我看到选择层相交,并且在第一个相交和图像外观之间有一个小的但可观察到的延迟。据我所知,后台加载代码正在按预期工作。

我希望我能帮助你更多,但我只是没有看到问题。

【讨论】:

  • 仍有一些东西阻塞了 UI。使用我在问题中描述的 UIImageView 子类,当我不更改触摸事件上的图像时,框架的动画会在触摸事件之后立即开始。使用您的方法,第一次需要绘制图像时,UI 会被阻塞一小段时间。
  • 然后我怀疑是其他原因导致了阻塞。你有没有使用 Instruments 来发现什么?
  • 另外,正如所写,动画将立即开始,但图像要到“一段时间后”才能设置,因此可能会出现阻塞。
  • 没有。当需要读取图像时,动画不会立即开始。这似乎与 GCD 的目的相矛盾,但是当我快速连续点击两个图像视图时,在第二个被点击的 imageView 开始缩小之前会有明显的延迟。
  • 我会小心使用具有如此低优先级的全局队列。在提问者描述的场景中,这个块可能会在一段时间内无法运行,因为更高优先级的项目正在饿死它。
【解决方案2】:

您可以使用AFNetworking 库,在其中通过导入类别

"UIImageView+AFNetworking.m" 并使用如下方法:

[YourImageView setImageWithURL:[NSURL URLWithString:@"http://image_to_download_from_serrver.jpg"] 
      placeholderImage:[UIImage imageNamed:@"static_local_image.png"]
               success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
                  //ON success perform 
               }
               failure:NULL];

希望这会有所帮助。

【讨论】:

  • 我无法判断它是否正常工作,因为我使用 [NSData dataWithContentsOfURL:url] 来同步加载图像(阻止 UI)的完美 URL,“成功”加载一个空图像URL,或失败,或成功加载图片但不显示。
【解决方案3】:

我的应用程序有一个非常相似的问题,我必须下载大量图像,并且我的 UI 不断更新。以下是解决我的问题的简单教程链接:

NSOperations & NSOperationQueues Tutorial

【讨论】:

    【解决方案4】:

    这是个好方法:

    -(void)updateImageViewContent {
        dispatch_async(dispatch_get_main_queue(), ^{
    
            UIImage * img = [UIImage imageNamed:@"background.jpg"];
            [[self imageView] setImage:img];
        });
    }
    

    【讨论】:

    • 仍然阻塞,但只是第一次调用。我在 UIScrollView 中有几个 UIView 实例。
    • 这不是好办法。 UI 更改必须在主线程上完成。
    • 此更改正在主队列上完成。与@yoeriboven 之前的评论相反,更改不必是同步的。
    【解决方案5】:

    为什么不使用像AsyncImageView 这样的第三方库?使用它,您所要做的就是声明您的 AsyncImageView 对象并传递您要加载的 url 或图像。在图像加载过程中会显示一个活动指示器,并且不会阻塞 UI。

    【讨论】:

      【解决方案6】:

      -(void)touchesBegan:在主线程中调用。通过调用 dispatch_async(dispatch_get_main_queue) 您只需将块放入队列中。当队列准备好时(即系统结束处理您的触摸),GCD 将处理此块。这就是为什么在您松开手指并让 GCD 处理已在主队列中排队的所有块之前,您无法看到加载和分配给 self.image 的 woodTile。

      更换:

          dispatch_async(dispatch_get_main_queue(), ^{
          [self setImage:[UIImage imageNamed:@"woodenTile.jpg"]];
        });
      

      作者:

      [self setImage:[UIImage imageNamed:@"woodenTile.jpg"]];
      

      应该可以解决您的问题……至少对于展示它的代码而言。

      【讨论】:

      • 来自Apple doc“这个函数是将块提交到调度队列的基本机制。对该函数的调用总是在块提交后立即返回,而不是等待块被调用。目标队列确定该块是串行调用还是与提交到同一队列的其他块并发调用。独立的串行队列相对于彼此并行处理。主队列恰好是串行队列。
      【解决方案7】:

      考虑使用SDWebImage:它不仅在后台下载和缓存图像,还加载和渲染它。

      我在一个 tableview 中使用它并取得了很好的效果,该 tableview 有大图像,即使下载后加载也很慢。

      【讨论】:

        【解决方案8】:

        https://github.com/nicklockwood/FXImageView

        这是一个可以处理背景加载的图像视图。

        用法

        FXImageView *imageView = [[FXImageView alloc] initWithFrame:CGRectMake(0, 0, 100.0f, 150.0f)];
        imageView.contentMode = UIViewContentModeScaleAspectFit;
        imageView.asynchronous = YES;
        
        //show placeholder
        imageView.processedImage = [UIImage imageNamed:@"placeholder.png"];
        
        //set image with URL. FXImageView will then download and process the image
        [imageView setImageWithContentsOfURL:url];
        

        要获取文件的 URL,您可能会发现以下内容很有趣:

        Getting bundle file references / paths at app launch

        【讨论】:

        • 第一次仍然阻塞 UI。
        【解决方案9】:

        当您在应用程序中使用 AFNetwork 时,您不需要使用任何块来加载图像,因为 AFNetwork 为它提供了解决方案。如下:

        #import "UIImageView+AFNetworking.h"
        

        还有

           Use **setImageWithURL** function of AFNetwork....
        

        谢谢

        【讨论】:

        • 它正在加载图像,但它也阻塞了 UI。性能也比我使用 dispatch_async 的初始代码差。
        • 我在单元格中使用过它,它对我来说工作正常,不会阻塞 GUI。但是如果你想要比你可以使用lazytable苹果示例代码但是你需要根据你的需要修改它......
        • 我相信你。我认为这可能与 iPad mini 硬件有关,因为它在模拟器和@ipmcc 的 iPad 2 中工作。我去看看lazytable
        • 好的,但是如果您仍然遇到问题,那么您可以在队列中添加加载操作,但这将与 AFNetwork 功能相同.....
        【解决方案10】:

        我实现它的一种方法是:(虽然我不知道它是否是最好的)

        首先我使用串行异步或并行队列创建一个队列

        queue = dispatch_queue_create("com.myapp.imageProcessingQueue", DISPATCH_QUEUE_SERIAL);**
        
        or
        
        queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0);
        

        **

        您可能会发现哪个更适合您的需求。

        之后:

         dispatch_async( queue, ^{
        
        
        
                    // Load UImage from URL
                    // by using ImageWithContentsOfUrl or 
        
                    UIImage *imagename = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
        
                    // Then to set the image it must be done on the main thread
                    dispatch_sync( dispatch_get_main_queue(), ^{
                        [page_cover setImage: imagename];
                        imagename = nil;
                    });
        
                });
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-06-25
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多