在iOS开发的过程中,我们经常会遇到一个问题,那就是从网络下载的图片应该如何来存储,首先能够想到的可能就是使用字典把图片保存起来,那么下次再去请求的时候就可以直接使用而不需要下载了,但是使用字典未必是一个好的方案。其实NSCache类更好,因为它是Foundation框架专门为处理缓存而设计的。

 

NSCache

NSCache是一个类似于集合的容器,它也存储key-value对,这一点类似于NSDictionary类。我们通常需要缓存一些临时存储、短时间使用、但创建昂贵的对象,通过重用这些对象可以优化应用的性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,可以在内存紧张时会被丢弃。如果对象被丢弃了,则下次使用时需要重新计算。


当一个key-value对在缓存中时,缓存维护它的一个强引用。存储在NSCache中的通用数据类型通常是实现了NSDiscardableContent协议的对象。在缓存中存储这类对象是有好处的,因为当不再需要它时,可以丢弃这些内容,以节省内存。默认情况下,缓存中的NSDiscardableContent对象在其内容被丢弃时,会被移除出缓存,尽管我们可以改变这种缓存策略。如果一个NSDiscardableContent被放进缓存,则在对象被移除时,缓存会调用discardContentIfPossible方法。

NSCache与可变集合有几点不同:

1:NSCache类结合了各种自动删除策略,以确保不会占用过多的系统内存。如果其它应用需要内存时,系统自动执行这些策略。当调用这些策略时,会从缓存中删除一些对象,以最大限度减少内存的占用。
2:NSCache是线程安全的,我们可以在不同的线程中添加、删除和查询缓存中的对象,而不需要锁定缓存区域。
3:不像NSMutableDictionary对象,一个缓存对象不会拷贝key对象。
   

这些特性对于NSCache类来说是必须的,因为在需要释放内存时,缓存必须异步地在幕后决定去自动修改自身。下面看一下头文件:

open class NSCache<KeyType, ObjectType> : NSObject where KeyType : AnyObject, ObjectType : AnyObject {
    
    open var name: String

    unowned(unsafe) open var delegate: NSCacheDelegate?
    
    open func object(forKey key: KeyType) -> ObjectType?

    open func setObject(_ obj: ObjectType, forKey key: KeyType) // 0 cost

    open func setObject(_ obj: ObjectType, forKey key: KeyType, cost g: Int)

    open func removeObject(forKey key: KeyType)

    open func removeAllObjects()
    
    open var totalCostLimit: Int // limits are imprecise/not strict

    open var countLimit: Int // limits are imprecise/not strict

    open var evictsObjectsWithDiscardedContent: Bool
}

public protocol NSCacheDelegate : NSObjectProtocol {

    @available(watchOS 2.0, *)
    optional public func cache(_ cache: NSCache<AnyObject, AnyObject>, willEvictObject obj: Any)
}

相关内容如下:

由上可知,NSCache类中东西并不多,NSCache提供了一组方法来存取key-value对,类似于NSMutableDictionary类。我们在存储对象时,可以为对象指定一个消耗值,即存储方法:setObject(obj: AnyObject, forKey key: AnyObject, cost g: Int)中的cost参数,

cost这个消耗值用于计算缓存中所有对象的一个消耗总和

1)当内存受限或者总消耗超过了限定的最大总消耗,则缓存能够开启一个丢弃过程以移除一些对象。不过,这个过程不能保证被丢弃对象的顺序。其结果是,如果我们试图操作这个消耗值来实现一些特殊的行为,则后果可能会损害我们的程序。

2)通常情况下,这个消耗值是对象的字节大小。如果这些信息不是现成的,则我们不应该去计算它,因为这样会使增加使用缓存的成本。如果我们没有可用的值传递,则直接传递0,或者是使用-setObject:forKey:方法,这个方法不需要传入一个消耗值。

 

countLimit:限定了缓存最多维护的对象的个数

默认值为0,表示不限制数量。但需要注意的是,这不是一个严格的限制。如果缓存的数量超过这个数量,缓存中的一个对象可能会被立即丢弃、或者稍后、也可能永远不会,具体依赖于缓存的实现细节。

 

totalCostLimit:限定缓存能维持的最大内存

默认值也是0,表示没有限制。当我们添加一个对象到缓存中时,我们可以为其指定一个消耗(cost),如对象的字节大小。如果添加这个对象到缓存导致缓存总的消耗超过totalCostLimit的值,则缓存会自动丢弃一些对象,直到总消耗低于totalCostLimit值。不过被丢弃的对象的顺序无法保证。需要注意的是totalCostLimit也不是一个严格限制,其去除策略是与countLimit一样的。相关例子可以看这里:Objective-C中的缓存

 

evictsObjectsWithDiscardedContent:该布尔值标识缓存是否自动舍弃那些内存已经被丢弃的对象如果设置为YES,则在对象的内存被丢弃时舍弃对象。默认值为YES。

 

NSDiscardableContent协议

@protocol NSDiscardableContent
@required
- (BOOL)beginContentAccess;
- (void)endContentAccess;
- (void)discardContentIfPossible;
- (BOOL)isContentDiscarded;
@end

NSDiscardableContent是一个协议,实现这个协议的目的是为了让我们的对象在不被使用时,可以将其丢弃,以让程序占用更少的内存。

一个NSDiscardableContent对象的生命周期依赖于一个“counter”变量。一个NSDiscardableContent对象实际是一个可清理内存块,这个内存记录了对象当前是否被其它对象使用。如果这块内存正在被读取,或者仍然被需要,则它的counter变量是大于或等于1的;当它不再被使用时,就可以丢弃,此时counter变量将等于0。

当counter变量等于0时,如果当前时间点内存比较紧张的话,内存块就可能被丢弃。为了丢弃这些内容,可以调用对象的discardContentIfPossible方法,这样当counter变量等于0时将会释放相关的内存。而如果counter变量不为0,则该方法什么也不做。
 

默认情况下,NSDiscardableContent对象的counter变量初始值为1,以确保对象不会被内存管理系统立即释放。从这个点开始,我们就需要去跟踪counter变量的状态。为此。协议声明了两个方法:beginContentAccess和endContentAccess。其中调用beginContentAccess方法会增加对象的counter变量+1,这样就可以确保对象不会被丢弃。当不在需要对象的时候,通过调用endContentAccess方法使变量-1.通常是让对象的counter值变回为0,这样在对象的内容不再被需要时,就要以将其丢弃。

 

通常,使用NSCache会结合NSDiscardableContent协议,实现了这个协议的类需要在被引用之前,必须调用beginContentAccess来标记为可使用的,如果在使用之前没有调用beiginContentAccess,那么就会抛出异常。在使用结束之后,调用endContentAccess,来标记它为可以被释放的。如果实现了NSDiscardableContent协议的对象放入了NSCache中,那么,在清除它的时候,会调用discardContentIfPossible方法来判断引用状况,没有引用,则销毁。

 

NSPurgeableData

还有一个类是NSPurgeableData,和NSCache搭配起来使用,效果很好。NSPurgeableData是NSMutableData的子类并遵守了NSDiscardableContent协议。如果某个对象所占用的内存能够根据需要随时丢弃,那么就可以实现该协议所定义的接口,这就是说,当系统资源紧张时,可以把保存NSPurgeableData对象的那块内存释放掉。NSDiscardableContent中定义了isContentDiscarded可以用来查寻相关内存是否已经是否。

如果需要访问某个NSPurgeableData对象,可以调用其beginContentAccess方法,告诉它现在还不应该丢弃自己所占用的内存。用完之后,调用endContentAccess方法,告诉它在必要的时候丢弃自己所占用的内存,这些调用可以嵌套,所以说,™就像递增与递减引用计数所用的方法那样。只有对象的“引用计数”为0时才可以丢弃。
 

如果将NSPurgeableData对象加入NSCache,那么当该对象为系统所丢弃时,也会自动从缓存中移除。通过NSCache的evictsObjectsWithDiscardedContent属性,可以开启和关闭此功能。

 

我们也来做一个简单的图片缓存机制:

//创建应该缓存单例
extension NSCache {
    class var sharedInstance : NSCache {
        struct Static {
            static let instance : NSCache = NSCache()
        }
        return Static.instance
    }
}

class ImageLoader:NSObject{
    
    class func downloadImageWithURL(urlString: String, completionHander: (image: UIImage?,url:String) -> Void) {
        //异步获取图片
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
            let cacheData = NSCache.sharedInstance.objectForKey(urlString) as? NSData
            if let goodData = cacheData {
                let imge = UIImage(data: goodData)
                //返回主线程
                dispatch_async(dispatch_get_main_queue(), {
                    completionHander(image: imge,url: urlString)
                })
                return
            }
            //如果没有图片则网络获取
            let session = NSURLSession.sharedSession()
            let url = NSURL(string: urlString)
            let downloadTask = session.dataTaskWithURL(url!, completionHandler: { (data, res, error) in
                guard error == nil && data != nil else {
                    completionHander(image: nil,url: urlString)
                    return
                }
                let image = UIImage(data: data!)
                NSCache.sharedInstance.setObject(data!, forKey: urlString)
                dispatch_async(dispatch_get_main_queue(), {
                    completionHander(image: image, url: urlString)
                })
            })
            downloadTask.resume()
        }
    }
}

虽然NSCache用起来比较简单,但还需要注意几个地方:

1:不同的NSCache对象管理的缓存是不同的,不能只通过名称来区别。同一个名称对应的是不同的缓存。所以,如果你需要在多个地方管理同一个缓存对象,要么把对象传递过去使用,要么使用单例来解决这个问题。

2:系统对缓存的清理并不是必须的。如果你设定了缓存上限,那么超过上限时系统便会自动清理。如果没有设定上限,则系统只有在内存警告发生的时候才会去清理这些内存。

3:NSCache的缓存如果设定了 cost,清理缓存时并不保证移除顺序,也不会由于谁的 cost比较小就清除谁,所以如果你想使用cost,那就要注意好这些规则。

4:NSCache并不会复制对象,而只是对要缓存的对象做了强引用,所以你的缓存对象并不需要实现NSCopying协议,想想都比较开心。

5:NSCache只能缓存到内存中,如果下次启动,缓存对象被释放,这些缓存也会被释放。

 

构建缓存时选用NSCache而非NSDictionary中的几个要点:

1、实现缓存时应选用NSCache而非NSDictionary对象。因为NSCache可以提供优雅的自动删减功能,而且是“线程安全的”,NSDictionary并不具备此优势,要明白对于缓存来说,线程的安全是非常重要的。此外,它与字典不同,并不会拷贝键。
2、可以给NSCache对象设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度则定义了缓存删减其中对象的时机,。但是绝对不要把这些尺度当成可靠的“硬限制”(hard limit),它们仅对NSCache起指导作用。
3、将NSPurgeableData与NSCache搭配使用,可以实现自动清除数据的功能,也就是说将NSPurgeableData对象所占用内存为系统所丢弃时,该对象自身也会从缓存中移除。

4、如果缓存使用得当,那么应用程序的响应速度就能提高。只有那种“重新计算起来很费事的”数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。

 

参考文章:

NSCache Class Reference

NSDiscardableContent

NSCache 源码分析  

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐