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.