SDWebImage(2)

638 阅读7分钟

1:NSCache

1)NSCache是Foundation框架提供的缓存类。使用方法类似于字典。 它遵循NSCacheDelegate协议。具有

- (nullable ObjectType)objectForKey:(KeyType)key;
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
- (void)removeObjectForKey:(KeyType)key;

- (void)removeAllObjects;

方法。通过key-value进行查找添加设置删除元素。cost默认是0会影响缓存策略。

2)特点

  • 它是线程安全的。NSMutableDictionary是线程不安全的。
  • 内存不足,NSCache会自动释放存储对象。
  • NSCache的key不会被拷贝,不需要实现Copying协议,不需要拷贝。
  • 当超过totalCostLimit会被挤掉,旧的会被释放。
  • 可以通过手动调用remove进行手动的释放。
  • 当按home键在app进入后台之后,NSCache对象会进行自动释放所有对象。
  • 如果在模拟器上模拟内存警告。NsCache不会释放对象。
  • 如果NSCache遵循NSDiscardableContent标示当前对象是可被清除的。提高缓存的淘汰。系统会将内存上的存储置换到磁盘上,如果一个内存被标记为purgabel可被清除的。就不会置换直接被清除。

使用NSPurgeableData

 UIImage *image = [UIImage imageNamed:@"logo"];;
    CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
    /**
     计数器
     count >=1 ,使用
     count == 0, 丢弃的
     */
    _testPurgeableData = [[NSPurgeableData alloc] initWithData:(__bridge NSData *)rawData];
@interface NSPurgeableData : NSMutableData <NSDiscardableContent> {
@private
    NSUInteger _length;
    int32_t _accessCount;
    uint8_t _private[32];
    void *_reserved;
}

可被清除取决于计数器count >=1 当前对象可以被使用访问 == 0 当前对象会被丢弃的不可访问。与引用计数不同。通过NSDiscardableContent进行控制

@protocol NSDiscardableContent
@required
- (BOOL)beginContentAccess;//count+1
- (void)endContentAccess;//count-1
- (void)discardContentIfPossible;
- (BOOL)isContentDiscarded;
@end

3)两个重要的指标(缓存策略)

  • 通过设置countLimit缓存数据的数目,和设置totalCostLimit混存数据占用内存大小。来控制混存释放的时机。
  • totalCostLimit 总消耗大小,超过会做混存的修剪操作。但无法确定丢弃的顺序。因为依赖于底层的算法淘汰策略是不一样的。
  • countLimit数量限制

2:NSCache 使用实例

GNUStepFoundation 源码地址

1:_evictsObjectsWithDiscardedContent如果实现这个协议就标示被移除掉 与purgabel相同。
2:NSMapTable * _objects;``key内存管理语义,肯定不是copy

3:setObject: forKey实现

- (void) setObject: (id)obj forKey: (id)key cost: (NSUInteger)num
{
  _GSCachedObject *oldObject = [_objects objectForKey: key];
  _GSCachedObject *newObject;

  if (nil != oldObject)
    {
      [self removeObjectForKey: oldObject->key];//移除旧的
    }
  //缓存的淘汰
  [self _evictObjectsToMakeSpaceForObjectWithCost: num];//cost当前的消耗默认是0
  newObject = [_GSCachedObject new];
  // Retained here, released when obj is dealloc'd
  newObject->object = RETAIN(obj);
  newObject->key = RETAIN(key);
  newObject->cost = num;
  if ([obj conformsToProtocol: @protocol(NSDiscardableContent)])
    {
      newObject->isEvictable = YES;
      [_accesses addObject: newObject];
    }
  [_objects setObject: newObject forKey: key];
  RELEASE(newObject);
  _totalCost += num;
}

3.2cost如何计算消耗的

- (void)_evictObjectsToMakeSpaceForObjectWithCost: (NSUInteger)cost
{
  NSUInteger spaceNeeded = 0;
  NSUInteger count = [_objects count];

  if (_costLimit > 0 && _totalCost + cost > _costLimit)
    {
      spaceNeeded = _totalCost + cost - _costLimit; // 计算要清除的空间
    }

  // Only evict if we need the space.
  if (count > 0 && (spaceNeeded > 0 || count >= _countLimit))
    {
      NSMutableArray *evictedKeys = nil;
      // Round up slightly.
      NSUInteger averageAccesses = ((_totalAccesses / (double)count) * 0.2) + 1;//平均访问次数。_totalAccesses是所有访问次数的总和根据二八原则,+1保证结果不为0。
      NSEnumerator *e = [_accesses objectEnumerator];
      _GSCachedObject *obj;

      if (_evictsObjectsWithDiscardedContent)
	{
	  evictedKeys = [[NSMutableArray alloc] init];
	}
      while (nil != (obj = [e nextObject]))
	{
	  // Don't evict frequently accessed objects.
	  if (obj->accessCount < averageAccesses && obj->isEvictable)//当前对象是否可被移除
	    {
	      [obj->object discardContentIfPossible];//发送通知。释放内存
	      if ([obj->object isContentDiscarded])
		{
		  NSUInteger cost = obj->cost;

		  // Evicted objects have no cost.
		  obj->cost = 0;
		  // Don't try evicting this again in future; it's gone already.
		  obj->isEvictable = NO;
		  // Remove this object as well as its contents if required
		  if (_evictsObjectsWithDiscardedContent)
		    {
		      [evictedKeys addObject: obj->key];
		    }
		  _totalCost -= cost;
		  // If we've freed enough space, give up
		  if (cost > spaceNeeded)
		    {
		      break;
		    }
		  spaceNeeded -= cost;
		}
	    }
	}
      // Evict all of the objects whose content we have discarded if required
      if (_evictsObjectsWithDiscardedContent)
	{
	  NSString *key;

	  e = [evictedKeys objectEnumerator];
	  while (nil != (key = [e nextObject]))
	    {
	      [self removeObjectForKey: key];//移除
	    }
	}
    [evictedKeys release];
    }
}

注:关键是根据计算的averageAccesses频次进行移除。低于的都被释放掉。

4:_GSCachedObject

@interface _GSCachedObject : NSObject
{
  @public
  id object;
  NSString *key;
  int accessCount;//对象能够被访问的次数
  NSUInteger cost;//内存消耗
  BOOL isEvictable;//是否能够被驱逐,移除
}
@end

3:NSCache 使用实例

Swfit Foundation 源码地址 1: setObject函数

    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 //costDiff有值代表当前已经缓存过对象了
            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 //totalCostLimit默认不设置不走这段 无意义
        while purgeAmount > 0 {
            if let entry = _head { //_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()
    }

2: remove(entry)函数实现,更新双向链表结构,做修改的操作。

 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
        }
    }

3:insert(entry)函数实现主要注意点

/***先做清空操作***/
......
 guard entry.cost > currentElement.cost else { //依据cost进行排序新插入的如果大于当前节点消耗,把当前的插入放在最前面。如果cost为0不进行排序。
            // Insert entry at the head
            entry.prevByCost = nil
            entry.nextByCost = currentElement
            currentElement.prevByCost = entry
            
            _head = entry
            return
        }
......

        currentElement.nextByCost = entry //插入当前链表


4:NSCacheKeykey值 NSCacheEntry内容

private class NSCacheEntry<KeyType : AnyObject, ObjectType : AnyObject> {
    var key: KeyType
    var value: ObjectType
    var cost: Int
    var prevByCost: NSCacheEntry? //双向链表
    var nextByCost: NSCacheEntry? // 双向链表
    init(key: KeyType, value: ObjectType, cost: Int) {
        self.key = key
        self.value = value
        self.cost = cost
    }
}

5:为了保证key值的唯一性。重写了hash函数和isEqual函数。

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

6:使用NSLock保证线程读写安全。

7:purgeAmountpurgeCount缓存策略.背后也是根据 totalCostLimitcountLimit缓存策略。当两个都设置的时按顺序执行。totalCostLimit也不是严格的限制,会依据缓存策略的不同进行移除。

var purgeAmount = (totalCostLimit > 0) ? (_totalCost - totalCostLimit) : 0

        }
 var purgeCount = (countLimit > 0) ? (_entries.count - countLimit) : 0
  
        }

4:NSURLCache

1:使用NSURLCache建立缓存系统。

  • 可以指定缓存的磁盘和内存的大小。
    NSURLCache * cache = [[NSURLCache sharedURLCache] initWithMemoryCapacity:10*1024*1024 diskCapacity:30*1024*1024 diskPath:@"文件保存路径"];

  • 主要用于对网络请求的缓存。Get请求
  • cache-Control 默认选项

1)max-age 缓存时间

2)public:谁都可以缓存

3)private 只有客户端进行缓存,比如中间的代理是无法缓存的。

4)no-cache并不代表不缓存,只不过要跟服务端进行确认,如果没变化使用本地缓存,如果有变化使用服务器端。

5) no-store禁止使用缓存。

地址:Library-->cache

2:内存缓存和磁盘缓存。

3: CachePolicy 沙盒中CFNetwork开头的缓存

  • useProtocalCachePolicy默认缓存策略,对于特定URL使用网络协议中实现的缓存策略。(HTTP)
  • reloadIgnoringLocalCacheData (或者reloadIngnoringCacheData):不使用缓存直接请求原始数据。
  • returnCacheDataElseLoad :无论缓存是否过期,有缓存则使用缓存,否则重新请求原始数据。
  • returnCacheDataDontLoad:无论缓存是否过期,有缓存则使用缓存,否则视为失效。不会重新请求原始数据。

4:lastModified比对服务,资源是否更新。Code = 200 代表请求数据。Code = 304 代表在缓存中读取数据

code = 200

code = 304

5:etag看服务器是否支持与lastModified作用相同。区别在于lastModified通过日期进行比较。有可能会出现判断的错误。而etag是通过当前的服务器生成hash值进行比对,更准确。但平时使用lastModified就够了。

6:缓存到沙盒当中的。

5:SDEWebImage如何配置缓存策略的

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0,
    SDWebImageDownloaderProgressiveDownload = 1 << 1,

    /**
     * By default, request prevent the of NSURLCache. With this flag, NSURLCache
     * is used with default policies.
     */
    SDWebImageDownloaderUseNSURLCache = 1 << 2, //默认忽略当前缓存。需要去服务器进行请求。为了避免重复缓存。


    /**
     * Call completion block with nil image/imageData if the image was read from NSURLCache
     * (to be combined with `SDWebImageDownloaderUseNSURLCache`).
     */

    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    
    /**
     * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
     * extra time in background to let the request finish. If the background task expires the operation will be cancelled.
     */

    SDWebImageDownloaderContinueInBackground = 1 << 4,

    /**
     * Handles cookies stored in NSHTTPCookieStore by setting 
     * NSMutableURLRequest.HTTPShouldHandleCookies = YES;
     */
    SDWebImageDownloaderHandleCookies = 1 << 5,

    /**
     * Enable to allow untrusted SSL certificates.
     * Useful for testing purposes. Use with caution in production.
     */
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

    /**
     * Put the image in the high priority queue.
     */
    SDWebImageDownloaderHighPriority = 1 << 7,
};

参考:SDWebImage学习笔记(二): SDWebImageDownloader