50

Shortly, I have an NSDictionary with urls for images that I need to show in my UITableView. Each cell has a title and an image. I had successfully made this happen, although the scrolling was lagging, as it seemed like the cells downloaded their image every time they came into the screen. I searched for a bit, and found SDWebImage on github. This made the scroll-lagg go away. I am not completely sure what it did, but I believed it did some caching. But! Every time I open the app for the first time, I see NO images, and I have to scroll down, and back up for them to arrive. And if I exit the app with home-button, and open again, then it seemes like the caching is working, because the images on the screen are visible, however, if I scroll one cell down, then the next cell has no image. Until i scroll past it and back up, or if I click on it. Is this how caching is supposed to work? Or what is the best way to cache images downloaded from the web? The images are being updated rarily, so I was close to just import them to the project, but I like to have the possibility to update images without uploading an update..

Is it impossible to load all the images for the whole tableview form the cache(given that there is something in the cache) at launch? Is that why I sometimes see cells without images?

And yes, I'm having a hard time understanding what cache is.

--EDIT--

I tried this with only images of the same size (500x150), and the aspect-error is gone, however when I scroll up or down, there are images on all cells, but at first they are wrong. After the cell has been in the view for some milliseconds, the right image appears. This is amazingly annoying, but maybe how it has to be?.. It seemes like it chooses the wrong index from the cache at first. If I scroll slow, then I can see the images blink from wrong image to the correct one. If I scroll fast, then I believe the wrong images are visible at all times, but I can't tell due to the fast scrolling. When the fast scrolling slows down and eventually stops, the wrong images still appear, but immediately after it stops scrolling, it updates to the right images. I also have a custom UITableViewCell class, but I haven't made any big changes.. I haven't gone through my code very much yet, but I can't think of what may be wrong.. Maybe I have something in the wrong order.. I have programmed much in java, c#, php etc, but I'm having a hard time understanding Objective-c, with all the .h and .m ... I have also `

@interface FirstViewController : UITableViewController{

/**/
NSCache *_imageCache;
}

(among other variables) in FirstViewController.h. Is this not correct?

Here's my cellForRowAtIndexPath.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"hallo";
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

if (cell == nil)
{
    cell = [[CustomCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}

NSMutableArray *marr = [hallo objectAtIndex:indexPath.section];
NSDictionary *dict = [marr objectAtIndex:indexPath.row];

NSString* imageName = [dict objectForKey:@"Image"];
//NSLog(@"url: %@", imageURL);

UIImage *image = [_imageCache objectForKey:imageName];

if(image)
{
    cell.imageView.image = image;
}
else
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        NSString* imageURLString = [NSString stringWithFormat:@"example.com/%@", imageName];
        NSURL *imageURL = [NSURL URLWithString:imageURLString];
        UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:imageURL]];

        if(image)
        {
            dispatch_async(dispatch_get_main_queue(), ^{
                CustomCell *cell =(CustomCell*)[self.tableView cellForRowAtIndexPath:indexPath];
                if(cell)
                {
                    cell.imageView.image = image;
                }
            });
            [_imageCache setObject:image forKey:imageName];
        }
    });
}

cell.textLabel.text = [dict objectForKey:@"Name"];

return cell;
}
Venk
  • 5,949
  • 9
  • 41
  • 52
Sti
  • 8,275
  • 9
  • 62
  • 124
  • 1
    can you post your code in `cellRowRowAtIndexPath` and anything else that might be relevant here? Caching is just storing something you expect to need to access again in an easier to reach place to speed up subsequent access. In this case, caching means that your app should actually store the image data for each cell after downloading it the first time, and then just re-using it any time that particular cell needs to be displayed (which is what SDWebImage should be doing for you). – Dima Jul 16 '12 at 19:56
  • Done. I have switched out SDWebImage, but still getting some weird results.. Sometimes the wrong image is displayed while scrolling.. – Sti Jul 17 '12 at 04:46
  • 1
    Just initialize `cell.imageView.image` right before that first `dispatch_async`. – Rob Jul 17 '12 at 12:42
  • checkout UIImageLoader https://github.com/gngrwzrd/UIImageLoader - this helps with this exact situation. – gngrwzrd Dec 22 '15 at 23:40
  • I know it's a bit late, but we've released an open source lib that solves the task perfectly — https://github.com/Alterplay/APSmartStorage . It gets a file from Internet and stores it smartly on disk and in memory. – slatvick Feb 10 '14 at 13:32

7 Answers7

78

Caching just means keeping a copy of the data that you need so that you don't have to load it from some slower source. For example, microprocessors often have cache memory where they keep copies of data so that they don't have to access RAM, which is a lot slower. Hard disks often have memory caches from which the file system can get much quicker access to blocks of data that have been accessed recently.

Similarly, if your app loads a lot of images from the network, it may be in your interest to cache them on your device instead of downloading them every time you need them. There are lots of ways to do that -- it sounds like you already found one. You might want to store the images you download in your app's /Library/Caches directory, especially if you don't expect them to change. Loading the images from secondary storage will be much, much quicker than loading them over the network.

You might also be interested in the little-known NSCache class for keeping the images you need in memory. NSCache works like a dictionary, but when memory gets tight it'll start releasing some of its contents. You can check the cache for a given image first, and if you don't find it there you can then look in your caches directory, and if you don't find it there you can download it. None of this will speed up image loading on your app the first time you run it, but once your app has downloaded most of what it needs it'll be much more responsive.

Caleb
  • 124,013
  • 19
  • 183
  • 272
  • 1
    Wow, great answer. I will check out the NSCache. But I can't imagine how it works.. If I create an NSCache-object in lets say viewDidLoad, wouldn't it be empty every time I launch my app? Or does it work in some other way maybe.. How can I access my previous cache-object..? Well, I'll read up on it in the morning. Thanks! – Sti Jul 17 '12 at 01:20
  • 1
    Yes, it would. Read more, though, and you'll see that it has a delegate method that's called before it removes an object -- you could use that to save objects to secondary storage. Or you could implement your own disk-based cache and leave NSCache out of the equation. – Caleb Jul 17 '12 at 01:37
  • Hi Caleb, great answer, I have a note though. How can RAM access be SLOWER than hard disk access. As far as I know, it's the opposite, RAM address access is faster than any other memory storage way. Thanks. – Malloc Jun 06 '13 at 23:12
  • @Malloc You seem to have misunderstood; I didn't say that disks are faster than RAM. Processors often have caches so that they don't have to access RAM, and such caches are built using faster memory than what's used for main memory. Similarly, disks often include solid state caches that are faster than the disk. This saves time because of *locality of reference*, i.e. the idea that if the processor asks for one piece of data, there's a higher probability that it'll want that data again or some data in close proximity. – Caleb Jun 07 '13 at 01:02
  • @Malloc Also, since the OP is asking with respect to iOS, it's worth noting that there's no actual disk involved -- it's all solid state. Still, there's a range of speeds... In decreasing order of speed: registers, processor cache, main memory, secondary storage, network. – Caleb Jun 07 '13 at 01:08
  • I have found that a huge issue with using NSCache is that it releases all its memory when entering backgroundmode. Which means that the images need to be fetched over again. Not optimal in my opinion – Pedroinpeace Nov 19 '14 at 08:43
  • If URLCache does all the caching just by following the cachePolicy then how come there are some thirdy-party APIs for image caching? What is it that they offer than doesn't come out of the box? – mfaani Oct 15 '18 at 16:06
25

I think Caleb answered the caching question well. I was just going to touch upon the process for updating your UI as you retrieve images, e.g. assuming you have a NSCache for your images called _imageCache:

First, define an operation queue property for the tableview:

@property (nonatomic, strong) NSOperationQueue *queue;

Then in viewDidLoad, initialize this:

self.queue = [[NSOperationQueue alloc] init];
self.queue.maxConcurrentOperationCount = 4;

And then in cellForRowAtIndexPath, you could then:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ilvcCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    // set the various cell properties

    // now update the cell image

    NSString *imagename = [self imageFilename:indexPath]; // the name of the image being retrieved

    UIImage *image = [_imageCache objectForKey:imagename];

    if (image)
    {
        // if we have an cachedImage sitting in memory already, then use it

        cell.imageView.image = image;
    }
    else
    {
        cell.imageView.image = [UIImage imageNamed:@"blank_cell_image.png"];

        // the get the image in the background

        [self.queue addOperationWithBlock:^{

            // get the UIImage

            UIImage *image = [self getImage:imagename];

            // if we found it, then update UI

            if (image)
            {
                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    // if the cell is visible, then set the image

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

                [_imageCache setObject:image forKey:imagename];
            }
        }];
    }

    return cell;
}

I only mention this as I've seen a few code samples floating around on SO recently that use GCD to update the appropriate UIImageView image property, but in the process of dispatching the UI update back to the main queue, they employ curious techniques (e.g., reloading the cell or table, just updating the image property of the existing cell object returned at the top of the tableView:cellForRowAtIndexPath (which is a problem if the row has scrolled off the screen and the cell has been dequeued and is being reused for a new row), etc.). By using cellForRowAtIndexPath (not to be confused with tableView:cellForRowAtIndexPath), you can determine if the cell is still visible and/or if it may have scrolled off and been dequeued and reused.

primax79
  • 428
  • 2
  • 14
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thank you so much for the answer, and the code! I tried the code, but something weird happened.. When i scroll fast, some of the aspects on some of the images are wrong..It seemes like the aspect of the image below is transferred to the image over it, or the other way around.. Is this avoidable, or do I have to make all the Images with the same size?(or at least same aspect).. – Sti Jul 17 '12 at 02:09
  • @StianFlatby That's strange. You're seeing different behavior when you scroll slowly versus when you scroll fast? And are you doing any configuration of your imageview outside of that dispatch_async to the main queue? I've done a lot of testing, but I have to admit that all of images for my tableviews are a consistent size (and small thumbnail images which are optimized for tableviews ... big images in tableviews will always have horrible performance). Maybe you can update your question with your full source code for your `cellForRowAtIndexPath`. – Rob Jul 17 '12 at 03:03
  • @StianFlatby I just tested it against a library of mixed sized images and to me it appears that (a) the difference in cell rendition was a function of whether the image was loaded from cache (synchronously on main queue) or not (asynchronously dispatch from background queue), not the speed of the scroll; and (b) it looks like if you do it synchronously, the default cell is reformatted based upon the size/presence of images, so, you'd either want to create your own custom cell and thus you can configure your imageView any way you want or just resize your images to some consistent size. – Rob Jul 17 '12 at 03:34
  • Thank you for the effort! I have updated my question with new information and some code. I appreciate if you take a look and see if you see anything wrong. – Sti Jul 17 '12 at 04:00
  • You are probably seeing the image from the dequeued cell, so you might want to initialize the `cell.imageView.image` before asynchronously going off and trying to retrieve the new one. So, just initialize before doing that. I've changed my code snippet accordingly. – Rob Jul 17 '12 at 12:40
  • so that means that instead of showing the wrong image, it will show no image? I don't think this will solve that problem.. This also happens when all the correct images has been loaded correctly. I scroll slowly down the entire tableview, and I can confirm that the correct images appear on all cells, but when I scroll fast back up, it shows the wrong images. Also at semi-fast scroll the images updates visibly. This means that the image exists in the cache, so it shouldnt be the blank_cell-problem..? This must be in if(image){}, not in the else{}. Maybe I'm wrong. I'll try it now – Sti Jul 17 '12 at 14:38
  • @StianFlatby When you scroll, rows that are scrolled off of the view have their cells recycled for use by other rows. Thus when a cell is dequeue and reused, the old image is still there. So, if you decide your new cell doesn't have a cached image, before you go off to get the new image in the background, you'll want to make sure that you get rid of any old image sitting there or else you'll see the old image (for the old, dequeued row) sitting there for the moments it takes to download the new image. – Rob Jul 17 '12 at 14:41
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/14001/discussion-between-stian-flatby-and-robert-ryan) – Sti Jul 17 '12 at 14:41
14

The simplest solution is to go with something heavily used that has been stress tested.

SDWebImage is a powerful tool that helped me solve a similar problem and can easily be installed w/ cocoa pods. In podfile:

platform :ios, '6.1'
pod 'SDWebImage', '~>3.6'

Setup cache:

SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image)
{
    // image is not nil if image was found
}];

Cache image:

[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];

https://github.com/rs/SDWebImage

Matt Perejda
  • 509
  • 5
  • 14
1

I think will be better for you user something like DLImageLoader. More info -> https://github.com/AndreyLunevich/DLImageLoader-iOS

[[DLImageLoader sharedInstance] loadImageFromUrl:@"image_url_here"
                                   completed:^(NSError *error, UIImage *image) {
                                        if (error == nil) {
                                            imageView.image = image;
                                        } else {
                                            // if we got an error when load an image
                                        }
                                   }];
MaeSTRo
  • 355
  • 2
  • 6
  • 18
1

For the part of the question about wrong images, it's because of the reuse of cells. Reuse of cells means that the existing cells, which go out of view (for example, the cells which go out of the screen in the top when you scroll towards the bottom are the ones coming back again from the bottom.) And so you get incorrect images. But once the cell shows up, the code for fetching the proper image executes and you get the proper images.

You can use a placeholder in 'prepareForReuse' method of the cell. This function is mostly used when you need to reset the values when the cell is brought up for reuse. Setting a placeholder here will make sure you won't get any incorrect images.

Swasidhant
  • 11
  • 2
0

Caching images can be done as simply as this.

ImageService.m

@implementation ImageService{
    NSCache * Cache;
}

const NSString * imageCacheKeyPrefix = @"Image-";

-(id) init {
    self = [super init];
    if(self) {
        Cache = [[NSCache alloc] init];
    }
    return self;
}

/** 
 * Get Image from cache first and if not then get from server
 * 
 **/

- (void) getImage: (NSString *) key
        imagePath: (NSString *) imagePath
       completion: (void (^)(UIImage * image)) handler
    {
    UIImage * image = [Cache objectForKey: key];

    if( ! image || imagePath == nil || ! [imagePath length])
    {
        image = NOIMAGE;  // Macro (UIImage*) for no image 

        [Cache setObject:image forKey: key];

        dispatch_async(dispatch_get_main_queue(), ^(void){
            handler(image);
        });
    }
    else
    {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0ul ),^(void){

            UIImage * image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[imagePath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]];

            if( !image)
            {
                image = NOIMAGE;
            }

            [Cache setObject:image forKey: key];

            dispatch_async(dispatch_get_main_queue(), ^(void){
                handler(image);
            });
        });
    }
}


- (void) getUserImage: (NSString *) userId
           completion: (void (^)(UIImage * image)) handler
{
    [self getImage: [NSString stringWithFormat: @"%@user-%@", imageCacheKeyPrefix, userId]
         imagePath: [NSString stringWithFormat: @"http://graph.facebook.com/%@/picture?type=square", userId]
        completion: handler];
}

SomeViewController.m

    [imageService getUserImage: userId
                    completion: ^(UIImage *image) {
            annotationImage.image = image;
    }];
Taku
  • 5,639
  • 2
  • 42
  • 31
  • Have you tested this code and does it work? Just going over the code seems like it won't work as your main method does a check `if( ! image || imagePath == nil || ! [imagePath length])` if image doesn't exist, then sets a nonexistant image to the cache (bad practice). Otherwise (else condition), if the image exists you are synchronously fetching it??? – Stunner Dec 24 '18 at 02:27
0
////.h file

#import <UIKit/UIKit.h>

@interface UIImageView (KJ_Imageview_WebCache)


-(void)loadImageUsingUrlString :(NSString *)urlString placeholder :(UIImage *)placeholder_image;

@end

//.m file


#import "UIImageView+KJ_Imageview_WebCache.h"


@implementation UIImageView (KJ_Imageview_WebCache)




-(void)loadImageUsingUrlString :(NSString *)urlString placeholder :(UIImage *)placeholder_image
{




    NSString *imageUrlString = urlString;
    NSURL *url = [NSURL URLWithString:urlString];



    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,     NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *getImagePath = [documentsDirectory stringByAppendingPathComponent:[self tream_char:urlString]];

    NSLog(@"getImagePath--->%@",getImagePath);

    UIImage *customImage = [UIImage imageWithContentsOfFile:getImagePath];




    if (customImage)
    {
        self.image = customImage;


        return;
    }
    else
    {
         self.image=placeholder_image;
    }


    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *uploadTask = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {


        if (error)
        {
            NSLog(@"%@",[error localizedDescription]);

           self.image=placeholder_image;

            return ;
        }

        dispatch_async(dispatch_get_main_queue(), ^{


            UIImage *imageToCache = [UIImage imageWithData:data];



            if (imageUrlString == urlString)
            {

                self.image = imageToCache;
            }



            [self saveImage:data ImageString:[self tream_char:urlString]];

        });




    }];

    [uploadTask resume];



}


-(NSString *)tream_char :(NSString *)string
{
    NSString *unfilteredString =string;

    NSCharacterSet *notAllowedChars = [[NSCharacterSet characterSetWithCharactersInString:@"!@#$%^&*()_+|abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"] invertedSet];
    NSString *resultString = [[unfilteredString componentsSeparatedByCharactersInSet:notAllowedChars] componentsJoinedByString:@""];
    NSLog (@"Result: %@", resultString);



    return resultString;

}

-(void)saveImage : (NSData *)Imagedata ImageString : (NSString *)imageString
{

    NSArray* documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES);

    NSString* documentDirectory = [documentDirectories objectAtIndex:0];
    NSString* documentDirectoryFilename = [documentDirectory stringByAppendingPathComponent:imageString];




    if (![Imagedata writeToFile:documentDirectoryFilename atomically:NO])
    {
        NSLog((@"Failed to cache image data to disk"));
    }
    else
    {
        NSLog(@"the cachedImagedPath is %@",documentDirectoryFilename);
    }

}

@end


/// call 

 [cell.ProductImage loadImageUsingUrlString:[[ArrProductList objectAtIndex:indexPath.row] valueForKey:@"product_image"] placeholder:[UIImage imageNamed:@"app_placeholder"]];
codercat
  • 22,873
  • 9
  • 61
  • 85