缓存框架学习(一 MemoryCache)

做开发时,合理的利用缓存是非常重要的,一方面可以为用户减少访问流量,另一方面也能加快应用的访问速度, 这部分的缓存学习内容是基于 PINCache的, PINCache项目是在Tumblr 宣布不在维护 TMCache 后,由 Pinterest 维护和改进的基于TMCache的一个内存缓存,修复了TMCache存在的性能和死锁问题,可以说是有了一个较大的提升。

PINCache 是多线程安全的, 使用键值队来保存数据。PINCache中包含两个类, 一个是PINMemoryCache负责内存缓存,一个是PINDiskCache负责磁盘缓存,PINCache属于它们的上层封装,将具体的缓存操作交给它的两个对象属性(PINMemoryCache属性,PINDiskCache属性)当App接收到内存警告时,PINCache会清理掉所有的内存缓存。关于缓存部分我想用三节来说,分别对应PINMemoryCache,PINDiskCache, 最后通过PINCache总结整个流程。我是将PINCache的源码敲了一遍,基本都了解了,在这一遍下来也颇有心得,于是决定写着系列关于缓存的文章,我想以后还会有关于多线程,网络部分的吧,学习框架,多学习,多进步

我觉得从.m文件开始讲起,因为这是整个框架的核心部分而.h是方法调用。

先铺一下需要了解的知识:

  1. 内存缓存:一般使用字典来作为数据的缓存池,配合一个保存每个内存缓存数据的缓存时间的字典,一个保存每个内存缓存数据的缓存容量的字典,一个保存内存缓存总容量的变量。对于增删改查操作,基本也都是围绕着字典来的,需要重点注意的就是在这些个操作过程的多线程安全问题,还有同步和异步访问方法,以及异步方法中的Block参数的循环引用问题。

  2. 线程安全:现在iPhone早已步入多核时代,多核就会产生并发操作,并发操作会遇到读写问题,比如去银行取款,取款时卡内余额显示1000,你决定取1000,当你进行取款操作的时候,你的家人往你卡上打了2000,假设取款操作先结束那么保存卡内余额的值会变成3000,如果存款操作先完成,那么取完款之后卡内余额变成了0, 所以会产生问题,这个时候我们就需要加锁操作,当执行读写,写写操作不能同时进行,必须要加同步锁,确保线程安全,同一时间只能有一条线程执行相应的操作。具体看框架中的代码:

     // 代码加锁
     - (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost
     {
         [self lock];
             // 缓存数据
             _dictionary[key] = object;
         [self unlock];
     }
     
     // 代码不加锁
     - (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost
     {
         // 缓存数据
         _dictionary[key] = object;
     }
    

    因为函数体在内存中是一片固定的内存区域,任何时间可以被任意线程访问,假设t1 线程 Thread1访问, 需要保存的值为 object1, key1,此时 Thread2访问值为 object2, key2,因为 Thread1未执行完函数,所以此时在函数内就有四个参数值 key1, object1, key2, object2,然后同时执行_dictionary[key] = object(_dictionary 为NSMutableDictionary不是线程安全)这条语句, 所以可能会出现_dictionary[key1] = object2 的问题; 如果进行加锁操作,当 Thread1未执行结束时, Thread2是无法执行_dictionary[key] = object 这条语句的。注意我们日常开发实在主线程中进行,很少涉及多线程问题。

  3. 锁:在PINCache中使用的是信号量来实现同步锁,具体代码如下:

     @property (strong, nonatomic) dispatch_semaphore_t lockSemaphore;
     
     - (void)lock
     {
         dispatch_semaphore_wait(_lockSemaphore, DISPATCH_TIME_FOREVER);
     }
     
     - (void)unlock
     {
         dispatch_semaphore_signal(_lockSemaphore);
     }
    

    我自己的代码中用的是pthread,效率能比信号量加锁稍微高一点点

  4. 缓存策略:有优先删除缓存最久,最少使用的策略,也有优先删除,容量最大,最少使用的策略。

  5. 临界区: 当访问一个公共资源时,而这些公共资源无法被多个线程同时访问,当一条线程进入临界区时, 其他线程必须等待,公用资源是互斥的

  6. 共享资源: 一个类中的属性, 成员变量全局变量就是这个对象的共享资源, 无论有多少个线程访问该对象, 访问的属性全局变量成员变量都是同一块内存区域, 不会因为线程不同创建不同的内存区域. 所以对于多线程操作的问题要将共享区域的取值, 设置值操作加锁

内存缓存我们要用个字典来存放数据,用个字典存放条数据的容量,用个字典来存放每条数据的最后的修改时间

/**
 *  缓存数据, key可以为 URL, value 为网络数据
 */
@property (nonatomic, strong) NSMutableDictionary *dictionary;
/**
 *  每个缓存数据的最后访问时间
 */
@property (nonatomic, strong) NSMutableDictionary *dates;
/**
 *  记录每个缓存的花费
 */
@property (nonatomic, strong) NSMutableDictionary *costs;

同样我们还希望当通过GCD异步操作时为我们的缓存过程单独有个线程名

#if OS_OBJECT_USE_OBJC  // iOS 6 之后 SDK 支持 GCD ARC, 不需要再 Dealloc 中 release
@property (nonatomic, strong) dispatch_queue_t concurrentQueue;
#else
@property (nonatomic, assign) dispatch_queue_t concurrentQueue;
#endif

锁,锁,锁重要的事说3遍

@implementation WNMemoryCache {
    pthread_mutex_t _lock;
}

#define Lock(_lock) (pthread_mutex_lock(&_lock))
#define Unlock(_lock) (pthread_mutex_unlock(&_lock))

初始化方法

- (instancetype)init {
    if (self = [super init]) {
        1.
        NSString *queueName = [NSString stringWithFormat:@"%@.%p",WannaMemoryCachePrefix,self];
        // 以指定的名称, 创建并发队列, 用于异步缓存数据
        _concurrentQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_CONCURRENT);
        
        2.
        _removeAllObjectOnMemoryWoring = YES;
        _removeAllObjectOnEnteringBackground = YES;
        
        3.
        _dictionary = [NSMutableDictionary dictionary];
        _dates = [NSMutableDictionary dictionary];
        _costs = [NSMutableDictionary dictionary];
        
        4. 
        _willAddObjectBlock = nil;
        _willRemoveObjectBlock = nil;
        _willRemoveAllObjectsBlock = nil;
        
        _didAddObjectBlock = nil;
        _didRemoveObjectBlock = nil;
        _didRemoveAllObjectsBlock = nil;
        
        _didReceiveMemoryWarningBlock = nil;
        _didEnterBackgroundBlock = nil;
        
        5. 
        _ageLimit = 0.0;
        _costLimit = 0;
        _totalCost = 0;

        6.
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_4_0 && !TARGET_OS_WATCH
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(didReceiveEnterBackgroundNotification:)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
        
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(didReceiveMemoryWarningNotification:)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];

#endif
    }
    return self;
}
  1. 指定并发队列的名称
  2. 默认进入后台,收到内存警告时清理所有的内存缓存这个时候需要监听内存警告 name:UIApplicationDidReceiveMemoryWarningNotification和进入后台 name:UIApplicationDidEnterBackgroundNotification的通知 第6步
  3. 将缓存数据,时间,消耗的字典进行初始化,直接访问属性能够避免通过self调用get方法时消息发送时间的花费
  4. 定义的回调函数,并初始化为空
  5. _ageLimit 缓存存活时间, 如果设置为一个大于0的值, 就被开启为 TTL 缓存(指定存活期的缓存),即如果 ageLimit > 0 => ttlCache = YES;
    _constLimit 内存花费限制,_totalConst 总的内存缓存消耗

这里要说一下TTLCache,当一个Ceche被设置为TTLCache,那么它的存活时间只有指定的时长ageLimit,当它存活的时间超过ageLimit时会被清理。在PINCache中设置ageLimit并未将TTLCache设置称为YES,但是通过阅读PINCache源码发现,只有设置好ageLimit,TTLCache才能在一定的时间限制内清空过期缓存,而设置ageLimit时就说明缓存有了存活周期,所以此时一定是TTLCache;(如理解有误欢迎指正)

@property (strong, readonly) __nonnull dispatch_queue_t concurrentQueue;
/** 内存缓存所占的总容量*/
@property (assign, readonly) NSUInteger totalCost;
@property (assign) NSUInteger costLimit;
/**
 *  缓存存活时间, 如果设置为一个大于0的值, 就被开启为 TTL 缓存(指定存活期的缓存),即如果 ageLimit > 0 => ttlCache = YES;
 */
@property (assign) NSTimeInterval ageLimit;
/**
 *  如果指定为 YES, 缓存行为就像 TTL 缓存, 缓存只在指定的存活期(ageLimit)内存活
 * Accessing an object in the cache does not extend that object's lifetime in the cache
 * When attempting to access an object in the cache that has lived longer than self.ageLimit,
 * the cache will behave as if the object does not exist
 */
@property (assign, getter=isTTLCache) BOOL ttlCache;
/** 是否当内存警告时移除缓存, 默认 YES*/
@property (assign) BOOL removeAllObjectOnMemoryWoring;
/** 是否当进入到后台时移除缓存, 默认 YES*/
@property (assign) BOOL removeAllObjectOnEnteringBackground;

@property (copy) WNMemoryCacheObjectBlock __nullable willAddObjectBlock;

@property (copy) WNMemoryCacheObjectBlock __nullable willRemoveObjectBlock;

@property (copy) WNMemoryCacheObjectBlock __nullable didAddObjectBlock;

@property (copy) WNMemoryCacheObjectBlock __nullable didRemoveObjectBlock;

@property (copy) WNMemoryCacheBlcok __nullable willRemoveAllObjectsBlock;
@property (copy) WNMemoryCacheBlcok __nullable didRemoveAllObjectsBlock;
@property (copy) WNMemoryCacheBlcok __nullable didReceiveMemoryWarningBlock;
@property (copy) WNMemoryCacheBlcok __nullable didEnterBackgroundBlock;

这里并没有指定为nonatomic,所以就是默认的atomic,atomic是原子属性,线程安全。atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。在多线程环境下,原子操作是必要的,否则有可能引起错误的结果。

这样的话setter/getter会变成下面的样式,添加线程安全

- (BOOL)isTTLCache {
    BOOL isTTLCache;
    
    [self lock];
        isTTLCache = _ttlCache;
    [self unlock];
    
    return isTTLCache;
}

- (void)setTtlCache:(BOOL)ttlCache {
    [self lock];
        _ttlCache = ttlCache;
    [self unlock];
}

getter实现创建一个局部变量用于在临界区内获得对象内部的属性值,setter在临界区内设置属性

/**
 *  收到内存警告操作
 */
- (void)didReceiveMemoryWarningNotification:(NSNotification *)notify {
    1.
    if (self.removeAllObjectOnMemoryWoring) {
        [self removeAllObject:nil];
    }
    
    __weak typeof(self)weakSelf = self;
    AsyncOption(
                __strong typeof(weakSelf)strongSelf = weakSelf;
                if (!strongSelf) return ;
                2.
                Lock(_lock);
                WNMemoryCacheBlcok didReceiveMemoryWarningBlock = strongSelf->_didReceiveMemoryWarningBlock;
                Unlock(_lock);
                3.
                if (didReceiveMemoryWarningBlock) {
                    didReceiveMemoryWarningBlock(strongSelf);
                }
    );
}
  1. 如果指定了当收到内存警告时清理缓存,执行 removeAllObject 方法
  2. 加锁获得当前线程指定的_didReceiveMemoryWaringBlock 回调
  3. 执行回调

这里要说一下, 为什么只在第二步中加锁,第三步没有加锁;

先指定个假设前提回调是个超级耗时操作, 并且现在函数被两条线程访问,

(1). 如果没有锁当线程1获得didReceiveMemoryWarningBlock,这个时候 CPU 调度到线程2,由于didReceiveMemoryWarningBlock在线程1中已经获得,所以在线程2中执行的起始是线程1中的回调,导致回调不正确;

(2). 如果将第三步也加锁,线程1执行到第二步,加锁,获得回调并执行,依旧是当线程1执行到第二步时, CPU 调度到线程2,此时线程2执行发现线程1加锁操作,导致线程2等待,线程1安全执行线程1的回调,而回调是一个假设耗时10000s 的操作,导致线程2需要等待10000s, 效率低下;

(3).上述加锁方式执行的话,获得回调函数是线程安全, 线程1获得线程1中的回调, 线程2获得线程2中的回调, 所以即使在执行回调时进行 CPU 调度,那么线程1依旧执行的是线程1的回调,线程2执行线程2的回调,提高了效率,又避免安全性

所以,加锁可以避免线程问题,但盲目加锁会造成效率执行低下

/**
 *  程序进入后台操作
 */
- (void)didReceiveEnterBackgroundNotification:(NSNotification *)notify {
    if (self.removeAllObjectOnEnteringBackground) {
        [self removeAllObject:nil];
    }
    __weak typeof(self)weakSelf = self;
    AsyncOption(
               __strong typeof(weakSelf)strongSelf = weakSelf;
               if (!strongSelf) return ;
               Lock(_lock);
               WNMemoryCacheBlcok didEnterBackgroundBlock = strongSelf->_didEnterBackgroundBlock;
               Unlock(_lock);
               if (didEnterBackgroundBlock) {
                   didEnterBackgroundBlock(strongSelf);
               }
    );

}

函数体与执行内存警告的函数体相同, 就是回调方法不同。

继续往下看:

/**
 *  线程安全, 移除指定 key 的缓存, 并执行回调
 *
 *  @param key 指定的缓存 key
 */
- (void)removeObjectAndExectureBlockForKey:(NSString *)key {
    1.
    Lock(_lock);
    id object = _dictionary[key];
    NSNumber *cost = _costs[key];
    WNMemoryCacheObjectBlock willRemoveObjectBlock = _willRemoveObjectBlock;
    WNMemoryCacheObjectBlock didRemoveObjectBlcok = _didRemoveObjectBlock;
    Unlock(_lock);
    
    2.
    if (willRemoveObjectBlock) {
        willRemoveObjectBlock(self, key, object);
    }
    
    3.
    Lock(_lock);
    if (cost) {
        _totalCost -= [cost unsignedIntegerValue];
    }
    [_dictionary removeObjectForKey:key];
    [_costs removeObjectForKey:key];
    [_dates removeObjectForKey:key];
    Unlock(_lock);
    
    4.
    if (didRemoveObjectBlcok) {
        didRemoveObjectBlcok(self, key, object);
    }
}

1 . 加锁获得对应 key 存储的对象, 消耗, 及制定的回调

2 . 执行将要移除的回调, 与第四步形成呼应

3 . 如果存储该缓存存在花费, 从总花费中减去该该缓存的花费, 移除 key 对应的缓存对象, 花费以及最后修改的时间,而这些操作是要放在一片临界区内的

       /**
     *  使所有的缓存时间 <= date
     *
     *  @param date 指定的缓存时间
     */
    - (void)trimMemoryToDate:(NSDate *)date {
        1.
        Lock(_lock);
        NSArray *sortKeyByDate = (NSArray *)[[_dates keysSortedByValueUsingSelector:@selector(compare:)] reverseObjectEnumerator];
        Unlock(_lock);
        
        2.
        NSUInteger index = [self binarySearchEqualOrMoreDate:date fromKeys:sortKeyByDate];
        3.
            NSIndexSet *indexSets = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, index)];
        4.
        [sortKeyByDate enumerateObjectsAtIndexes:indexSets
                                         options:NSEnumerationConcurrent
                                      usingBlock:^(NSString *key, NSUInteger idx, BOOL * _Nonnull stop) {       
                                            5.
                                          if (key) {
                                              [self removeObjectAndExectureBlockForKey:key];
                                          }
                                      }];
    }

这个方法我做了些修改, 在 PINCache 中,第一步按时间排序, 第二部从头开始遍历, 将时间 < 指定 date 的值移除, 我觉得当数据量很大时, 遍历的效率低下, 于是我写了个二分搜索, 搜索第一个大于等于 date 的位置, 所以我在第一步将排序结果进行倒转, 小的在前,大的在后

  1. 对_dates根据 key 排序, 排序结果是时间大的在前面, 比如20150101 在 20141230前面; 之后执行数组倒转, 小的在前, 大的在后
  1. 二分搜索算法, 搜索第一个大于等于指定 date 的位置
  1. 创建区间[0, index)
  1. 变量区间, 如果有 key, 就将其从缓存中移除, 并执行指定的"移除数据的回调"
    /**
     *   根据缓存大小移除缓存到临界值, 缓存大的先被移除
     *
     *  @param limit 缓存临界值
     */
    - (void)trimToCostLimit:(NSUInteger)limit {
        // 1.
        __block NSUInteger totalCost = 0;
        // 2.
        Lock(_lock);
        totalCost = _totalCost;
        NSArray *keysSortByCost = [_costs keysSortedByValueUsingSelector:@selector(compare:)];
        Unlock(_lock);
        // 3.
        if (totalCost <= limit) {
            return ;
        }
        // 4.
        [keysSortByCost enumerateObjectsWithOptions:NSEnumerationReverse
                                         usingBlock:^(NSString *key, NSUInteger idx, BOOL * _Nonnull stop) {
                                             [self removeObjectAndExectureBlockForKey:key];
                                             Lock(_lock);
                                             totalCost = _totalCost;
                                             Unlock(_lock);
                                             if (totalCost <= limit) {
                                                 *stop = YES;
                                             }
                                         }];
    }       
  1. 设置局部变量, 负责记录在移除的过程中总花费的变化
  1. 加锁获取公共资源
  1. 如果当前的总花费小于限制值,直接返回
  1. 执行移除缓存操作, 从大到小逐个移除, 同时加锁修改总花费, 当总花费小于限制时, 停止移除操作
/**
 *  递归检查并清除超过规定时间的缓存对象, TTL缓存操作
 */
- (void)trimToAgeLimitRecursively {

    Lock(_lock);
    NSTimeInterval ageLimit = _ageLimit;
    BOOL ttlCache = _ttlCache;
    Unlock(_lock);
    
    if (ageLimit == 0.0 || !ttlCache) {
        return ;
    }
    // 从当前时间开始, 往前推移 ageLimit(内存缓存对象允许存在的最大时间)
    NSDate *trimDate = [NSDate dateWithTimeIntervalSinceNow:-ageLimit];
    // 将计算得来的时间点之前的数据清除, 确保每个对象最大存在 ageLimit 时间
    [self trimMemoryToDate:trimDate];
    
    // ageLimit 之后在递归执行
    __weak typeof(self)weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(ageLimit * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong typeof(weakSelf)strongSelf = weakSelf;
        [strongSelf trimToAgeLimitRecursively];
    });
}

这个方法我写了注释, 主要思路就是从当前时间为起点,往前推移一个设置的缓存存活时间, 这段时间段内的缓存应当被清理,然后 ageLimit 之后继续执行该方法, 同样清理这段时间里的缓存, 这是个递归调用, 每隔 ageLimit 时间请一次缓存

/**
 *  移除所有的数据
 *
 *  @param callBack 回调
 */
- (void)removeAllObject:(WNMemoryCacheBlcok)callBack {
    __weak typeof(self)weakSelf = self;
    // 异步移除所有数据
    AsyncOption(
                __strong typeof(weakSelf)strongSelf = weakSelf;
                [strongSelf removeAllObjects];
                if (callBack) {
                    callBack(strongSelf);
                });
}

异步指定移除操作,将移除缓存方法放入到 GCD 的异步线程中

/**
 *  线程安全的缓存对象的读取操作, 所有关于缓存读取的操作都是调用该方法
 *
 *  @param key 要获得的缓存对应的 key
 *
 *  @return 缓存对象
 */
- (__nullable id)objectForKey:(NSString *)key {
    if (!key) {
        return nil;
    }
    
    NSDate *now = [NSDate date];
    Lock(_lock);
    id object = nil;
    /**
     *  如果指定了 TTL, 那么判断是否指定存活期, 如果指定存活期, 要判断对象是否在存活期内
     *  如果没有指定 TTL, 那么缓存对象一定存在, 直接获得
     */
    if (!self->_ttlCache ||
        self->_ageLimit <= 0 ||
        fabs([_dates[key] timeIntervalSinceDate:now]) < self->_ageLimit) {
        object = _dictionary[key];
    }
    Unlock(_lock);
    if (object) {
        Lock(_lock);
        _dates[key] = now;
        Unlock(_lock);
    }
    return object;
}

/**
 *  线程安全的缓存存储操作, 所有的缓存写入都是调用该方法
 *
 *  @param object 要缓存的对象
 *  @param key    缓存对象对应的 Key
 *  @param cost   缓存的代价
 */
- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost {
    if (!key || !object) {
        return ;
    }
    // 加锁获得回调
    Lock(_lock);
    WNMemoryCacheObjectBlock willAddObjectBlock = _willAddObjectBlock;
    WNMemoryCacheObjectBlock didAddObjectBlock = _didAddObjectBlock;
    NSUInteger coseLimit = _costLimit;
    Unlock(_lock);
    
    // 执行回调
    if (willAddObjectBlock) {
        willAddObjectBlock(self, key, object);
    }
    
    // 加锁设置缓存信息
    Lock(_lock);
    _dictionary[key] = object, _costs[key] = @(cost), _dates[key] = [NSDate date];
    _totalCost += cost;
    Unlock(_lock);
    
    // 执行回调     
    if (didAddObjectBlock) {
        didAddObjectBlock(self, key, object);
    }
    
    // 如果设置花费限制, 判断此时总花费是否大于花费限制
    if (coseLimit > 0) {
        [self trimCostByDateToCostLimit:coseLimit];
    }
}

/**
 *  根据时间, 先移除时间最久的缓存, 直到缓存容量小于等于指定的 limit
 *  LRU(Last Recently Used): 最久未使用算法, 使用时间距离当前最就的将被移除
 */
- (void)trimCostByDateToCostLimit:(NSUInteger)limit {
    __block NSUInteger totalCost = 0;
    Lock(_lock);
    totalCost = _totalCost;
    NSArray *keysSortedByDate = [_dates keysSortedByValueUsingSelector:@selector(compare:)];
    Unlock(_lock);
    if (totalCost <= limit) {
        return;
    }
    
    // 先移除时间最长的缓存, date 时间小的
    [keysSortedByDate enumerateObjectsUsingBlock:^(NSString *key, NSUInteger idx, BOOL * _Nonnull stop) {
        [self removeObjectAndExectureBlockForKey:key];
        Lock(_lock);
        totalCost = _totalCost;
        Unlock(_lock);
        if (totalCost <= limit) {
            *stop = YES;
        }
    }];
}

执行缓存的设置和获取操作,最核心的是线程安全设置和获得, 异步也只是将线程安全的方法放入到异步线程, 在此不再赘述, 更多看源码, 有详细注释

还有两点要说:

PINCache 实现了下标脚本设置和获取方法, 即通过 id obj = cache[@"key"] 获得缓存值, cache[@"key"] = object设置缓存值.

具体步骤是两个协议方法

@required
/**
 *  下标脚本的取值操作, 实现该方法, 可以通过下标脚本获得存储的缓存值
 *  就像这样获得缓存值 id obj = cache[@"key"]
 *  @param key 缓存对象关联的 key
 *
 *  @return  指定 key 的缓存对象
 */
- (id)objectForKeyedSubscript:(NSString *)key;

/**
 *  下标脚本的设置值操作, 实现该方法可以通过下标脚本设置缓存
 *  像这样 cache[@"key"] = object
 *  @param obj 要缓存的对象
 *  @param key 缓存对象关联的 key
 */
- (void)setObject:(id)obj forKeyedSubscript:(NSString *)key;

/**
 *  以上两个方法应该确保线程安全
 */

MemoryCache 中具体实现

#pragma mark - Protocol Method
- (void)setObject:(id)obj forKeyedSubscript:(NSString *)key {
    [self setObject:obj forKey:key withCost:0];
}

- (id)objectForKeyedSubscript:(NSString *)key {
    return [self objectForKey:key];
}

设置和获得缓存都应该是线程安全的

还有一点就是由于我们设置属性为 atomic, 所以我们的 setter/getter 要确保线程安全, 具体上代码:

- (NSTimeInterval)ageLimit {
    Lock(_lock);
    NSTimeInterval age = _ageLimit;
    Unlock(_lock);
    return age;
}

- (void)setAgeLimit:(NSTimeInterval)ageLimit {
    Lock(_lock);
    _ageLimit = ageLimit;
    if (ageLimit > 0) {
        _ttlCache = YES;
    }
    Unlock(_lock);
    [self trimToAgeLimitRecursively];
}

- (NSUInteger)costLimit {
    Lock(_lock);
    NSUInteger limit = _costLimit;
    Unlock(_lock);
    return limit;
}

- (void)setCostLimit:(NSUInteger)costLimit {
    Lock(_lock);
    _costLimit = costLimit;
    Unlock(_lock);
    if (costLimit > 0) {
        [self trimCostByDateToCostLimit:costLimit];
    }
}

以上就是内存缓存一些必要知识, 以上只是一部分代码, 具体看项目源码 WannaCache

感谢:

Amin706 PINCache

阳光飞鸟 atomic与nonatomic的区别

临界区-百度百科

临界区-维基百科

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容