缓存框架学习(一 MemoryCache)

作者: 小梁同学 | 来源:发表于2016-05-05 22:12 被阅读3774次

    做开发时,合理的利用缓存是非常重要的,一方面可以为用户减少访问流量,另一方面也能加快应用的访问速度, 这部分的缓存学习内容是基于 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的区别

    临界区-百度百科

    临界区-维基百科

    相关文章

      网友评论

      • 未来靠打拼:- (void)setObject:(id)object forKey:(NSString *)key
        {
        [self setObject:object forKey:key withCost:0];
        }
        每次内存缓存保存 传入cost都是0 是怎么实现内存lru的。

      本文标题:缓存框架学习(一 MemoryCache)

      本文链接:https://www.haomeiwen.com/subject/wwndrttx.html