HJCache: iPhone cache library for asynchronous image loading and caching.

HJCache is an iOS library that makes it very easy to asynchronously load images from URLs, display them in smooth scrolling tables, and cache the images in file storage. Think about any app that loads lots of images over the net and displays them as you use the app: eg scrolling through a list of tweets or facebook posts; swiping through a photo album from a remote server. For apps like that you want the images to load asynchronously in the background so that a slow network connection doesn’t make the UI freeze. You also maybe want :

  • To cache them locally so they appear faster the next time the app is used;
  • The cache can trim its size;
  • The images to be shared within the app, eg so that if you are looking at a list of posts by the same person, their profile pic is loaded and just once and the UIImage object is shared;
  • That interrupted downloads don’t mess up the cache with incomplete images;
  • That normal memory management just works, even though an object in one place may also be shared in other places, and in different parts of the cache.
  • Restrict use of the network to images that are on the screen now, if for example you scroll though a long table of images, the one’s that the user scrolled past and are not off the top of the screen don’t need to be loaded.
  • Allow images to finish loading to the cache in some cases, even if the views they will be used in are removed because the user went back to a previous screen right away.
  • Sometimes load a few image ahead of when they are needed, eg for a swipe through photo album.

HJCache implements all this and more. (BTW, HJCache was first called HJ Object Manager, it’s the same things and the class names didn’t change.)

HJCache is free to use in free or paid iOS and Mac apps, and its been in use in one form or another since 2009 in several commercial iPhone apps made by us. For example we use it in our own app Focus for Facebook. The basis of HJCache is explained in this blog post ‘Asynchronous Image Loading in UITableView‘, but its come a long way since then.

The design goals of this library are, in order:

  1. Make it easy to use asynchronously loaded images in iOS apps.
  2. Make it easy to use a shared file cache.
  3. Make memory management ‘just work’ according to Cocoa norms.
  4. Support typical cases of viewing images, eg scrolling through a list of tweets showing the tweeters profile pic, swiping through a photo album loaded over the net.
  5. Allow the library to be used for other kinds of data too.

How to Use HJCache

To start with, somewhere in your app initialization, you create a shared instance of the object manager HJObjManager, and if you want to use file caching you also create an HJMOFileCache for it to use:

objMan = [[HJObjManager alloc] init];
//if you are using for full screen images, you'll need a smaller memory cache:
//objMan = [[HJObjManager alloc] initWithLoadingBufferSize:2 memCacheSize:2]];

NSString* cacheDirectory = [NSHomeDirectory() stringByAppendingString:@"/Library/Caches/imgcache/flickr/"] ;
HJMOFileCache* fileCache = [[[HJMOFileCache alloc] initWithRootPath:cacheDirectory] autorelease];
objMan.fileCache = fileCache;

HJManagedImageV is a subclass of UIView used for showing images that are being managed (asynchronously loaded from a url, share UIImage objects, and cache image data in files). You can use an instance of an HJManagedImageV anywhere you would normally use a UIImageView, including in Interface Builder.

To get your managed image to load and display, you set its url, and then tell the object manager to manage it:

managedImage.url = imageUrlForCurrentRow;
[self.objectManager manage:managedImage];
//NB, the library wants to be used from the main thread, so if you're not in the main thread, use:
//[self.objectManager performSelectorOnMainThread:@selector(manage:) withObject:managedImage waitUntilDone:YES];

That’s all there is to it! The object manager keeps track of the views that are using the shared UIImage objects, and as long as you do normal memory management of your managed image objects just like any other UIView, then the object manager will clean up everything once its no longer on the screen, or cache a certain number of UIImages in memory for reuse, and/or cache the images in files.

Please note that HJManagedImageV is really a user of HJCache, its a class that has an object in it that’s managed by the object manager. It won’t do everything that everyone needs to display and interact with cached images for all apps. If it doesn’t do what you need, just subclass and modify, or make your own version, using HJManagedImageV and the documentation in the header files to guide you. HJCache can be used for more than just images too. Again, take a look at the simple HJManagedImageV class to see how you can make your own classes that use managed objects. The hard work is done in HJObjManager, the handler and the file cache, which are all agnostic about what kind of objects they are managing. It shouldn’t be too scary to replace HJManagedImageV with something that does what you need.

Getting HJCache

HJCache is up on github here: https://github.com/markofjohnson/HJCache. The distribution includes some very simple demo apps. (Don’t use these apps to see how to construct great looking tables with images because they don’t demonstrate good practices for customizing UITableViewCell, but you can use them to see how to use the lib). Note, this lib isn’t just for putting dynamically loaded images in tables, it can be used with images in any kind of view, and it can be used for non-image data too.

Please respond in the comments with questions, let us know what you think, report bugs, and please post a comment telling us about your app thats using HJCache. I’m pretty busy, so I won’t respond to questions like ‘how do I use UITableView’. If you are a beginner, check out all the awesome books on iOS programming. We (Hunter and Johnson) are a boutique consultancy providing design, programming and marketing of iOS apps, and we publish our own apps too. If you are a company building something with HJCache, we can make some consulting hours available to you if you need help.

If I get time, I might write detailed documentation showing step by step how to use all the features, and how to customize the library to do really neat stuff like automagically resize images and add rounded corners. If and when I do so, the detailed documentation will probably be packaged as an ebook for sale.

Main Classes of HJ Managed Objects

HJObjManager is the object manager class. You typically make one shared instance of this class. It encapsulates all the sharing and caching. If you need separate caches that work differently or store different data you can have more than one. HJObjManager can be given an instance of a HJMOFileCache if you want to cache data in files during and between runs of the app. Internally the object manager has a list of active managed objects, ie ones that are currently being used in the app or are being loaded. It’s a FIFO list of configurable size, its function is to limit how many shared objects are live and being loaded at once. When an object gets booted off the end of the list its loading is cancelled. This means that the object manager seamlessly deals with a large table of images where the user is quickly scrolling through many screen fulls of images and it focuses on loading the images on the screen when the user stops scrolling. This also makes it easy to do stuff like photo album viewers that load images ahead of where the user is without having to keep track of everything manually.

HJMOFileCache does all of the file caching for the object manager. Its a separate class so that you can replace it with your own caching code if you need to. It has a trim method that can be used to limit the file cache size. Note that trimming the cache takes longer the larger the cache is. There’s another cache class, HJMOBigFileCache, which is better if you want your cache size to be thousands of files big. It scales better because it only trims 1/10th of the cache at a time.

HJMOUser is a protocol that is implemented by any class that has a managed object that you want to use with HJCache. You can write your own classes to be managed by implementing HJMOUser, there’s a base class you can optionally extend: HJMOUserBase. An HJMOUser has the following responsibilities:

  • Provide a URL for the data you want to load
  • Optionally provide an oid ‘object id’ that is used to identify the shared data, instead of the url. The oid is useful for cases where the same data can be loaded from different urls, which is sometimes the case due to websites load balancing design.
  • Provide a changeManagedObjStateFromLoadedToReady method. This method is called by the handler object to turn data loaded from the url into the actual managed object that gets shared and cached. For example, make a UIImage from an NSData. This is the mechanism that allows HJCache to manage any kind of data / object, not just UIImage.
  • Provide managedObjReady and managedObjFailed methods so the HJMOUser can be told when its shared managed object is ready to use.
  • Store a reference to an HJMOHandler object which is set by the object manager, so that in the dealloc method it can call [moHandler removeUser:self]; . This is necessary to make memory management within HJCache work.

To load data using the library, an instance of a HJMOUser object is made for the url you need to load from, and then its passed to a shared object manager: [myObjectManager manage:myManagedObject]; The callback methods are the called when the managed object is loaded and ready, or if there is an error.

HJManagedImageV is an a subclass of UIView that also implements the HJMOUser protocol, and does everything needed to display UIImages with the managed object framework. HJManagedImageV can be used in Interface Builder in place of a UIView. If you want to use HJCache for images, you should probably use it or a customized version of it. Also use this class a reference if you want to use HJCache with non-image data, so you can see how to make your own HJMOUser classes.

HJMOHandler is where much of the code is. There’s one for every shared object and it takes care of loading data from URLs, etc. For the most part HJMOHandlers stay encapsulated inside HJObjManager, so users of the library don’t need to worry much about them. The handlers internally use a weak mutable array of HJMOUsers that are using the shared object its handling. The handlers have references to the users, and the users have reference to the handler, but because weak references are used in the handler, HJMOUser objects will get dealloced as normal if they are coded using normal memory management practices. The dealloc method of the HJMOUser object needs to call [moHandler removeUser:self] so that the handler and the object manager can manage their memory. This design decouples the memory management of the shared objets from the user objects, so programmers can just treat user objects like any other objective-C object or UIView, even though there’s some hairy shared and cached objects underneath.

Comments

  1. moosc says:

    Why no put the code in github or similar page?

  2. sph says:

    First of all thanks for releasing this.
    I added the objects to my project, which is using Core Data to save the table data. It also uses HJManagedImageV in a table cell, just like the flickr sample (nothing more).
    The problem lies within the first run when the data is first fetched and the imageURL is set (cellForRowAtIndexPath) . startDownloadingFromURL (in HJMOHandler) is called, but didReceiveData is never called, thus no images are set.
    When I restart the application and the table is filled with data (due to Core Data), the images are set properly, because didReceiveData and connectionDidFinishLoading are called.
    Maybe you have some idea what is going wrong. I could send the project by mail if you would like to have a look.

    Thanks

    • markj says:

      Good question! The library needs to be used from the main thread, something to do with how NSURLConnections schedule to do the actual loading. If you need to call it from code that isn’t running on the main thread, then:
      [appDelegate.objMan performSelectorOnMainThread:@selector(manage:) withObject:imageView waitUntilDone:YES];

      instead of
      [appDelegate.objMan manage:imageView]

  3. Thanks for sharing this, might need it soon!

  4. Tony says:

    I was just started to tackle this for my current project, began doing my research online first, of course … found an older page (http://www.markj.net/iphone-asynchronous-table-image/), read through all the comments — and man am I happy to see this updated library released now! Props to you for contributing — the timing couldn’t be better!

  5. james says:

    How would I apply a mask to the image? What would be the way to approach it? thanks.

    • markj says:

      To mask the image you’ll want to write your own version of HJManagedImageV. There are two ways to do it. Do you want to download an image, then mask it (round corners right?), then cache it, then draw it. Or, do you want to download it, then cache it, then draw it with rounded corners? I recommend the later, its just easier, and you’ll have the original cached in case you need to draw it at different sizes in different views. So to do this, just write a custom drawRect: method to directly draw the image to the view using a mask (from a png file). This technique is very quick, and you can use it to make masked images in table cells that will scroll smoothly even on first generation iPhones. You’ll need something like this in your drawRect:

      CGRect rect = CGRectMake(6,-28-50,50,50);
      UIImage* mask = [UIImage imageNamed:@"50x50mask.png"];
      CGContextSaveGState(context);
      CGContextClipToMask(context,rect,mask.CGImage);
      CGContextDrawImage(context, rect, self.photo.image.CGImage);
      CGContextRestoreGState(context);

      The coordinates are all funcky when you are drawing directly into CGContext, its all upside down.

  6. paul_1991 says:

    Hi!!! Very nice work!!! Thanks for sharing!
    I have a small question… is there a method to clear the content of the imageview? thanks

  7. Gary says:

    Wow, this is great! How can I send you a donation?

  8. Virrey06 says:

    Thanks for this awesome library!!. The old blog post helped us a lot!, I ‘m looking forward to use it in my next projects!.

  9. Natan Alves says:

    Thanks for this Class, i used in my project and worked perfectly.
    My doubt is how can i use that image loaded into my custom cell to use in another view. Example, the user select a row in the table and the image that is loaded is used on the another view that will be place. Is possible?
    Hope you understand, my english is not to good.
    []s

    • markj says:

      Its easy, just use another HJManagedImageV object and set the oid or url to the same. So you have 2 different instances of HJManagedImageV, but because they have the same oid (or just same url if you are not using oid) they will both _share_ the same UIImage object. They will both share the same image, but neither of them have to know about the other, so it doesn’t matter if one view goes away and its HJManagedImageV gets deleted, the shared image that being managed will just work. Making this work is a major reason for HJCache in the first place. When the 2nd managed image is used, HJCache will either get the actual image from the first managed image view, or it will get it from the in memory cache, or if you are using a file cache it will get it from the file system. Does that make sense?

      If you comment in the non Flickr example in the demo project, you can see that the table cells all have their own HJManagedImageV objects, but they end up sharing the same images.

  10. Natan Alves says:

    Thanks for the reply Markj, worked perfectly.
    Thanks!

  11. John says:

    I’m trying to use your libraries with a custom table cell to display the images and text. It seems to ignore the custom cell object and display the UITableCell regardless.

    Thoughts?

    • markj says:

      Well, if you put an HJManagedImageV into your custom table cell just like any other view, it should just work if your table code and custom cell code is correct?

  12. jjacob says:

    Thanks for contributing this. Huge help. One question, though:

    I have an iPad app that is showing a large number of full screen images in a scroll view. I’m using the 3-wide technique (current image, one left and one right and recentering the scroll view after the scroll). I have three HJManagedImageV instances and am trying to reuse them by calling clear, setting to the new URL and then telling the manager to manage again. My problem is that I have a memory leak doing this. Is this anything you guys tried or should I stick with deallocing/reallocing HJManagedImageV instances instead of reusing them?

    Thanks again!

    • markj says:

      HJObjManager has an internal memory cache (default size 20) and an internal buffer of ‘actively loading’ managed objects (default size 12).
      Try sizing the cache to just 1 or 2, and the number of active loading images 1 either 1 or 2 by using the initializer:

      objMan = [[HJObjManager alloc] initWithLoadingBufferSize:2 memCacheSize:2] ];

      0 will be good enough for the memory cache if you are using the file cache. The loading buffer size should always be 1 or greater. In fact it should always be at least as large as the number of images on the screen at the same time. So for a full screen photo browser it can be 1. If you set the loading buffer to 2 then you can also start loading the next photo before you display it, to make the app more responsive as users look through one by one. If you set the memory buffer to 3, the you can keep the UIImage in memory 1 for the current image and 1 for each side, to keep flicking back and forward snappy, though the iPad is pretty quick at loading images from file on demand. We use these techniques with HJCache in our iPhone app Focus for Facebook in the photo viewer, which also uses a 3 wide scroll view but with HJCache letting us easily to load ahead. See what works best for you and write back telling us how it works out.

  13. Sommer says:

    Hi. Thanks for sharing this great work. I have the same question as James above that wanted to make a mask for the image. I want to do even more image operations that do take some time (resizing and cropping), thus making the table less responsive if done after the caching (I also have a lot of images in a grid). Do you have some hint on where best to start modifying your code (or a subclass) to do the image operations before it is stored in the cache?

    • markj says:

      This version of HJCache doesn’t currently have the right hooks inside the object manager for the cache to store different versions of the same image (original and unmodified), though that’s a feature we had in a predecessor version HJCache. Probably the easiest way to make HJCache do what you need is to modify the HJManagedImage to modify the UIImage at the same time it converts the NSData to the UIImage managed object. Then any other HJManagedImage that uses the same url/oid will get to share the modified image if the image is in use elsewhere or if its in the object managers memory cache. You probably want to cache the modified image on file too? If so have your version of HJManagedImage write over the file cache data with the modified data. readyFilePathForOid: will give you the path to the file to overwrite. You can reach the HJCache object from HJManagedImageV with moHandler.objManager. Let me know how that works out for you. Hmm, but then the next time you use the image from file cache it you’ll have to test if its already the modified size or not, so that you don’t end up modifying it twice.

  14. markj says:

    I just plugged a dumb memory leak, and pushed to git.
    https://github.com/markofjohnson/HJCache

  15. Hi Mark

    Thanks for the great library. It is really easy to use and it works like a breeze. Having to go over my own app and trying to find leaks I saw that there are some leaks I think are coming from your code. There are some releases missing from the HJImageV inside the dealloc of some retaining properties. As far as I know each retaining property should be released but for example “oid” and “url” are not released but I could also be wrong.
    [thanks Andreas... fixed now in GitHib - Mark J]

    -Andy

  16. Rob says:

    awesome library! works great.

    i have a small problem where the loading wheel fails to stop and release when an image is obtained from cache. i set a breakpoint in setImage and the release code there is getting called, but i can still see the wheel behind the loaded image. everything works file when the image is downloaded.

    thanks so much for a great solution!

  17. markj says:

    Hi folks. With the help of schaefa on github I just plugged the last dumb memory leaks and fixed an obscure bug. Updates are pushed to github, so pull from there again if you are using HJCache.
    Deallocs are my obj-c downfall… I always forget to finish writing them.

  18. Adam Williams says:

    Thank you for making this tool available. I had no trouble getting started, but I have run into a snag with performance. Your demo application – the flickr one – does not exhibit this, but after reviewing the code ten times, I am hoping you can help me understand where I am going wrong.

    I have images loading in a table, and it scrolls beautifully. When I add a file cache, it then becomes jerky – when a cell is about to display, there is a slight pause. In your application, there is a fixed cell height of 80 points, whereas mine are based on a word-wrapping label, variable per cell. I have attempted to tweak the loading buffer and cache size, but it still seems that the file cache is slowing things down somehow.

    • markj says:

      Thanks for commenting Adam. There is a bug in this version where if images are coming from the file cache, then they are not being stored in the in memory cache. [As of March 3rd, this bug is fixed!] Once I fix that bug, performance might improve for you. This isn’t a problem for most apps as for small images, direct from file is plenty fast enough anyway. However, if the images you are using are large, or if you are resampling / scaling them when you are drawing them in your table cells or doing some other CPU heavy work, then you won’t get perfectly smooth scrolling.

      The other difference between from file and from URL is whether the images display synchronously or asynchronously. Currently, when loaded from file they display right away when the table cell comes into view, but when loaded from url they only display when the table stops scrolling. This could also be the source of the problem. The policy object is designed to allow you to specify whether you want images from file cache to display synchrounously or asynchrounously. If you are using large images or images that need to be scaled, you’ll need to use asynchronous display. Sorry but that part of the policy isn’t coded yet (because we never used it in our apps for big images in tables).

      I can fix these two limitation some time next week. It will be interesting to see if we can fix your problem.

  19. Sherwin Sowy says:

    HI markj,

    Great work on this HJache library. I am trying to implement it with a UIProgressView to show progress while downloading. Any tips on where I could put the hooks in and how to pass the progress view between your classes and the original caller (tableviewcontroller)?

    thanks.

    • markj says:

      HJMOHandler is the object that has the url objects and is getting the NSURLConnection callbacks when data arrives, so start there. Maybe add a ‘callbackOnProgress’?

  20. Rob says:

    thanks for the updated code – solved my memory issues!

    still having an issue with the loading wheel showing through images fetched from cache. if you can take a look at that at some point, that would be wonderful. and anything i can do to support the development and evolution of this kit, just let me know.

Trackbacks

  1. [...] coding instead.  The really useful def TTIMAGE has gone, in its place is the rather smart HJCache iPhone image caching library (tip for you – create HJObjManager as a singleton so you can access it from anywhere [...]

  2. [...] HJCache: iPhone cache library for asynchronous image loading and caching.. ← Completely remove MySQL server and MySQL client in [...]

  3. [...] Thư viện dùng để caching và load hình: http://www.markj.net/hjcache-iphone-image-cache/ [...]

  4. [...] the image from the array is assigned to the cell and if it hasn’t, a new request is made. The HJCache library could also have been used for caching and loading images. I’ve used this on several [...]

  5. [...] While building a new project at Hearst this week I looked into finding a decent solution for image caching that I felt would solve the problem once and for all. I think I’ve found it in HJCache. [...]

  6. [...] 你可以看作者的博客: hjcache:iphone cache library for asynchronous image loading and caching [...]

  7. [...] HJCache from MarkJ.net that allows for Asynchronous image loading and caching. [...]

Speak Your Mind

*