Now, I am developing a news reader app like BBC news iOS. see in BBC News
In my app, I must download image from server to and show it in view to make users easier to choose the news they want to read.
For more performance, I must cache image to avoid reloading image for server. I know that there are 2 kinds of cache: In-memory cache that saving images in memory (RAM) and DiskCach that save images in Disk to load it when we need.
My question is: What is best images cache mixed strategies for my App? (use both in-memory cache and image-cache) My solution is:
download image --> save them in diskcache + save them in memory cache --> load image from in-memory cache on demand and show in view ---> in-memory cache over its MAX_SIZE --> free in-memory cache ---> load image from disk cache on demand and save it to memory cache --> repeat........
Is my solution is right approach?
Another question: when in-memory cache over its MAX_SIZE --> we will free its --> all images in cache will lose so image in our view will disappear. --> How to solve this problem?
In one of my projects I implemented pretty much the same caching methods (Disk Cache and Memory Cache).
Maximum cache size
Each cache system had its own max size limit. The "size" of each image was computed differently in the cache systems.
For the memory cache, each image would have a size computed as image size = image width * image height (in pixels) So, the maximum size for the memory cache would represent a the maximum area of a pixel surface
For the disk cache, I used the actual file size for each file.
Making room
When using the cache systems, you might get to a situation where one of the caches is full and you want to insert a new item in it - you have to remove some items to make room.
What I did was assign a timestamp to each entry in the cache. Every time I access that item I updated the timestamp. When you want to make room, you just need to start removing items from the oldest to the newest based on the last access timestamp.
This is a simple algorithm for freeing up space and in some cases might actually behave poorly. It is up to you to experiment and see if you need something more advanced than this. For example, you could improve this method by adding a priority value for each item and keep old items in the cache if their priority is high. Again, it depends on your app's needs.
Expiration
For the disk cache, I would definitely add an expiration date for each entry. If the memory cache is destroyed when the user completely terminates the app, images in the disk cache might be stuck in there forever.
Encapsulation
Another aspect I would consider, is making the caching system as transparent as possible to the programmer. If you want to enable/disable one of the cache it would be best to have most of the code remain the same.
In my app, I built a central content delivery system and I would always request images from the internet through this object. The caching system would then check the local caches (memory / disk) and either return me the image immediately or make a request to download it.
Either way... I, as the "user" of the caching system did not care what was happening behind the curtains. All I knew is I made a request to get an image from an URL and I got it (faster or slower depending if the image was cached).
Swift World: Write Our Own Cache Part 3 — Memory Cache and Disk Cache
In this part, we will talk about memory cache and disk cache. The strategy in popular cache frameworks are combination of memory cache and disk cache to leverage each one’s advantages.
NSCache is often used as memory Cache. For example in Kingfisher,
open class ImageCache { //Memory fileprivate let memoryCache = NSCache<NSString, AnyObject>() ... }
Write image to memory cache:
memoryCache.setObject(image, forKey: computedKey as NSString, cost: image.kf.imageCost)
let options = options ?? KingfisherEmptyOptionsInfo let computedKey = key.computedKey(with: options.processor.identifier)
return memoryCache.object(forKey: computedKey as NSString) as? Image }
It isn’t difficult to understand the logic. We will go on to disk cache.
Disk cache uses iOS’s file system to store data converted from object. Usually it will create its own directory in app’s caches directory. A file will be created for every object. For example in Cache
let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true) path = "\(paths.first!)/\(DiskStorage.prefix).\(cacheName)"
public final class func defaultDiskCachePathClosure(path: String?, cacheName: String) -> String { let dstPath = path ?? NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! return (dstPath as NSString).appendingPathComponent(cacheName) }
If these IO operations on file system run in main thread, the UI will be stuck because reading from and writing to disk are time consuming. The popular method is to create new DispatchQueues to do the job asynchronously.
It seems every image that is subject to caching is ending up both in memory and on disk. Depending on the number of images and caching aggressiveness, this could be more expensive (in terms of resource usage) than the network request used to retrieve the image.
From my perspective, using both NSCache and the Cache directory together doesn’t add any value. They are both volatile and unpredictable (to different extents). The system could reclaim space from either when it deems it necessary.
From Apples File System Programming Guide:
"Note that the system may delete the Caches/ directory to free up disk space, so your app must be able to re-create or download these files as needed.”
Therefore its either a choice between caching in memory and caching on disk or an emphasis of one over the other. This inevitably leads to additional questions about how your app will be used and the what kind of images you are working with.
Some basic guidelines:
You don’t want to store a large number of full resolution images in memory
You don’t want to have too many file system I/O operations (spread over time)
You don’t want to have images in memory that are not going to be seen by the user
You don’t want to go to disk every time for frequently seen images
What is a large number? How much is too many? You will have to test and measure.
Test & Measure (and then measure again)
I would definitely recommend testing and measuring your caching strategy to see how much of an impact it has. Xcode instruments provides tools to accomplish this.
One way to go about this is:
Test with no cache (this will be your baseline)
Test with a simple caching strategy (maybe just using NSCache or the Cache directory)
Compare the results of step 2 to your baseline obtained from step 1
Not satisfied, then apply a different caching strategy. Repeat step 3. Otherwise, if you are satisfied with the resource usage and responsiveness of the app then you can stop here until there are significant changes to either your codebase, data or the iOS frameworks.
NSCache
A mutable collection you use to temporarily store transient key-value pairs that are subject to eviction when resources are low.
SDKs
iOS 4.0+
macOS 10.6+
tvOS 9.0+
watchOS 2.0+
Framework
Foundation
Declaration
Overview
Cache objects differ from other mutable collections in a few ways:
The NSCache class incorporates various auto-eviction policies, which ensure that a cache doesn’t use too much of the system’s memory. If memory is needed by other applications, these policies remove some items from the cache, minimizing its memory footprint.
You can add, remove, and query items in the cache from different threads without having to lock the cache yourself.
Unlike an NSMutableDictionary object, a cache does not copy the key objects that are put into it.
You typically use NSCache objects to temporarily store objects with transient data that are expensive to create. Reusing these objects can provide performance benefits, because their values do not have to be recalculated. However, the objects are not critical to the application and can be discarded if memory is tight. If discarded, their values will have to be recomputed again when needed.
Objects that have subcomponents that can be discarded when not being used can adopt the NSDiscardableContent protocol to improve cache eviction behavior. By default, NSDiscardableContent objects in a cache are automatically removed if their content is discarded, although this automatic removal policy can be changed. If an NSDiscardableContent object is put into the cache, the cache calls discardContentIfPossible() on it upon its removal.
You implement this protocol when a class’s objects have subcomponents that can be discarded when not being used, thereby giving an application a smaller memory footprint.
The delegate of an NSCache object implements this protocol to perform specialized actions when an object is about to be evicted or removed from the cache.