4

Possible Duplicate:
Table View with Images, slow load and scroll

I have a UITableView which downloads images for the UITableViewCells from a server. I observed that the tableView scrolls very slowly.

I thought that this might downloading problem, but I have realized that the table still scrolls slowly after download has finished and the image icon size is less.

I searched Google but couldn't find any help.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    btnBack.hidden = FALSE;

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil)
    {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;

        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
        cell.backgroundColor = [UIColor clearColor];

        cell.textLabel.font = [UIFont fontWithName:@"Noteworthy" size:17.0];
        cell.textLabel.font = [UIFont boldSystemFontOfSize:17.0];
        cell.textLabel.textColor = [UIColor blackColor];
        cell.textLabel.highlightedTextColor = [UIColor blackColor];
    }

        cell.textLabel.text = [NSString stringWithFormat:@"     %@", [test.arrTitle objectAtIndex:indexPath.row]];

        NSString *Path;
        Path = [NSString stringWithFormat:@"http://%@",[test.arrImages objectAtIndex:indexPath.row]];
        NSLog(@"image-->%@",[test.arrImages objectAtIndex:indexPath.row]);
        NSString *strImage = Path;
        NSURL *url4Image = [NSURL URLWithString:strImage];    
        NSData *data = [NSData dataWithContentsOfURL:url4Image];
        image =[[UIImage alloc] initWithData:data];
        cell.imageView.image =image;
        [image release];

        return cell;
}
Community
  • 1
  • 1
anil kumar
  • 53
  • 1
  • 1
  • 3
  • Have a look here http://stackoverflow.com/questions/7502073/loading-images-from-a-background-thread-using-blocks/7502281#7502281 – HeikoG Oct 03 '12 at 07:08

6 Answers6

18

While my original answer, below, attempted to solve several key problems associated with asynchronous image retrieval, it is still fundamentally limited. A proper implementation would also make sure that if you scrolled quickly, that the visible cells were prioritized over cells that had scrolled off screen. It would also support cancelation of prior requests (and canceled them for you when appropriate).

While we could add these sorts of capabilities to the below code, it is probably better to adopt an established, proven solution that utilizes the NSOperationQueue and NSCache technologies discussed below, but also addresses the above issues. The easiest solution is to adopt one of the established UIImageView categories that supports asynchronous image retrieval. The AFNetworking and SDWebImage libraries both have UIImageView categories that gracefully handle all of these issues.


You can use NSOperationQueue or GCD to do your lazy loading (see Concurrency Programming Guide for discussion of different asynchronous operations technologies). The former enjoys an advantage that you can specify precisely how many concurrent operations are permissible, which is very important in loading images from the web because many web servers limit how many concurrent requests they will accept from a given client.

The basic idea is:

  1. Submit request of the image data in a separate background queue;
  2. When done downloading image, dispatch UI update back to main queue because you should never do UI updates in the background;
  3. When running dispatched final UI update code on main queue, make sure the UITableViewCell is still visible and that it hasn't been dequeued and reused because the cell in question scrolled off the screen. If you don't do that, the wrong image may momentarily show up.

You would want to replace your code with something like the following code:

First, define a property for your NSOperationQueue that you will use for downloading images, as well as a NSCache for storing those images:

@property (nonatomic, strong) NSOperationQueue *imageDownloadingQueue;
@property (nonatomic, strong) NSCache *imageCache;

Second, initialize this queue and cache in viewDidLoad:

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.imageDownloadingQueue = [[NSOperationQueue alloc] init];
    self.imageDownloadingQueue.maxConcurrentOperationCount = 4; // many servers limit how many concurrent requests they'll accept from a device, so make sure to set this accordingly

    self.imageCache = [[NSCache alloc] init];

    // the rest of your viewDidLoad
}

Third, your cellForRowAtIndexPath might look like:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    btnBack.hidden = FALSE;

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;

        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
        cell.backgroundColor = [UIColor clearColor];

        cell.textLabel.font = [UIFont fontWithName:@"Noteworthy" size:17.0];
        cell.textLabel.font = [UIFont boldSystemFontOfSize:17.0];
        cell.textLabel.textColor = [UIColor blackColor];
        cell.textLabel.highlightedTextColor = [UIColor blackColor];
    }

    cell.textLabel.text = [NSString stringWithFormat:@"     %@", [test.arrTitle objectAtIndex:indexPath.row]];

    // code change starts here ... initialize image and then do image loading in background

    NSString *imageUrlString = [NSString stringWithFormat:@"http://%@", [test.arrImages objectAtIndex:indexPath.row]];
    UIImage *cachedImage = [self.imageCache objectForKey:imageUrlString];
    if (cachedImage) {
        cell.imageView.image = cachedImage;
    } else {
        // you'll want to initialize the image with some blank image as a placeholder

        cell.imageView.image = [UIImage imageNamed:@"blankthumbnail.png"];

        // now download in the image in the background

        [self.imageDownloadingQueue addOperationWithBlock:^{

            NSURL *imageUrl   = [NSURL URLWithString:imageUrlString];    
            NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
            UIImage *image    = nil;
            if (imageData) 
                image = [UIImage imageWithData:imageData];

            if (image) {
                // add the image to your cache

                [self.imageCache setObject:image forKey:imageUrlString];

                // finally, update the user interface in the main queue

                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    // Make sure the cell is still visible

                    // Note, by using the same `indexPath`, this makes a fundamental
                    // assumption that you did not insert any rows in the intervening
                    // time. If this is not a valid assumption, make sure you go back
                    // to your model to identify the correct `indexPath`/`updateCell`

                    UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
                    if (updateCell)
                        updateCell.imageView.image = image;
                }];
            }
        }];
    }

    return cell;
}

Fourth, and finally, while one might be inclined to write code to purge the cache in low memory situations, it turns out that it does this automatically, so no extra handling is needed here. If you manually simulate a low memory situation in the simulator, you won't see it evict its objects because NSCache doesn't respond UIApplicationDidReceiveMemoryWarningNotification, but during actual operation, when memory is low, the cache will be purged. Actually, NSCache no longer gracefully responds to low memory situations itself, so you really should add observer for this notification and empty the cache in low memory situations.

I might suggest a bunch of other optimizations (e.g. perhaps also caching images into persistent storage to streamline future operations; I actually put all of this logic in my own AsyncImage class), but first see if this solves the basic performance issue.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • thanks rob for giving code but my images are jumping from one cell to other cell while scrolling. – anil kumar Oct 04 '12 at 11:51
  • hi Rob! first of all very well written code. I used your code but having a little query my Images and other data are coming from a JSON file, and the text data is coming perfectly but images are taking loads of time to show, most of the time images do not loads. why is this so? I mean I am stuck here why it is happening..hoping for your prompt response. Thanks! – Deepak Khiwani May 07 '13 at 10:55
  • what value will come in image cache uiimage object in your code.? – sandeep tomar Feb 20 '16 at 06:29
  • because i am getting null if tried to get url in imagecache – sandeep tomar Feb 20 '16 at 06:29
  • Whatever key you use for saving the image in the cache should be the same as key for retrieving it. If you're getting `nil`, make sure your cache, itself, is not `nil`. If you're still having problems, post your own question with code samples and description of what diaristic a you have done, as it will be difficult to diagnose merely is comments here. – Rob Feb 20 '16 at 07:39
1

Write this in your UITableView cellForRowAtIndex: Method

asyncImageView = [[AsyncImageView alloc]initWithFrame:CGRectMake(30,32,100, 100)];         
[asyncImageView loadImageFromURL:[NSURL URLWithString:your url]];
[cell addSubview:asyncImageView];
[asyncImageView release];

Need to import AsyncImageView class and create Object For that Class

Hemang
  • 26,840
  • 19
  • 119
  • 186
Manu
  • 4,730
  • 2
  • 20
  • 45
  • This does _not_ seem like good application of `AsyncImageView`. After download of image is complete, proper asynchronous loading of images in tableview should be checking to make sure the cell in question is still on screen (e.g. `[self.tableview cellForRowAtIndexPath:indexPath] != nil`) before updating. If you flip through tableview quickly (esp if images are large), cells can be dequeued and reused by the time the image download is done. – Rob Oct 04 '12 at 06:28
0

Scrolling very slow because you are loading images in main thread i.e. synchronously. You can do the same in background thread i.e.asynchronously, have a look at SDWebImage.

Suresh Varma
  • 9,750
  • 1
  • 60
  • 91
tikhop
  • 2,012
  • 14
  • 32
  • thanks for quick reply am new to iPhone programing i tried this but at Add headers my Xcode doesn't shown "Other Linker Flags" setting and add the "-ObjC" flag – anil kumar Oct 03 '12 at 07:05
0

It is advisable that you store the images in an array and populate them in your viewDidLoad, and then in your cellForRowAtIndexPath: just set

cell.imageView.image = [yourImageArray objectAtIndex:indexPath.row];

As far as the slownesss is concerned, it is because you block the main thread with downloading the URLDATA in your cellForRowAtIndexPath method,so while scrolling unless and until the image is not fetched your main thread in which the application runs will get blocked.

Hemang
  • 26,840
  • 19
  • 119
  • 186
AppleDelegate
  • 4,269
  • 1
  • 20
  • 27
  • I understand the intuitive appeal of this approach, but while this solves the slow scrolling, it replaces it with a worse problem, a very slow initial loading of the tableview. This is also a pretty extravagant use of memory. Generally, most of us have gone to asynchronous lazy loading for optimal performance and memory utilization reasons. Perhaps you can refine this with some asynchronous preloading of some smart subset of the image cache (e.g. the next group of images), but that's non-trivial, and there are a bunch of other caching and optimizations I'd pursue before going there. – Rob Oct 04 '12 at 13:04
0

As said earlier: don't do any heavy lifting in cellForRowAtIndexPath You can easily get around by using GCD. loading images from a background thread using blocks

Community
  • 1
  • 1
HeikoG
  • 1,793
  • 1
  • 20
  • 29
0

You should look to use an NSOperationQueue to handle lazy loading of images and a custom tableviewcell. You can get sample example for NSOperationQueue here

Google for tweetie custom tableviewcell That should set you in the right direction.

Apple has a sample project for downloading images in tableViews: LazyTableImages

Suresh Varma
  • 9,750
  • 1
  • 60
  • 91