Two-tier Caching With NSCache

      6 Comments on Two-tier Caching With NSCache

NSCache is a great way to cache items like images. What I wanted was to persist the cached items and at the same time have a cache that’s really easy to use. I came up with a little class that’s using NSCache as a primary cache and the file system as the secondary (that would explain the title 🙂 ).

NSCache

NSCache is a great way to cache items, especially images. It works pretty much like a dictionary. You set objects for keys and then fetch them. Objects from the cache are removed automatically by the system, based on the eviction policies. It’s a great way to keep a small memory footprint.

What I wanted to do was persist those objects on the disk and lazy load them in the NSCache when they’re requested. So I created a little class for this, DACache.

DACache

DACache is using a primary and a secondary cache. Primary cache is your standard NSCache, and the secondary is the file system. You can use the class with only the primary cache if you want. This class is meant to be used as a dictionary, therefore it’s really simple to use:

var cache = DACache()
cache["key"] = dataObjectToCache // Store
let value: NSData? = cache["key"] // Fetch

When you set your object for a key, the object is saved in the NSCache and persisted to the file system. When you request an object it first checks if it’s already loaded in NSCache and returns it. If it’s not loaded it will check the file system, load it from the file system into NSCache and then return it. The diagram below shows it much better:

Let’s dive into some code.

The Code

First, we’ll start by defining an interface of our class:

public protocol DACacheManager {
    var primaryCache: DACacheProvider { get set }
    var secondaryCache: DACacheProvider? { get set }
    subscript(key: String) -> Data? { get set }
    func clearCache()
}

And now let’s implement the class:

public class DACache: DACacheManager {
    
    public static let shared: DACacheManager = DACache()
    
    public var primaryCache: DACacheProvider = MemoryCache()
    public var secondaryCache: DACacheProvider? = FileCache(cacheDir: "DACache")
    
    public subscript(key: String) -> Data? {
        get {
            guard let result = primaryCache.load(key: key) else {
                if let file = secondaryCache?.load(key: key) {
                    primaryCache.save(key: key, value: file as NSData?)
                    return file
                }
                return nil
            }
            return result
        }
        set {
            let data: NSData? = newValue as NSData?
            primaryCache.save(key: key, value: data)
            secondaryCache?.save(key: key, value: data)
        }
    }
    
    public func clearCache() {
        primaryCache.clearCache()
        secondaryCache?.clearCache()
    }
}

The primary and secondary caches are defined at the top, they conform to the DACacheProvider interface. We’re using ‘MemoryCache’ which is using NSCache and ‘FileCache’ will be using the file system. Both caches conform to the ‘DACacheProvider’ interface and the properties are settable, so you can provide your custom implementation for them. For example, instead of using the file system cache, you could easily implement a cache that’s using Core Data.

In the subscript, we’re getting and setting values. The setter is pretty straight forward, you just set values for keys on both cache providers. In the getter, we have a bit of logic. We’re checking if the value is in the primary cache, if it’s not, we’ll check the secondary cache. If the value is in the secondary cache, we’ll save it in the primary cache, so the next fetch would be faster. If the value is not in the secondary cache, it’s not cached, so we return nil.

At the end of the class, we have a ‘clearCache’ method that will simply clear all objects from NSCache and delete all the cached files on the file system.

Memory Cache

Our primary cache provider will be a wrapper around NSCache and is actually a pretty simple class. It will conform to ‘DACacheProvider’ interface:

public protocol DACacheProvider {
    func load(key: String) -> Data?
    func save(key: String, value: NSData?)
    func clearCache()
}

It’s a pretty simple interface, let’s take a look at the implementation:

public class MemoryCache: DACacheProvider {
    
    private let cache: NSCache<NSString, NSData> = NSCache<NSString, NSData>()
    
    public func load(key: String) -> Data? {
        return cache.object(forKey: NSString(string: key)) as Data?
    }
    
    public func save(key: String, value: NSData?) {
        if let new = value {
            self.cache.setObject(new, forKey: NSString(string: key))
        } else {
            self.cache.removeObject(forKey: NSString(string: key))
        }
    }
    
    public func clearCache() {
        cache.removeAllObjects()
    }
}

We’re using NSCache here with NSString as the key and NSData as the value. We can’t use swifts String here because it’s a struct and NSCache keys/values have to conform to AnyObject so you’ll see an error ‘Type ‘String’ does not conform to protocol ‘AnyObject”, like so:

In the save method we’ll check if the value we’re trying to save is nil, if it is, we’ll remove the object from the cache.

File Cache

‘FileCache’ also conforms to the same interface and is a class we’ll use to persist objects on the disk. We’ll persist objects in the ‘Library\Caches’, we don’t really want these persisted permanently and since iOS can clear the ‘Library\Caches’ folder in case the storage space is low, this is a perfect place.

This class is just a bit more complicated than ‘MemoryCache’ but it’s still pretty simple:

    private let cacheDirectory: String
    
    init(cacheDir: String, enableLogging: Bool = true) {
        cacheDirectory = cacheDir
        loggingEnabled = enableLogging
    }
    
    public func load(key: String) -> Data? {
        guard let path = fileURL(fileName: key) else {
            return nil
        }
        
        var data: Data?
        do {
            data = try Data(contentsOf: path)
        } catch {
            log("[DACache] Couldn't create data object: ", error)
        }
        return data
    }
    
    public func save(key: String, value: NSData?) {
        guard let path = fileURL(fileName: key) else {
            return
        }
        if let new = value as Data? {
            do {
                try new.write(to: path, options: .atomic)
            } catch {
                log("[DACache] Error writing data to the file: ", error)
            }
        } else {
            try? FileManager.default.removeItem(at: path)
        }
    }
    
    public func clearCache() {
        deleteCacheDirectory()
    }

There are some utility methods that I omitted here, you can find the full code on my GitHub account.

We can instantiate this class with a caches folder name and it will create a folder within the ‘Library\Caches’ directory with that name. This way you can have a very fine-grained control over your file cache, if you choose to. If you don’t care, all the files will be saved under ‘DACache’ folder.

Load and save methods might look complicated, but they’re not much different from the load and save for the ‘MemoryCache’ implementation. You load a file from a disk into a Data object and return it if it’s there. In the save method you check if the value is nil, if it is, you remove the file, otherwise you save it.

‘fileURL’ method will get you a URL to the file on the local file system, we’re using the key as the file name since keys are unique anyway. This method might be worth mentioning, so let’s see it:

    private func fileURL(fileName name: String) -> URL? {
        guard let escapedName = name.addingPercentEncoding(withAllowedCharacters: .alphanumerics) else {
            return nil
        }
        
        var cachesDir: URL?
        do {
            cachesDir = try cachesDirectory()
        } catch {
            log("[DACache] Error getting caches directory: ", error)
            return nil
        }
        
        return cachesDir?.appendingPathComponent(escapedName)

    }

We’ll be escaping the key that we pass in, just so we’re absolutely sure we get a qualified URL when we append the file name to the caches directory path. The rest of the method is uneventful 🙂

At the end of this class, we have ‘clearCache’ method that will delete the whole ‘cacheDirectory’ folder.

Example Usage

This is just an example on how you might use this class. It’s a method from one of the projects I’m working on, but it will give you an idea on how you might use it:

    func getImage(forPost post: WPPostItem, onCompleted: ((WPPostItem, UIImage?) -> ())?) {
        
        guard let url = URL(string: post.imageURL) else {
            onCompleted?(post, nil)
            return
        }
        
        if let cachedImageData = imagesCache[post.imageURL],
            let cachedImage = UIImage(data: cachedImageData, scale: UIScreen.main.scale)
        {
            onCompleted?(post, cachedImage)
            return
        }
        
        URLSession.shared.dataTask(with: url) {
            (data, response, error) in
            
            guard
                let responseData = data,
                let jsonDict = try? JSONSerialization.jsonObject(with: responseData, options: [JSONSerialization.ReadingOptions.allowFragments]) as? [String: Any?],
                let jsonImageDict = jsonDict?[Constants.guid.rawValue] as? [String: Any?],
                let imageURLString = jsonImageDict[Constants.rendered.rawValue] as? String,
                let imageURL = URL(string: imageURLString)
                else {
                    onCompleted?(post, nil)
                    return
            }
            
            URLSession.shared.dataTask(with: imageURL) {
                (data, response, error) in
                
                guard
                    let responseData = data,
                    let image = UIImage(data: responseData, scale: UIScreen.main.scale)
                    else {
                        onCompleted?(post, nil)
                        return
                }
                self.imagesCache[post.imageURL] = responseData
                
                onCompleted?(post, image)
            }.resume()
        }.resume()
    }

‘imagesCache’ is an instance of ‘DACache’. At the top of the method, I’m checking if I have cached data available. If there’s cached data I create an image out of it, otherwise I kick off a networking request and save the response in the cache when it’s finished.

Conclusion

I was working on an app recently and I wanted to cache images, so, naturally, I ended up using NSCache. I didn’t want to download all those images every time a user starts the app and NSCache will evict objects from the memory over time. So I came up with this class. I also like to keep things simple and added a subscript, just to keep my file easier. This class solved my problem pretty well and I really hope it will help you as well. You can find the ‘DACache’ on cocoapods and you can find the whole project on my GitHub account.

As always, have a nice day 🙂

Dejan.

More resources

6 thoughts on “Two-tier Caching With NSCache

  1. khi

    Hello,

    It’s a really interesting post, thank you for sharing it.
    This solution looks really simple and pretty, good job 🙂

    I created my data manager using CoreData, but I will keep it in mind for an other project.

    Kevin

    Reply
  2. Stefan Christopher

    Hi Dejan, can you use NS Cache for profile photos for a large social network (i.e. badoo)? Or what would you recommend?

    Reply
  3. Lakshaya

    Hi Dejan, Thank you for the great post but I have one question and that is when we fetch data from the files it takes some significant amount of time and since it happens on the main thread so definitely it will hamper the performance specially in tableviews. Any views on this?

    Regards,
    Lakshaya

    Reply
    1. Dejan Agostini Post author

      Hi Lakshaya,

      You’re right, if you’re saving/loading large amounts of data this code would impact performance. I would suggest modifying the load/save functions to be asynchronous. Have the function accept a closure and execute the body of the function on a background thread. Subscripting wouldn’t work in this case, but you can always use the functions directly. Looking at this code after a year makes me wonder, why didn’t I do it asynchronously back then, sorry about that.

      Thanks for reading the blog, I appreciate it 🙂
      Dejan.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.