iOS NSObject NSCache

NSCache

mutable Collection 으로 key-value쌍을 임시로 저장하는데 사용된다.

메모리캐시의 장점

캐시를 사용하여 비용이 많이 드는 데이터를 메모리에 임시로 저장 및 로드 한다.
메모리에 저장된 데이터는 접근이 빠르고 다시 계산할 필요가 없어 성능의 이점이 있다.

메모리캐시의 단점

많은 대용량 데이터를 캐싱할 때 다른 애플리케이션을 위한 RAM이 남아 있지 않을 정도 많은 객체를 캐시할 수 있다.
RAM을 확보하기 위해 애플리케이션을 종료할 수 있다.

자동 제거 정책

NSCache는 자체적으로 시스템 메모리를 너무 많이 사용하지 않도록 자동으로 제거되는 정책을 소유한다.
다른 응용 프로그램에서 메모리가 필요한 경우 이러한 정책은 캐시에서 일부 항목을 제거하여 메모리 사용 공간을 최소화한다.
객체와 달리 캐시는 저장된 key 객체를 복사하지 않는 특징이 존재
디폴트로 캐시 객체는 컨텐츠가 삭제되면 자동으로 제거 (변경 가능)

Header의 정책

NSURLCache는 ‘Cache-Control’ HTTP-Header 가 결정한다.

HTTP-Header에 설정가능한 케쉬 관련 표준
NSURLCacheStoragePolicy

캐싱 막기

캐싱을 끄기 위해서, 다음의 디렉티브들을 보낼 수 있다.

Cache-Control: no-cache, no-store, must-revalidate

정적 에셋 캐싱

변경되지 않을 애플리케이션 내 파일들에 대해, 보통 적극적인 캐싱을 추가할 수 있다. 이것은 예를 들자면, 이미지, CSS 파일 그리고 자바스크립트 파일과 같이 애플리케이션에 의해 서브되는 정적 파일들을 포함한다.

Cache-Control:public, max-age=31536000

추가로, Expires 헤더를 참고하자.

Source

// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
private class NSCacheEntry<KeyType : AnyObject, ObjectType : AnyObject> {
  var key: KeyType
  var value: ObjectType
  var cost: Int
  
  // Linked List를 사용하여 데이터 추가 및 삭제에 효율적으로 동작한다.
  // 탐색 시간 복잡도: O(n)
  // 노드가 있기 때문에 데이터의 추가, 삭제로부터 자유롭다.
  var prevByCost: NSCacheEntry?
  var nextByCost: NSCacheEntry?
  init(key: KeyType, value: ObjectType, cost: Int) {
    self.key = key
    self.value = value
    self.cost = cost
  }
}

fileprivate class NSCacheKey: NSObject {
  
  var value: AnyObject // NSMutableDictionary 객체와 달리 캐시는 그 안에 들어 있는 key/object를 복사하지 않는다.
  
  init(_ value: AnyObject) {
    self.value = value
    super.init()
  }
  
  override var hash: Int {
    switch self.value {
    case let nsObject as NSObject:
      return nsObject.hashValue
    case let hashable as AnyHashable:
      return hashable.hashValue
    default: return 0
    }
  }
  
  override func isEqual(_ object: Any?) -> Bool {
    guard let other = (object as? NSCacheKey) else { return false }
    
    if self.value === other.value {
      return true
    } else {
      guard let left = self.value as? NSObject,
            let right = other.value as? NSObject else { return false }
      
      return left.isEqual(right)
    }
  }
}

open class NSCache<KeyType : AnyObject, ObjectType : AnyObject> : NSObject {
  // 별도의 Dictionary를 두어 데이터 접근에도 용이합니다. 데이터 접근 시간복잡도: O(1)
  private var _entries = Dictionary<NSCacheKey, NSCacheEntry<KeyType, ObjectType>>()
  private let _lock = NSLock()
  private var _totalCost = 0
  private var _head: NSCacheEntry<KeyType, ObjectType>?
  
  open var name: String = ""
  
  // 캐시가 보유할 수 있는 최대 비용을 설정할 수 있다.
  // 캐시가 최대 cost를 초과하면 제거될 수 있다. 기본 값은 0으로 비용 제한이 없다.
  open var totalCostLimit: Int = 0 // limits are imprecise/not strict
  
  // 캐시에 허용되는 최대 객체 개수를 제한할 수 있다.
  // 캐시 개수가 이 값을 초과하면 제거될 수 있습니다. 기본 값은 0으로 개수 제한이 없다.
  open var countLimit: Int = 0 // limits are imprecise/not strict
  
  open var evictsObjectsWithDiscardedContent: Bool = false
  
  public override init() {}
  
  open weak var delegate: NSCacheDelegate?
  
  open func object(forKey key: KeyType) -> ObjectType? {
    var object: ObjectType?
    
    let key = NSCacheKey(key)
    
    _lock.lock() // 캐시를 직접 lock 하지 않아도 다른 쓰레드에서 캐시의 항목을 추가, 제거 및 쿼리할 수 있다.
    if let entry = _entries[key] {
      object = entry.value
    }
    _lock.unlock()
    
    return object
  }
  
  open func setObject(_ obj: ObjectType, forKey key: KeyType) {
    setObject(obj, forKey: key, cost: 0)
  }
  
  private func remove(_ entry: NSCacheEntry<KeyType, ObjectType>) {
    let oldPrev = entry.prevByCost
    let oldNext = entry.nextByCost
    
    oldPrev?.nextByCost = oldNext
    oldNext?.prevByCost = oldPrev
    
    if entry === _head {
      _head = oldNext
    }
  }
  
  private func insert(_ entry: NSCacheEntry<KeyType, ObjectType>) {
    guard var currentElement = _head else {
      // The cache is empty
      entry.prevByCost = nil
      entry.nextByCost = nil
      
      _head = entry
      return
    }
    
    guard entry.cost > currentElement.cost else {
      // Insert entry at the head
      entry.prevByCost = nil
      entry.nextByCost = currentElement
      currentElement.prevByCost = entry
      
      _head = entry
      return
    }
    
    while let nextByCost = currentElement.nextByCost, nextByCost.cost < entry.cost {
      currentElement = nextByCost
    }
    
    // Insert entry between currentElement and nextElement
    let nextElement = currentElement.nextByCost
    
    currentElement.nextByCost = entry
    entry.prevByCost = currentElement
    
    entry.nextByCost = nextElement
    nextElement?.prevByCost = entry
  }
  
  open func setObject(_ obj: ObjectType, forKey key: KeyType, cost g: Int) {
    let g = max(g, 0)
    let keyRef = NSCacheKey(key)
    
    _lock.lock()
    
    let costDiff: Int
    
    if let entry = _entries[keyRef] {
      costDiff = g - entry.cost
      entry.cost = g
      
      entry.value = obj
      
      if costDiff != 0 {
        remove(entry)
        insert(entry)
      }
    } else {
      let entry = NSCacheEntry(key: key, value: obj, cost: g)
      _entries[keyRef] = entry
      insert(entry)
      
      costDiff = g
    }
    
    _totalCost += costDiff
    
    var purgeAmount = (totalCostLimit > 0) ? (_totalCost - totalCostLimit) : 0
    while purgeAmount > 0 {
      if let entry = _head {
        delegate?.cache(unsafeDowncast(self, to:NSCache<AnyObject, AnyObject>.self), willEvictObject: entry.value)
        
        _totalCost -= entry.cost
        purgeAmount -= entry.cost
        
        remove(entry) // _head will be changed to next entry in remove(_:)
        _entries[NSCacheKey(entry.key)] = nil
      } else {
        break
      }
    }
    
    var purgeCount = (countLimit > 0) ? (_entries.count - countLimit) : 0
    while purgeCount > 0 {
      if let entry = _head {
        delegate?.cache(unsafeDowncast(self, to:NSCache<AnyObject, AnyObject>.self), willEvictObject: entry.value)
        
        _totalCost -= entry.cost
        purgeCount -= 1
        
        remove(entry) // _head will be changed to next entry in remove(_:)
        _entries[NSCacheKey(entry.key)] = nil
      } else {
        break
      }
    }
    
    _lock.unlock()
  }
  
  open func removeObject(forKey key: KeyType) {
    let keyRef = NSCacheKey(key)
    
    _lock.lock()
    if let entry = _entries.removeValue(forKey: keyRef) {
      _totalCost -= entry.cost
      remove(entry)
    }
    _lock.unlock()
  }
  
  open func removeAllObjects() {
    _lock.lock()
    _entries.removeAll()
    
    while let currentElement = _head {
      let nextElement = currentElement.nextByCost
      
      currentElement.prevByCost = nil
      currentElement.nextByCost = nil
      
      _head = nextElement
    }
    
    _totalCost = 0
    _lock.unlock()
  }
}

public protocol NSCacheDelegate : NSObjectProtocol {
  func cache(_ cache: NSCache<AnyObject, AnyObject>, willEvictObject obj: Any)
}

extension NSCacheDelegate {
  func cache(_ cache: NSCache<AnyObject, AnyObject>, willEvictObject obj: Any) {
    // Default implementation does nothing
  }
}

참고

swift-corelibs-foundation
BLU.LOG
Eth dev post

댓글남기기