ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Swift에서 디스크 캐시, 메모리 캐시란
    iOS 2018. 12. 3. 17:56

    https://stackoverflow.com/questions/11255653/in-memory-cache-and-diskcache-for-images-strategies


    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?

    Sorry for poor English. Thank in advance.

    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).



    https://medium.com/@NilStack/swift-world-write-our-own-cache-part-3-memory-cache-and-disk-cache-7056948eb52c



    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)

    Retrieve image from memory cache with key:

    open func retrieveImageInMemoryCache(forKey key: String, options: KingfisherOptionsInfo? = nil) -> Image? {
            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)"
    ...
    try weakSelf.fileManager.createDirectory(atPath: weakSelf.path,
    withIntermediateDirectories: true, attributes: nil)
    ...
    weakSelf.fileManager.createFile(atPath: filePath,
    contents: object.encode() as Data?, attributes: nil)

    In Kingfisher

    public final class func defaultDiskCachePathClosure(path: String?, cacheName: String) -> String {
    let dstPath = path ?? NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!
    return (dstPath as NSString).appendingPathComponent(cacheName)
    }
    ...
    try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
    ...
    self.fileManager.createFile(atPath:self.cachePath(forComputedKey: computedKey), contents: data, attributes: nil)
    ...

    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.

    For example in Cache

      /// Queue for write operations
    public fileprivate(set) var writeQueue: DispatchQueue
    /// Queue for read operations
    public fileprivate(set) var readQueue: DispatchQueue
    ...
    writeQueue = DispatchQueue(label: "\(DiskStorage.prefix).\(cacheName).WriteQueue",
    attributes: [])
    readQueue = DispatchQueue(label: "\(DiskStorage.prefix).\(cacheName).ReadQueue",
    attributes: [])

    In next part, we will introduce cache manager which control the complete workflow.

    Thanks for your time.


    I am caching an image locally..That same image will be used in few screens. Steps for doing this is as follows:

    1. Get image from cache

    2. If not present in NSCache, get image from Cache directory

    3. If not present in Cache Directory, download from Internet

    4. Save downloaded image to dir

    5. Insert the image in cache

    //Helper class is as

         class ImageCacheHelper:NSObject{
    
            static var cache = NSCache()
            static var isNotRunningDispatch:Bool = true
    
          class func setObjectForKey(imageData:NSData,imageKey:String){
    
                ImageCacheHelper.cache.setObject(imageData, forKey: imageKey)
    
            }
    
          class func getObjectForKey(imageKey:String)->NSData?{
    
                return ImageCacheHelper.cache.objectForKey(imageKey) as? NSData
    
            }
    
          class func getImage(imageUrl:String,completionHandler:(NSData)->()){        
                if ImageCacheHelper.isNotRunningDispatch{
    
                    ImageCacheHelper.isNotRunningDispatch = false
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), {
                        let imgUrl = NSURL(string:imageUrl)
                        let imageData = NSData(contentsOfURL: imgUrl!)!
                        ImageCacheHelper.setObjectForKey(imageData, imageKey: "\(imageUrl.hashValue)")
                        ImageCacheHelper.isNotRunningDispatch = true
                        completionHandler(imageData)
    
                    })
                }else{
                   print("alerady started loading image")
               }
            }
          }

    //How i call this from ViewController class is as

    let userImageUrl = "\(appBaseUrl)\(imageUrlString)"
    let fileManager = NSFileManager.defaultManager()
    let diskPaths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.CachesDirectory, NSSearchPathDomainMask.UserDomainMask, true)
    let cacheDirectory = diskPaths[0] as NSString
    let diskPath = cacheDirectory.stringByAppendingPathComponent("\(userImageUrl.hashValue)")
    if let data = ImageCacheHelper.getObjectForKey("\(userImageUrl.hashValue)"){
              let userImage =  UIImage(data: data)
              self.userImgView.image = userImage
    
    }else if fileManager.fileExistsAtPath(diskPath){
    
              let image =  UIImage(contentsOfFile: diskPath)
              self.userImgView.image = image
              ImageCacheHelper.setObjectForKey(UIImageJPEGRepresentation(image, 1.0)!, imageKey: "\(userImageUrl.hashValue)")
    
    }else   {
              ImageCacheHelper.getImage(userImageUrl){ imageData in
    
                imageData.writeToFile(diskPath, atomically: true)
                        dispatch_async(dispatch_get_main_queue()){
    
                            let userImage =  UIImage(data: imageData)
                            self.userImgView.image = userImage
    
                        }
            }
    }

    Flaws That I see in my code are as follows:

    1. Creating a static variable for NSCache.

    2. Would be great if settingObject in NSCache, getting would be as of NSUserDefaults.

    How to optimize this?

    Here is a snippet of project that i have tried to dohttps://drive.google.com/file/d/0B6dTvD1JbkgBS1k3Ry1xNmZ1VHc/view?usp=sharing

    Modify Caching Strategy

    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:

    1. Test with no cache (this will be your baseline)
    2. Test with a simple caching strategy (maybe just using NSCache or the Cache directory)
    3. Compare the results of step 2 to your baseline obtained from step 1
    4. 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.

    Declaration

    class NSCache<KeyType, ObjectType> : NSObject where KeyType : AnyObject, ObjectType : AnyObject
    

    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.

    Topics

    Managing the Name

    var name: String

    The name of the cache.

    Managing Cache Size

    var countLimit: Int

    The maximum number of objects the cache should hold.

    var totalCostLimit: Int

    The maximum total cost that the cache can hold before it starts evicting objects.

    Managing Discardable Content

    var evictsObjectsWithDiscardedContent: Bool

    Whether the cache will automatically evict discardable-content objects whose content has been discarded.

    protocol NSDiscardableContent

    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.

    Managing the Delegate

    var delegate: NSCacheDelegate?

    The cache’s delegate.

    protocol NSCacheDelegate

    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.

    Getting a Cached Value

    func object(forKey: KeyType) -> ObjectType?

    Returns the value associated with a given key.

    Adding and Removing Cached Values

    func setObject(ObjectType, forKey: KeyType)

    Sets the value of the specified key in the cache.

    func setObject(ObjectType, forKey: KeyType, cost: Int)

    Sets the value of the specified key in the cache, and associates the key-value pair with the specified cost.

    func removeObject(forKey: KeyType)

    Removes the value of the specified key in the cache.

    func removeAllObjects()

    Empties the cache.

    Relationships

    Inherits From

    Conforms To

    See Also

    Purgeable Collections

    class NSPurgeableData

    A mutable data object containing bytes that can be discarded when they're no longer needed.


    'iOS' 카테고리의 다른 글

    [펌] iOS TDD 자료  (0) 2018.12.12
    iOS 네이밍 컨벤션  (0) 2018.12.11
    rxSwift + Alamofire 코드  (0) 2018.12.02
    GCD의 개념  (0) 2018.11.30
    [iOS] iOS 면접 질문 인터뷰 모음.  (0) 2018.11.30

    댓글 0

Designed by Tistory.