iOS缓存 NSCache详解及SDWebImage缓存策略源码分析

本文深入探讨iOS缓存机制,详细讲解NSCache的使用及其优于普通字典的特性,如线程安全和自动释放。通过源码分析,揭示SDWebImage的图片缓存策略,包括内存缓存和磁盘缓存的实现。文章还补充了其他第三方库的缓存实现,强调不断探索以优化性能。
摘要由CSDN通过智能技术生成

你要知道的NSCache都在这里

转载请注明出处 http://blog.csdn.net/u014205968/article/details/78356828

本篇文章首先会详细讲解NSCache的基本使用,NSCacheFoundation框架提供的缓存类的实现,使用方式类似于可变字典,由于NSMutableDictionary的存在,很多人在实现缓存时都会使用可变字典,但NSCache在实现缓存功能时比可变字典更方便,最重要的是它是线程安全的,而NSMutableDictionary不是线程安全的,在多线程环境下使用NSCache是更好的选择。接着,会通过源码讲解SDWebImage的缓存策略。最后简要补充了第三方YYCache的实现思路。

NSCache

NSCache的使用很方便,提供了类似可变字典的使用方式,但它比可变字典更适用于实现缓存,最重要的原因为NSCache是线程安全的,使用NSMutableDictionary自定义实现缓存时需要考虑加锁和释放锁,NSCache已经帮我们做好了这一步。其次,在内存不足时NSCache会自动释放存储的对象,不需要手动干预,如果是自定义实现需要监听内存状态然后做进一步的删除对象的操作。还有一点就是NSCache的键key不会被复制,所以key不需要实现NSCopying协议。

上面讲解的三点就是NSCache相比于NSMutableDictionary实现缓存功能的优点,在需要实现缓存时应当优先考虑使用NSCache

首先看一下NSCache提供的属性和相关方法:

//名称
@property (copy) NSString *name;

//NSCacheDelegate代理
@property (nullable, assign) id<NSCacheDelegate> delegate;

//通过key获取value,类似于字典中通过key取value的操作
- (nullable ObjectType)objectForKey:(KeyType)key;

//设置key、value
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost

/*
设置key、value
cost表示obj这个value对象的占用的消耗?可以自行设置每个需要添加进缓存的对象的cost值
这个值与后面的totalCostLimit对应,如果添加进缓存的cost总值大于totalCostLimit就会自动进行删除
感觉在实际开发中直接使用setObject:forKey:方法就可以解决问题了
*/
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;

//根据key删除value对象
- (void)removeObjectForKey:(KeyType)key;

//删除保存的所有的key-value
- (void)removeAllObjects;

/*
NSCache能够占用的消耗?的限制
当NSCache缓存的对象的总cost值大于这个值则会自动释放一部分对象直到占用小于该值
非严格限制意味着如果保存的对象超出这个大小也不一定会被删除
这个值就是与前面setObject:forKey:cost:方法对应
*/
@property NSUInteger totalCostLimit;    // limits are imprecise/not strict

/*
缓存能够保存的key-value个数的最大数量
当保存的数量大于该值就会被自动释放
非严格限制意味着如果超出了这个数量也不一定会被删除
*/
@property NSUInteger countLimit;    // limits are imprecise/not strict
/*
这个值与NSDiscardableContent协议有关,默认为YES
当一个类实现了该协议,并且这个类的对象不再被使用时意味着可以被释放
*/
@property BOOL evictsObjectsWithDiscardedContent;

@end

//NSCacheDelegate协议
@protocol NSCacheDelegate <NSObject>
@optional
//上述协议只有这一个方法,缓存中的一个对象即将被删除时被回调
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end

通过接口可以看出,NSCache提供的方法都很简单,属性的意义也很明确,接下来举一个简单的栗子:

//定义一个CacheTest类实现NSCacheDelegate代理
@interface CacheTest: NSObject <NSCacheDelegate>

@end

@implementation CacheTest

//当缓存中的一个对象即将被删除时会回调该方法
- (void)cache:(NSCache *)cache willEvictObject:(id)obj
{
    NSLog(@"Remove Object %@", obj);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //创建一个NSCache缓存对象
        NSCache *cache = [[NSCache alloc] init];
        //设置缓存中的对象个数最大为5个
        [cache setCountLimit:5];
        //创建一个CacheTest类作为NSCache对象的代理
        CacheTest *ct = [[CacheTest alloc] init];
        //设置代理
        cache.delegate = ct;

        //创建一个字符串类型的对象添加进缓存中,其中key为Test
        NSString *test = @"Hello, World";
        [cache setObject:test forKey:@"Test"];

        //遍历十次用于添加
        for (int i = 0; i < 10; i++)
        {
            [cache setObject:[NSString stringWithFormat:@"Hello%d", i] forKey:[NSString stringWithFormat:@"World%d", i]];
            NSLog(@"Add key:%@  value:%@ to Cache", [NSString stringWithFormat:@"Hello%d", i], [NSString stringWithFormat:@"World%d", i]);
        }

        for (int i = 0; i < 10; i++)
        {
            NSLog(@"Get value:%@ for key:%@", [cache objectForKey:[NSString stringWithFormat:@"World%d", i]], [NSString stringWithFormat:@"World%d", i]);
        }

        [cache removeAllObjects];

        for (int i = 0; i < 10; i++)
        {
            NSLog(@"Get value:%@ for key:%@", [cache objectForKey:[NSString stringWithFormat:@"World%d", i]], [NSString stringWithFormat:@"World%d", i]);
        }

        NSLog(@"Test %@", test);
    }

    return 0;
}

输出结果如下:

//第一个for循环输出
Add key:Hello0  value:World0 to Cache
Add key:Hello1  value:World1 to Cache
Add key:Hello2  value:World2 to Cache
Add key:Hello3  value:World3 to Cache
Remove Object Hello, World
Add key:Hello4  value:World4 to Cache
Remove Object Hello0
Add key:Hello5  value:World5 to Cache
Remove Object Hello1
Add key:Hello6  value:World6 to Cache
Remove Object Hello2
Add key:Hello7  value:World7 to Cache
Remove Object Hello3
Add key:Hello8  value:World8 to Cache
Remove Object Hello4
Add key:Hello9  value:World9 to Cache
//第二个for循环输出
Get value:(null) for key:World0
Get value:(null) for key:World1
Get value:(null) for key:World2
Get value:(null) for key:World3
Get value:(null) for key:World4
Get value:Hello5 for key:World5
Get value:Hello6 for key:World6
Get value:Hello7 for key:World7
Get value:Hello8 for key:World8
Get value:Hello9 for key:World9
//removeAllObjects输出
Remove Object Hello5
Remove Object Hello6
Remove Object Hello7
Remove Object Hello8
Remove Object Hello9
//最后一个for循环输出
Get value:(null) for key:World0
Get value:(null) for key:World1
Get value:(null) for key:World2
Get value:(null) for key:World3
Get value:(null) for key:World4
Get value:(null) for key:World5
Get value:(null) for key:World6
Get value:(null) for key:World7
Get value:(null) for key:World8
Get value:(null) for key:World9
//输出test字符串
Test Hello, World

上面的代码创建了一个NSCache对象,设置了其最大可缓存对象的个数为5个,从输出可以看出,当我们要添加第六个对象时NSCache自动删除了我们添加的第一个对象并触发了NSCacheDelegate的回调方法,添加第七个时也是同样的,删除了缓存中的一个对象才能添加进去。

在第二个for循环中,我们通过key取出所有的缓存对象,前五个对象取出都为nil,因为在添加后面的对象时前面的被删除了,所以,当我们从缓存中获取对象时一定要判断是否为空,我们无法保证缓存中的某个对象不会被删除。

接着调用了NSCacheremoveAllObjects方法,一旦调用该方法,NSCache就会将其中保存的所有对象都释放掉,所以,可以看到调用该方法后NSCacheDelegate的回调方法执行了五次,将NSCache中的所有缓存对象都清空了。

在最后一个for循环中,根据key获取缓存中的对象时可以发现都为空了,因为都被释放了。

前面还创建了一个字符串的局部变量,在最开始将其加入到了缓存中,后来随着其他对象的添加,该字符串被缓存释放了,但由于局部变量对其持有强引用所以使用test还是可以访问到的,这是最基本的ARC知识,所以,NSCache在释放一个对象时只是不再指向这个对象,即,该对象的引用计数减一,如果有其他指针指向它,这个对象不会被释放。

上面就是NSCache的基本用法了,我们只需要设置对象和获取对象,其他事情NSCache都帮我们做完了,因此,实现缓存功能时,使用NSCache就是我们的不二之选。

再看一个栗子:

- (void)viewWillAppear:(BOOL)animated
{
    self.cache = [[NSCache alloc] init];
    [self.cache setCountLimit:5];
    self.cache.delegate = self;
    [self.cache setObject:@"AA" forKey:@"BBB"];
    [self.cache setObject:@"MMMM" forKey:@"CCC"];
}

- (void)cache:(NSCache *)cache willEvictObject:(id)obj
{
    NSLog(@"REMOVE %@", obj);
}

这是一个有视图控制器的栗子,我们创建了一个NSCache对象,并在其中添加了对象,当点击home键,程序进入后台后,可以发现NSCacheDelegate的回调函数触发了,所以,当程序进入后台,NSCache对象会自动释放所有的对象。如果在模拟器上模拟内存警告,也可以发现NSCache会释放所有的对象。所以NSCache删除缓存中的对象会在以下情形中发生:

  • NSCache缓存对象自身被释放

  • 手动调用removeObjectForKey:方法

  • 手动调用removeAllObjects

  • 缓存中对象的个数大于countLimit,或,缓存中对象的总cost值大于totalCostLimit

  • 程序进入后台后

  • 收到系统的内存警告

SDWebImage的缓存策略

在了解了NSCache的基本使用后,现在来通过SDWebImage的源码看看它是怎样进行图片的缓存操作的。由于篇幅的问题,本文将源码中的英文注释删掉了,有需要的读者可以对照着注释源码查阅本文章。本节内容包括了GCDNSOperation等多线程相关的知识,有疑问的读者可以查阅本博客iOS多线程——你要知道的GCD都在这里 以及 iOS多线程——你要知道的NSOperation都在这里 相关内容,本文就不再赘述了。

首先看一下官方给的设置图片后执行时序图:

SDWebImage执行时序图

整个执行流程非常清晰明了,本篇文章的重点在第四步、第五步和第八步,关于网络下载,以后会在讲解NSURLSession后进行相关的源码分析。查看SDWebImage的源码,与缓存有关的一共有四个文件SDImageCacheConfigSDImageCache,首先看一下SDImageCacheConfig的头文件:

@interface SDImageCacheConfig : NSObject

//是否压缩图片,默认为YES,压缩图片可以提高性能,但是会消耗内存
@property (assign, nonatomic) BOOL shouldDecompressImages;

//是否关闭iCloud备份,默认为YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;

//是否使用内存做缓存,默认为YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;

/** 缓存图片的最长时间,单位是秒,默认是缓存一周
 * 这个缓存图片最长时间是使用磁盘缓存才有意义
 * 使用内存缓存在前文中讲解的几种情况下会自动删除缓存对象
 * 超过最长时间后,会将磁盘中存储的图片自动删除
 */
@property (assign, nonatomic) NSInteger maxCacheAge;

//缓存占用最大的空间,单位是字节
@property (assign, nonatomic) NSUInteger maxCacheSize;

@end

NSCacheConfig类可以看得出来就是一个配置类,保存一些缓存策略的信息,没有太多可以讲解的地方,看懂就好,看一下NSCacheConfig.m文件的源码:

static const NSInteger kDefaultCacheMaxCacheAge = 60 * 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值