How to asynchronously load an image in an UIImageView?
Asked Answered
O

11

25

I have an UIView with an UIImageView subview. I need to load an image in the UIImageView without blocking the UI. The blocking call seems to be: UIImage imageNamed:. Here is what I thought solved this problem:

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

The image is small (150x100).

However the UI is still blocked when loading the image. What am I missing ?

Here is a small code sample that exhibits this behaviour:

Create a new class based on UIImageView, set its user interaction to YES, add two instances in a UIView, and implement its touchesBegan method like this:

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

Assign the tag 1 to one of these imageViews.

What happens exactly when you tap the two views almost simultaneously, starting with the view that loads an image? Does the UI get blocked because it's waiting for [self setImage:[UIImage imageNamed:@"woodenTile.jpg"]]; to return ? If so, how may I do this asynchronously ?

Here is a project on github with ipmcc code

Use a long press then drag to draw a rectangle around the black squares. As I understand his answer, in theory the white selection rectangle should not be blocked the first time the image is loaded, but it actually is.

Two images are included in the project (one small: woodenTile.jpg and one larger: bois.jpg). The result is the same with both.

Image format

I don't really understand how this is related to the problem I still have with the UI being blocked while the image is loaded for the first time, but PNG images decode without blocking the UI, while JPG images do block the UI.

Chronology of the events

step 0 step 1

The blocking of the UI begins here..

step 2

.. and ends here.

step 3

AFNetworking solution

    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]]]; }

Most of the time, it logs: success: {512, 512}

It also occasionnaly logs: success: {0, 0}

And sometimes: failure: <NSURLResponse: 0x146c0000> { URL: file:///var/mobile/Appl...

But the image is never changed.

Oatmeal answered 4/10, 2013 at 10:37 Comment(6)
You're missing nothing. UI changes always have to be synchronous. This is the best you can do.Mendicity
How big is the image? You'll probably find that the blocking call isn't loading the image, but rather rending the contents of the image when the image view is displayed on screen. Have you tried profiling the app to see where the bottle neck is are you just blindly assuming the image load is the issue?Polar
I was initially loading the image in a class method, only once for all instances of the class. The blocking occurs only the first time, which lead me to think that it was caused by UIImage imageNamed.Oatmeal
Use instruments to find the bottle neck.Piecedyed
See my question and answer here.Tiptop
@Tiptop That's interesting .The accepted answer looks similar to what ipmcc proposed here. But there are differences.Oatmeal
H
54

The problem is that UIImage doesn't actually read and decode the image until the first time it's actually used/drawn. To force this work to happen on a background thread, you have to use/draw the image on the background thread before doing the main thread -setImage:. This worked for me:

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];
    });
});

EDIT: The UI isn't blocking. You've specifically set it up to use UILongPressGestureRecognizer which waits, by default, a half a second before doing anything. The main thread is still processing events, but nothing is going to happen until that GR times out. If you do this:

    longpress.minimumPressDuration = 0.01;

...you'll notice that it gets a lot snappier. The image loading is not the problem here.

EDIT 2: I've looked at the code, as posted to github, running on an iPad 2, and I simply do not get the hiccup you're describing. In fact, it's quite smooth. Here's a screenshot from running the code in the CoreAnimation instrument:

enter image description here

As you can see on the top graph, the FPS goes right up to ~60FPS and stays there throughout the gesture. On the bottom graph, you can see the blip at about 16s which is where the image is first loaded, but you can see that there's not a drop in the frame rate. Just from visual inspection, I see the selection layer intersect, and there's a small, but observable delay between the first intersection and the appearance of the image. As far as I can tell, the background loading code is doing its job as expected.

I wish I could help you more, but I'm just not seeing the problem.

Hege answered 8/10, 2013 at 14:57 Comment(11)
Something is still blocking the UI. Using the UIImageView subclass I described in the question, when I don't change the image on touch event, the animation of the frame starts immediately after the touch event. Using your method, the UI is blocked for a short amount of time the first time the image needs to be drawn.Oatmeal
Then I suspect something else is causing the block. Have you used Instruments to find out what?Hege
Also, as written, the animation is going to start right away, but the image won't be set until "some time later" so that may be giving the appearance of blocking.Hege
No. The animation doesn't start right away when an image needs to be read. This seems to contradict the purpose of GCD, but when I tap two image views in quick succession, there is a visible delay before the second tapped imageView starts to shrink.Oatmeal
Something else is going on here. You should break out Instruments and find out what.Hege
When I run the code I posted, I see the initial load of the image take ~400ms (I used a big image to exaggerate the time) and the time from setting the image on view to the end of that runloop pass as 200 microseconds, so something else is doing work there and holding up your UI.Hege
I'd be careful about using the global queue with such a low priority. In the scenario the asker describes, this block may not run for some time because higher priority items are starving it.Beghtol
@Hege I have uploaded a minimal project on github that reproduces what I'm explaining. See in the question.Oatmeal
@AntoineLecaille: Looked at your code, edited my answer. Long story short, the delay is because you're using UILongPressGestureRecognizer not because of the image loading.Hege
I don't think you understand my problem. The code you provided runs when the long press gesture is already in UIGestureRecognizerStateChanged state. My selectionLayer stop redrawing and following my finger the first time an image need to be loaded, but you have to run the code on an actual iPad (mini in my case) to see this.Oatmeal
Unfortunately, when I run it using Instruments, the problem does not occur ... like an Heisenbug. It's perfectly smooth when I debug it!Oatmeal
A
5

You can use AFNetworking library , in which by importing the category

"UIImageView+AFNetworking.m" and by using the method as follows :

[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];

hope this helps .

Apoenzyme answered 14/10, 2013 at 8:2 Comment(1)
I can't tell if it's working because a perfectly fine URL that I use with [NSData dataWithContentsOfURL:url] to synchronously load the image (blocking the UI), "successfully" loads an empty image for the URL, or fails, or successfully loads the image but doesn't display it.Oatmeal
A
4

I had a very similar issue with my application where I had to download lot of images and along with that my UI was continuously updating. Below is the simple tutorial link which resolved my issue:

NSOperations & NSOperationQueues Tutorial

Arathorn answered 14/10, 2013 at 14:56 Comment(0)
C
3

this is the good way:

-(void)updateImageViewContent {
    dispatch_async(dispatch_get_main_queue(), ^{

        UIImage * img = [UIImage imageNamed:@"background.jpg"];
        [[self imageView] setImage:img];
    });
}
Costotomy answered 4/10, 2013 at 10:41 Comment(3)
Still blocking, but only the first time it is called. I have several instances of the UIView inside a UIScrollView.Oatmeal
This is not the good way. UI-changes have to be done on the main thread.Mendicity
This change IS being done on the main queue. Contrary to your earlier comment @yoeriboven, the change does NOT have to be synchronous.Beghtol
V
2

Why don't you use third party library like AsyncImageView? Using this, all you have to do is declare your AsyncImageView object and pass the url or image you want to load. An activity indicator will display during the image loading and nothing will block the UI.

Viceroy answered 4/10, 2013 at 11:23 Comment(0)
P
1

-(void)touchesBegan: is called in the main thread. By calling dispatch_async(dispatch_get_main_queue) you just put the block in the queue. This block will be processed by GCD when the queue will be ready (i.e. system is over with processing your touches). That's why you can't see your woodenTile loaded and assigned to self.image until you release your finger and let GCD process all the blocks that have been queued in the main queue.

Replacing :

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

by :

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

should solve your issue… at least for the code that exhibits it.

Petry answered 15/10, 2013 at 22:7 Comment(1)
From Apple doc "This function is the fundamental mechanism for submitting blocks to a dispatch queue. Calls to this function always return immediately after the block has been submitted and never wait for the block to be invoked. The target queue determines whether the block is invoked serially or concurrently with respect to other blocks submitted to that same queue. Independent serial queues are processed concurrently with respect to each other." Main queue happens to be a serial queue.Petry
D
0

Consider using SDWebImage: it not only downloads and caches the image in the background, but also loads and renders it.

I've used it with good results in a tableview that had large images that were slow to load even after downloaded.

Diplococcus answered 15/10, 2013 at 6:15 Comment(0)
S
0

https://github.com/nicklockwood/FXImageView

This is an image view which can handle background loading.

Usage

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];

To get an URL for your file you might find the following interesting:

Getting bundle file references / paths at app launch

Spic answered 15/10, 2013 at 6:21 Comment(1)
Still blocking the UI the first time.Oatmeal
W
0

When you are using AFNetwork in an application, you do not need to use any block for load image because AFNetwork provides solution for it. As below:

#import "UIImageView+AFNetworking.h"

And

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

Thanks

Weatherboarding answered 15/10, 2013 at 11:11 Comment(4)
It's loading the image, but it's also blocking the UI. The performance is also worse than my initial code with dispatch_async.Oatmeal
I have used it in cell and it is working fine for me without blocking GUI. But if you want than you can use lazytable apple sample code but you need to modify it according to your need....Weatherboarding
I believe you. I think it might be related to the iPad mini hardware, as it's working in simulator and @ipmcc's iPad 2 . I'll have a look at lazytableOatmeal
Okay, but if you still are facing issues then you can add load operation in the queue but it will be same as AFNetwork feature.....Weatherboarding
G
0

One way i've implemented it is the Following: (Although i do not know if it's the best one)

At first i create a queue by using Serial Asynchronous or on Parallel Queue

queue = dispatch_queue_create("com.myapp.imageProcessingQueue", DISPATCH_QUEUE_SERIAL);**

or

queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0);

**

Which ever you may find better for your needs.

Afterwards:

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

        });
Giselle answered 15/10, 2013 at 11:48 Comment(0)
D
0

There is a set of methods introduced to UIImage in iOS 15 to decode images and create thumbnails asynchronously on background thread

func prepareForDisplay(completionHandler: (UIImage?) -> Void)

Decodes an image asynchronously and provides a new one for display in views and animations.

func prepareThumbnail(of: CGSize, completionHandler: (UIImage?) -> Void)

Creates a thumbnail image at the specified size asynchronously on a background thread.

You can also use a set of similar synchronous APIs, if you need more control over where you want the decoding to happen, e.g. specific queue:
func preparingForDisplay() -> UIImage?
func preparingThumbnail(of: CGSize) -> UIImage?

Diacritical answered 10/1, 2022 at 15:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.