深入源碼理解YYCache 恳邀、SDWebImage、AFNetworking灶轰、NSCache 緩存方式與對比

深入源碼理解YYCache 谣沸、SDWebImage、AFNetworking笋颤、NSCache 緩存方式與對比

轉(zhuǎn)載請注明出處 http://www.reibang.com/p/18d9fe85266d

在之前的一篇文章iOS緩存 NSCache詳解及SDWebImage緩存策略源碼分析中詳細(xì)講解了NSCache的用法以及SDWebImage內(nèi)存和磁盤緩存的源碼分析乳附,本篇文章將簡要講解AFNetworking緩存類和YYCache并作出對比。

由于之前的一篇文章已經(jīng)詳細(xì)講解了NSCacheSDWebImage緩存策略伴澄,本篇文章不再贅述赋除,會(huì)簡要介紹一下AFNetworkingYYCache的源碼。

AFNetworking圖片緩存AFAutoPurgingImageCache

AFNetworking也提供了同SDWebImage一樣的下載圖片的功能非凌,也提供了緩存這些圖片的功能贤重,但它只提供了內(nèi)存緩存,沒有提供磁盤緩存功能清焕。

看一下頭文件:

//緩存協(xié)議并蝗,如果用戶需要實(shí)現(xiàn)自定義的
@protocol AFImageCache <NSObject>

//添加圖片并傳遞一個(gè)唯一id,一般使用圖片的URL
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;

//刪除id為identifier的圖片
- (BOOL)removeImageWithIdentifier:(NSString *)identifier;

//刪除所有緩存圖片
- (BOOL)removeAllImages;

//根據(jù)id獲取圖片
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier;
@end

//AFImageRequestCache協(xié)議秸妥,繼承AFImageCache協(xié)議
@protocol AFImageRequestCache <AFImageCache>

//添加圖片滚停,傳入下載圖片的request和額外的id
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

//刪除圖片,傳入下載圖片的request和額外的id
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

//根據(jù)下載圖片的request和額外的id獲取圖片
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

@end

//AFNetworking提供的緩存類粥惧,
@interface AFAutoPurgingImageCache : NSObject <AFImageRequestCache>

//單位是字節(jié)键畴,能夠支持最大緩存大小
@property (nonatomic, assign) UInt64 memoryCapacity;

//建議的當(dāng)內(nèi)存緩存要釋放的時(shí)候需要釋放到多大的大小
@property (nonatomic, assign) UInt64 preferredMemoryUsageAfterPurge;

//當(dāng)前內(nèi)存緩存占用的字節(jié)大小
@property (nonatomic, assign, readonly) UInt64 memoryUsage;

//構(gòu)造函數(shù)
- (instancetype)init;

//構(gòu)造函數(shù)
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity;

@end

從頭文件可以看出,AFNetworking提供的AFAutoPurgingImageCache接口不多突雪,而且都很簡單起惕,只實(shí)現(xiàn)了內(nèi)存緩存的功能。

看一下實(shí)現(xiàn)文件:

//緩存對象包裝類
@interface AFCachedImage : NSObject
//緩存的圖片
@property (nonatomic, strong) UIImage *image;
//id
@property (nonatomic, strong) NSString *identifier;
//圖片字節(jié)大小
@property (nonatomic, assign) UInt64 totalBytes;
//淘汰算法是LRU所以需要記錄上次訪問時(shí)間
@property (nonatomic, strong) NSDate *lastAccessDate;
//沒用到的屬性
@property (nonatomic, assign) UInt64 currentMemoryUsage;

@end

@implementation AFCachedImage
//初始化構(gòu)函數(shù)
-(instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier {
    if (self = [self init]) {
        self.image = image;
        self.identifier = identifier;
        //計(jì)算圖片的字節(jié)大小咏删,每個(gè)像素占4字節(jié)32位
        CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
        CGFloat bytesPerPixel = 4.0;
        CGFloat bytesPerSize = imageSize.width * imageSize.height;
        self.totalBytes = (UInt64)bytesPerPixel * (UInt64)bytesPerSize;
        self.lastAccessDate = [NSDate date];
    }
    return self;
}
//通過緩存對象獲取圖片時(shí)要更新上次訪問時(shí)間為當(dāng)前時(shí)間
- (UIImage*)accessImage {
    //直接使用NSDate
    self.lastAccessDate = [NSDate date];
    return self.image;
}

- (NSString *)description {
    NSString *descriptionString = [NSString stringWithFormat:@"Idenfitier: %@  lastAccessDate: %@ ", self.identifier, self.lastAccessDate];
    return descriptionString;

}

@end

//AFNetworking緩存類的豬腳
@interface AFAutoPurgingImageCache ()
//可變字典用于存儲所有的緩存對象AFCachedImage對象惹想,key為字符串類型
@property (nonatomic, strong) NSMutableDictionary <NSString* , AFCachedImage*> *cachedImages;
//當(dāng)前緩存對象內(nèi)存占用大小
@property (nonatomic, assign) UInt64 currentMemoryUsage;
//用于線程安全防止產(chǎn)生競爭條件,沒有用鎖
@property (nonatomic, strong) dispatch_queue_t synchronizationQueue;
@end

@implementation AFAutoPurgingImageCache

//構(gòu)造函數(shù)督函,默認(rèn)內(nèi)存占用100M嘀粱,每次清除緩存到60M
- (instancetype)init {
    return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024];
}

//構(gòu)造函數(shù)
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity {
    if (self = [super init]) {
        self.memoryCapacity = memoryCapacity;
        self.preferredMemoryUsageAfterPurge = preferredMemoryCapacity;
        self.cachedImages = [[NSMutableDictionary alloc] init];
        //創(chuàng)建一個(gè)并行隊(duì)列,但后面使用時(shí)都是在同步情況或barrier情況下辰狡,隊(duì)列中的任務(wù)還是以串行執(zhí)行
        //可以防止產(chǎn)生競爭條件锋叨,保證線程安全
        NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
        self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

        //添加通知,監(jiān)聽收到系統(tǒng)的內(nèi)存警告后刪除所有緩存對象
        [[NSNotificationCenter defaultCenter]
         addObserver:self
         selector:@selector(removeAllImages)
         name:UIApplicationDidReceiveMemoryWarningNotification
         object:nil];

    }
    return self;
}
//析構(gòu)函數(shù)宛篇,刪除通知的監(jiān)聽
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
//memoryUsage的getter
- (UInt64)memoryUsage {
    __block UInt64 result = 0;
    dispatch_sync(self.synchronizationQueue, ^{
        result = self.currentMemoryUsage;
    });
    return result;
}

//添加圖片到緩存中
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
    //使用dispatch_barrier_async不阻塞當(dāng)前線程娃磺,不阻塞向隊(duì)列中添加任務(wù)
    //但隊(duì)列中其他任務(wù)要執(zhí)行就必須等待前一個(gè)任務(wù)結(jié)束,不管是不是并發(fā)隊(duì)列
    dispatch_barrier_async(self.synchronizationQueue, ^{
        //創(chuàng)建AFCachedImage對象
        AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];
        //判斷對應(yīng)id是否已經(jīng)保存在緩存字典中了
        AFCachedImage *previousCachedImage = self.cachedImages[identifier];
        //如果已經(jīng)保存了減去占用的內(nèi)存大小
        if (previousCachedImage != nil) {
            self.currentMemoryUsage -= previousCachedImage.totalBytes;
        }
        //更新字典叫倍,更新占用內(nèi)存大小
        self.cachedImages[identifier] = cacheImage;
        self.currentMemoryUsage += cacheImage.totalBytes;
    });
    //同上偷卧,該block必須等待前一個(gè)block執(zhí)行完成才可以執(zhí)行
    dispatch_barrier_async(self.synchronizationQueue, ^{
        //判斷當(dāng)前占用內(nèi)存大小是否超過了設(shè)置的內(nèi)存緩存總大小
        if (self.currentMemoryUsage > self.memoryCapacity) {
            //計(jì)算需要釋放多少空間
            UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
            //把緩存字典中的所有緩存對象取出
            NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
           //設(shè)置排序描述器豺瘤,按照上次訪問時(shí)間排序
            NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
                                                                           ascending:YES];
            //排序取出的所有緩存對象
            [sortedImages sortUsingDescriptors:@[sortDescriptor]];

            UInt64 bytesPurged = 0;
            //遍歷,釋放緩存對象涯冠,滿足要求后break
            for (AFCachedImage *cachedImage in sortedImages) {
                [self.cachedImages removeObjectForKey:cachedImage.identifier];
                bytesPurged += cachedImage.totalBytes;
                if (bytesPurged >= bytesToPurge) {
                    break ;
                }
            }
            //更新當(dāng)前占用緩存大小
            self.currentMemoryUsage -= bytesPurged;
        }
    });
}

//刪除圖片
- (BOOL)removeImageWithIdentifier:(NSString *)identifier {
    __block BOOL removed = NO;
    //同步方法
    dispatch_barrier_sync(self.synchronizationQueue, ^{
        AFCachedImage *cachedImage = self.cachedImages[identifier];
        if (cachedImage != nil) {
            [self.cachedImages removeObjectForKey:identifier];
            self.currentMemoryUsage -= cachedImage.totalBytes;
            removed = YES;
        }
    });
    return removed;
}

//刪除所有圖片
- (BOOL)removeAllImages {
    __block BOOL removed = NO;
    //同步方法
    dispatch_barrier_sync(self.synchronizationQueue, ^{
        if (self.cachedImages.count > 0) {
            [self.cachedImages removeAllObjects];
            self.currentMemoryUsage = 0;
            removed = YES;
        }
    });
    return removed;
}

//根據(jù)id獲取圖片
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
    __block UIImage *image = nil;
    dispatch_sync(self.synchronizationQueue, ^{
        AFCachedImage *cachedImage = self.cachedImages[identifier];
        //更新訪問時(shí)間
        image = [cachedImage accessImage];
    });
    return image;
}
//AFImageRequestCache協(xié)議的方法炉奴,通過request構(gòu)造一個(gè)key然后調(diào)用前面的方法
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
    [self addImage:image withIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
//同上
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
    return [self removeImageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
//同上
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
    return [self imageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
//通過request和額外的id構(gòu)造一個(gè)key
- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier {
    //取圖片的URI然后追加額外的id構(gòu)造key
    NSString *key = request.URL.absoluteString;
    if (additionalIdentifier != nil) {
        key = [key stringByAppendingString:additionalIdentifier];
    }
    return key;
}

@end

AFAutoPurgingImageCache的實(shí)現(xiàn)很簡單,邏輯也都很簡單蛇更,不再贅述了瞻赶。它的淘汰算法采用的是LRU,從源碼中其實(shí)也可以看出缺點(diǎn)挺多的派任,比如上次訪問時(shí)間使用NSDate類砸逊,使用UNIX時(shí)間戳應(yīng)該更好,不僅內(nèi)存占用小排序也更快吧掌逛。淘汰緩存時(shí)需要從緩存字典中取出所有的緩存對象然后根據(jù)NSDate排序师逸,如果有大量緩存圖片,這里似乎就是一個(gè)性能瓶頸豆混,但它的優(yōu)點(diǎn)就是實(shí)現(xiàn)簡單明了篓像,對于性能要求不高的程序選擇這個(gè)也沒有太多影響。

YYCache的內(nèi)存緩存和磁盤緩存

本節(jié)文章將講解YYCache的內(nèi)存緩存YYMemoryCache和磁盤緩存YYDiskCache皿伺,但源碼較多员辩,而且本文的篇幅有限,所以不再和之前的文章一樣貼所有的源碼來講解鸵鸥,這里只會(huì)貼一些比較重要的代碼進(jìn)行講解奠滑,需要深入研究的讀者還是要自己看一下完整的源碼。

YYMemoryCache講解

貼一下頭文件的接口代碼:

@interface YYMemoryCache : NSObject

//內(nèi)存
@property (nullable, copy) NSString *name;
//當(dāng)前緩存對象的個(gè)數(shù)
@property (readonly) NSUInteger totalCount;
//當(dāng)前緩存對象的總cost數(shù)
@property (readonly) NSUInteger totalCost;

//同NSCache妒穴,支持緩存多少個(gè)對象
@property NSUInteger countLimit;
//同NSCache宋税,每個(gè)緩存對象可以設(shè)置一個(gè)cost,這個(gè)值就標(biāo)識支持緩存的最大cost總量
@property NSUInteger costLimit;
//緩存的過期時(shí)間讼油,單位是秒
@property NSTimeInterval ageLimit;

//自動(dòng)清理緩存的時(shí)間間隔杰赛,默認(rèn)是5秒
@property NSTimeInterval autoTrimInterval;

//是否在程序進(jìn)入后臺后刪除所有的緩存對象
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;

//收到系統(tǒng)內(nèi)存警告后執(zhí)行的回調(diào)塊
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);

//程序進(jìn)入后臺后執(zhí)行的回調(diào)塊
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);

//是否在主線程釋放對象,如果有UIView這樣的UI對象需要在主線程中釋放
@property BOOL releaseOnMainThread;

//是否異步釋放汁讼,默認(rèn)是YES
@property BOOL releaseAsynchronously;

//key的緩存對象是否存在
- (BOOL)containsObjectForKey:(id)key;

//根據(jù)key獲取緩存對象
- (nullable id)objectForKey:(id)key;

//設(shè)置key的緩存對象
- (void)setObject:(nullable id)object forKey:(id)key;

//設(shè)置key的緩存對象并設(shè)置其cost值
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;

//刪除key的緩存對象
- (void)removeObjectForKey:(id)key;

//刪除所有緩存對象
- (void)removeAllObjects;

//清除緩存到緩存對象只有count個(gè)
- (void)trimToCount:(NSUInteger)count;

//清除緩存到cost上限為cost
- (void)trimToCost:(NSUInteger)cost;

//清除超過age的過期緩存對象
- (void)trimToAge:(NSTimeInterval)age;

@end

YYMemeoryCache提供的接口和NSCache類似淆攻,提供緩存對象個(gè)數(shù)限制、緩存對象占用cost限制和緩存對象過期限制嘿架,實(shí)現(xiàn)緩存對象的清理。YYMemeoryCacheAFAutoPurgingImageCache不同啸箫,他使用鏈表來管理緩存對象耸彪,同樣也使用LRU淘汰算法來清除緩存對象。

首先看一下鏈表節(jié)點(diǎn)定義:

//這是一個(gè)雙向鏈表忘苛,保存prev和next指針
@interface _YYLinkedMapNode : NSObject {
    @package
    //__unsafe_unretained標(biāo)識不保留其他節(jié)點(diǎn)對象蝉娜,提高效率
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    //記錄key唱较、value、cost和訪問時(shí)間召川,使用unix時(shí)間戳
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}
@end

@implementation _YYLinkedMapNode
@end

上面的代碼就是雙向鏈表的節(jié)點(diǎn)南缓,其實(shí)和AFAutoPurgingImageCache封裝的緩存對象一樣,不過YYMemeoryCache支持任意類型的緩存對象而不限于是圖片荧呐,YYMemeoryCache性能和效率非常高汉形,通過上面的定義也可以看出,能夠提高效率的地方一定會(huì)用最好的方式實(shí)現(xiàn)倍阐,由于是YYMemeoryCache管理緩存對象的節(jié)點(diǎn)概疆,所以能夠保證在生命周期內(nèi)不會(huì)被釋放,所以使用__unsafe_unretained修飾峰搪,不會(huì)產(chǎn)生任何問題岔冀。

再看一下鏈表的定義:

@interface _YYLinkedMap : NSObject {
    @package
    //使用Core Foundation的CFMutableDictionaryRef可變字典保存上面的節(jié)點(diǎn)對象
    CFMutableDictionaryRef _dic; // do not set object directly
    //記錄總cost
    NSUInteger _totalCost;
    //記錄總count
    NSUInteger _totalCount;
    //記錄雙向鏈表的head指針
    _YYLinkedMapNode *_head; // MRU, do not change it directly
    //雙向鏈表的tail指針
    _YYLinkedMapNode *_tail; // LRU, do not change it directly
    //是否在主線程中釋放緩存對象
    BOOL _releaseOnMainThread;
    //是否異步釋放緩存對象
    BOOL _releaseAsynchronously;
}

//雙向鏈表在表頭插入節(jié)點(diǎn)
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

//將節(jié)點(diǎn)移到表頭,LRU算法概耻,訪問一個(gè)緩存對象后就將這個(gè)對象移動(dòng)到表頭
//不需要向AFNetworking一樣記錄NSDate然后更新排序
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

//刪除一個(gè)節(jié)點(diǎn)
- (void)removeNode:(_YYLinkedMapNode *)node;

//刪除尾節(jié)點(diǎn)
- (_YYLinkedMapNode *)removeTailNode;

//刪除所有的節(jié)點(diǎn)
- (void)removeAll;

@end

上面是鏈表的定義使套,它使用Core FoundationCFMutableDictionaryRef來保存節(jié)點(diǎn)對象,并維護(hù)一個(gè)雙向鏈表鞠柄,記錄了一些需要使用的參數(shù)侦高。也就是說YYMemeoryCache會(huì)通過構(gòu)造一個(gè)_YYLinkedMapNode對象來封裝需要緩存的對象,然后使用_YYLinkedMap對象來維護(hù)和管理這個(gè)雙向鏈表春锋,由于篇幅問題不列舉所有的實(shí)現(xiàn)方法了矫膨,這些方法都是常規(guī)的鏈表操作,讀者可自行閱讀期奔,舉一個(gè)源碼:

//將一個(gè)節(jié)點(diǎn)移動(dòng)到表頭侧馅,由于使用LRU淘汰算法
//所以當(dāng)我們訪問一個(gè)緩存對象時(shí)就會(huì)調(diào)用這個(gè)方法將封裝緩存對象的節(jié)點(diǎn)移動(dòng)到表頭
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
    //判斷節(jié)點(diǎn)是頭節(jié)點(diǎn)、尾節(jié)點(diǎn)或普通節(jié)點(diǎn)呐萌,然后修改指針指向
    if (_head == node) return;
    
    if (_tail == node) {
        _tail = node->_prev;
        _tail->_next = nil;
    } else {
        node->_next->_prev = node->_prev;
        node->_prev->_next = node->_next;
    }
    node->_next = _head;
    node->_prev = nil;
    _head->_prev = node;
    _head = node;
}

其他方法就不再贅述了馁痴,講到這里再結(jié)合之前AFNetworking的緩存源碼,相信大家也可以自己寫出添加緩存肺孤、刪除緩存的代碼了罗晕。接下來看一下YYMemeoryCache如何添加緩存對象的:

//添加緩存對象
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    //key為空直接返回
    if (!key) return;
    //要緩存的對象object不存在就刪除對應(yīng)key的已經(jīng)緩存的對象
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    //使用pthread_mutext互斥鎖,防止產(chǎn)生競爭條件保障線程安全
    pthread_mutex_lock(&_lock);
    //首先從存儲緩存對象的字典中獲取緩存節(jié)點(diǎn)
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    //獲取一個(gè)時(shí)間
    NSTimeInterval now = CACurrentMediaTime();
    //如果從緩存字典中找到了節(jié)點(diǎn)
    if (node) {
        //更新節(jié)點(diǎn)的信息
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        //將節(jié)點(diǎn)移動(dòng)至表頭
        [_lru bringNodeToHead:node];
    } else {
        //如果節(jié)點(diǎn)不存在赠堵,就構(gòu)造一個(gè)
        node = [_YYLinkedMapNode new];
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;
        //將新創(chuàng)建的節(jié)點(diǎn)插在表頭
        [_lru insertNodeAtHead:node];
    }
    //如果鏈表的總cost值大于限制值就清理
    if (_lru->_totalCost > _costLimit) {
        //異步在串行隊(duì)列中執(zhí)行清除操作
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }
    /*
    如果鏈表緩存節(jié)點(diǎn)總數(shù)大于限制值就清理一個(gè)節(jié)點(diǎn)
    由于每次添加緩存對象的時(shí)候都會(huì)判斷是否超出總數(shù)小渊,所以只需要?jiǎng)h除一個(gè)節(jié)點(diǎn)就能保持總數(shù)小于等于限制值
    */
    if (_lru->_totalCount > _countLimit) {
        //獲取要?jiǎng)h除的尾節(jié)點(diǎn)
        _YYLinkedMapNode *node = [_lru removeTailNode];
        //根據(jù)配置值獲取隊(duì)列
        if (_lru->_releaseAsynchronously) {
            dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
            dispatch_async(queue, ^{
                /*
                這里是一個(gè)小技巧,block捕獲并持有node
                由于是異步方法茫叭,當(dāng)前代碼塊會(huì)立刻結(jié)束酬屉,所以局部變量node會(huì)被釋放,緩存節(jié)點(diǎn)的引用計(jì)數(shù)只有當(dāng)前block有
                此時(shí)block執(zhí)行完成以后就會(huì)由這個(gè)線程負(fù)責(zé)釋放節(jié)點(diǎn)的內(nèi)存,就實(shí)現(xiàn)了其他線程異步釋放內(nèi)存的操作
                */
                [node class]; //hold and release in queue
            });
        } else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [node class]; //hold and release in queue
            });
        }
    }
    //釋放鎖
    pthread_mutex_unlock(&_lock);
}

YYMemoryCache對于線程安全沒有使用GCD串行隊(duì)列來實(shí)現(xiàn)呐萨,在第一個(gè)版本中使用的是性能最好的自旋鎖杀饵,由于自旋鎖存在一些bug,所以后來的版本改成了互斥鎖谬擦,pthread_mutext上鎖和釋放鎖的效率也很高切距,具體可以參考YYCache作者寫的文章不再安全的 OSSpinLock,使用GCD隊(duì)列來實(shí)現(xiàn)線程安全防止產(chǎn)生競爭條件很簡單惨远,但是效率沒有互斥鎖高谜悟,所以作者選擇了使用互斥鎖。

上面的代碼是向YYMemeoryCache中添加緩存對象锨络,代碼也很簡單赌躺,就是簡單的鏈表操作,比較值得注意的就是在其他線程異步釋放對象羡儿,有可能我們會(huì)覺得釋放對象并不需要消耗太多的資源礼患,但是累積起來也會(huì)產(chǎn)生一定的消耗了。

在看一下根據(jù)key獲取緩存對象的方法:

- (id)objectForKey:(id)key {
    if (!key) return nil;
    //加互斥鎖
    pthread_mutex_lock(&_lock);
    //從字典中獲取key對應(yīng)的緩存對象節(jié)點(diǎn)
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    //如果存在就更新訪問時(shí)間并將節(jié)點(diǎn)移至表頭
    if (node) {
        node->_time = CACurrentMediaTime();
        [_lru bringNodeToHead:node];
    }
    pthread_mutex_unlock(&_lock);
    return node ? node->_value : nil;
}

獲取緩存對象也很簡單掠归,接下來看一下根據(jù)限制清除緩存的操作:

/*
遞歸函數(shù)缅叠,一種實(shí)現(xiàn)定時(shí)器的技巧
使用GCD 每_autoTrimInterval秒(默認(rèn)5s)執(zhí)行一次_trimInBackground方法
遞歸調(diào)用就可以實(shí)現(xiàn)每_autoTrimInterval秒進(jìn)行一次緩存對象的清除操作
*/
- (void)_trimRecursively {
    __weak typeof(self) _self = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        [self _trimInBackground];
        [self _trimRecursively];
    });
}

//調(diào)用三種清除策略
- (void)_trimInBackground {
    dispatch_async(_queue, ^{
        [self _trimToCost:self->_costLimit];
        [self _trimToCount:self->_countLimit];
        [self _trimToAge:self->_ageLimit];
    });
}

//只看一個(gè)栗子
- (void)_trimToCost:(NSUInteger)costLimit {
    BOOL finish = NO;
    //加鎖,并判斷是否已經(jīng)滿足限制的要求
    pthread_mutex_lock(&_lock);
    if (costLimit == 0) {
        [_lru removeAll];
        finish = YES;
    } else if (_lru->_totalCost <= costLimit) {
        finish = YES;
    }
    //釋放所
    pthread_mutex_unlock(&_lock);
    //如果已經(jīng)滿足要求就直接返回虏冻,不需要做清除操作
    if (finish) return;
    //定義一個(gè)數(shù)組用于存儲要?jiǎng)h除的緩存節(jié)點(diǎn)對象
    NSMutableArray *holder = [NSMutableArray new];
    //循環(huán)
    while (!finish) {
        //trylock會(huì)嘗試上鎖肤粱,上鎖成功返回0,如果已經(jīng)被占用返回一個(gè)非零值
        if (pthread_mutex_trylock(&_lock) == 0) {
                //判斷是否滿足限制要求
            if (_lru->_totalCost > costLimit) {
                //刪除尾節(jié)點(diǎn)厨相,加入到要?jiǎng)h除的數(shù)組中
                _YYLinkedMapNode *node = [_lru removeTailNode];
                if (node) [holder addObject:node];
            } else {
                finish = YES;
            }
            pthread_mutex_unlock(&_lock);
        } else {
            //如果沒有上鎖成功就睡10ms重試
            usleep(10 * 1000); //10 ms
        }
    }
    //如果要?jiǎng)h除的數(shù)組個(gè)數(shù)大于0领曼,就根據(jù)配置異步或主線程中釋放對象
    //這里的釋放技巧和之前講解的一致
    if (holder.count) {
        dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
        dispatch_async(queue, ^{
            [holder count]; // release in queue
        });
    }
}

上面的代碼也很簡單,使用GCD和遞歸函數(shù)實(shí)現(xiàn)了一個(gè)簡單的計(jì)時(shí)器蛮穿,每5ms執(zhí)行一次清理操作庶骄,代碼很簡單不再贅述了。

到現(xiàn)在為止践磅,我們已經(jīng)熟悉了AFNetworking內(nèi)存緩存单刁、SDWebImage內(nèi)存緩存和YYCache的內(nèi)存緩存的實(shí)現(xiàn),SDWebImage內(nèi)存緩存其實(shí)就直接使用了NSCache府适。

基于內(nèi)存的緩存可以使用NSCacheNSMutableDictionary來實(shí)現(xiàn)羔飞,但使用NSCache其清除緩存的算法不是我們可控的,比如我們想要LRU淘汰算法檐春,或者FILO逻淌、FIFO等各種算法都沒辦法實(shí)現(xiàn),此時(shí)只能自己實(shí)現(xiàn)疟暖,并且NSCache緩存的讀寫效率并不高恍风,他幫我們做的只有自動(dòng)清理緩存,所以在性能要求不高的情況下使用NSCache很合適誓篱,其實(shí)現(xiàn)簡單朋贬,已經(jīng)幫我們完成了所有的工作,我們只需要像操作字典一樣操作他窜骄。

如果要實(shí)現(xiàn)自定義的淘汰算法锦募,就需要自定義實(shí)現(xiàn),如AFNetworking使用NSMutableDictionary實(shí)現(xiàn)LRU淘汰算法邻遏,其實(shí)通過對比YYCacheAFNetworkign不難發(fā)現(xiàn)糠亩,AFAutoPurgingImageCache的性能瓶頸有很多,值得提升的地方也有很多准验,YYMemeoryCache為了追求極致的性能很多地方都是直接使用C函數(shù)赎线,直接使用Core FoundationCFMutableDictionaryRef可變字典,包括釋放對象這樣不怎么消耗資源的操作都盡量放在其他線程執(zhí)行糊饱。所以垂寥,對于高性能需求的場景就需要我們深思實(shí)現(xiàn)方式。

YYDiskCache

YYCache的磁盤緩存YYDiskCache的實(shí)現(xiàn)相比就復(fù)雜一些了另锋,作者在經(jīng)過大量調(diào)研和實(shí)驗(yàn)后發(fā)現(xiàn)滞项,SQLite對于數(shù)據(jù)的寫入性能高于直接寫文件,但是對于讀性能來說需要考慮數(shù)據(jù)的大小夭坪,對于20KB以上的數(shù)據(jù)讀文件的性能要高于讀數(shù)據(jù)庫的性能文判,所以,為了實(shí)現(xiàn)高性能的磁盤緩存室梅,作者結(jié)合了SQLite和文件系統(tǒng)戏仓,將緩存數(shù)據(jù)的元數(shù)據(jù)保存在數(shù)據(jù)庫中,對于大于20KB的數(shù)據(jù)存入文件系統(tǒng)中亡鼠,讀取時(shí)直接從文件系統(tǒng)中讀取赏殃。到這里,需求基本都說完了拆宛,如果讓讀者自行實(shí)現(xiàn)相信你也能寫出一個(gè)磁盤緩存嗓奢。

因?yàn)榻Y(jié)合了SQLite和文件系統(tǒng),作者實(shí)現(xiàn)了YYKVStorage類封裝了數(shù)據(jù)庫和文件的操作浑厚,YYDiskCache類似于表現(xiàn)層股耽,進(jìn)一步封裝并提供便于使用的接口,YYKVStorage不是線程安全的钳幅,所以保證線程安全的操作由YYDiskCache實(shí)現(xiàn)物蝙。

看一下YYKVStorage的頭文件聲明代碼中比較重要的部分:

//緩存對象model
@interface YYKVStorageItem : NSObject

//唯一id key
@property (nonatomic, strong) NSString *key;                ///< key
//value
@property (nonatomic, strong) NSData *value;                ///< value
//保存在文件系統(tǒng)中的文件名,如果只保存在數(shù)據(jù)庫中則為nil
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
//value的字節(jié)大小
@property (nonatomic) int size;                             ///< value's size in bytes
//修改時(shí)間敢艰,unix時(shí)間戳
@property (nonatomic) int modTime;                          ///< modification unix timestamp
//訪問時(shí)間诬乞,unix時(shí)間戳
@property (nonatomic) int accessTime;                       ///< last access unix timestamp
//附加數(shù)據(jù),如果沒有為nil
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end

//存儲方式類型,key-value緩存對象的存儲位置
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    //只存在文件系統(tǒng)里
    YYKVStorageTypeFile = 0,
    //只存在數(shù)據(jù)庫里
    YYKVStorageTypeSQLite = 1,
    //文件系統(tǒng)和數(shù)據(jù)庫都存
    YYKVStorageTypeMixed = 2,
};

上面定義了一個(gè)封裝的緩存對象類YYKVStorageItem震嫉,還定義了三種緩存對象存儲方式森瘪,可以只使用文件系統(tǒng)、只使用數(shù)據(jù)庫或在兩者中都存儲票堵,用戶可以按需選擇扼睬,這個(gè)存儲類型的設(shè)置方式是在初始化構(gòu)造函數(shù)中指定的。

在看一下文件系統(tǒng)的目錄結(jié)構(gòu)和數(shù)據(jù)庫表結(jié)構(gòu):

 File:
 /path/
      /manifest.sqlite
      /manifest.sqlite-shm
      /manifest.sqlite-wal
      /data/
           /e10adc3949ba59abbe56e057f20f883e
           /e10adc3949ba59abbe56e057f20f883e
      /trash/
            /unused_file_or_folder
 
 SQL:
 create table if not exists manifest (
    key                 text,
    filename            text,
    size                integer,
    inline_data         blob,
    modification_time   integer,
    last_access_time    integer,
    extended_data       blob,
    primary key(key)
 ); 
 create index if not exists last_access_time_idx on manifest(last_access_time);

類似于SDWebImage的磁盤緩存悴势,文件名稱使用MD5散列key獲得窗宇,YYKVStorage類主要實(shí)現(xiàn)了文件和數(shù)據(jù)庫的增刪改查操作,由于篇幅問題這里就分別舉一個(gè)栗子:

//SQLite插入一條數(shù)據(jù)
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
    NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
    if (!stmt) return NO;
    
    int timestamp = (int)time(NULL);
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
    sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
    sqlite3_bind_int(stmt, 3, (int)value.length);
    if (fileName.length == 0) {
        sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
    } else {
        sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
    }
    sqlite3_bind_int(stmt, 5, timestamp);
    sqlite3_bind_int(stmt, 6, timestamp);
    sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);
    
    int result = sqlite3_step(stmt);
    if (result != SQLITE_DONE) {
        if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
        return NO;
    }
    return YES;
}

//向文件中寫數(shù)據(jù)
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
    if (key.length == 0 || value.length == 0) return NO;
    if (_type == YYKVStorageTypeFile && filename.length == 0) {
        return NO;
    }
    
    if (filename.length) {
        //將value數(shù)據(jù)寫入文件系統(tǒng)中
        if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
        //將數(shù)據(jù)插入到數(shù)據(jù)庫中
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            [self _fileDeleteWithName:filename];
            return NO;
        }
        return YES;
    } else {
        if (_type != YYKVStorageTypeSQLite) {
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}

//通過key獲取緩存對象
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
    if (key.length == 0) return nil;
    //首先查找數(shù)據(jù)庫特纤,不獲取
    YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
    if (item) {
        //更新訪問
        [self _dbUpdateAccessTimeWithKey:key];
        //如果文件路徑存在军俊,就去讀文件系統(tǒng)
        if (item.filename) {
            item.value = [self _fileReadWithName:item.filename];
            if (!item.value) {
                [self _dbDeleteItemWithKey:key];
                item = nil;
            }
        }
    }
    return item;
}

//清除緩存直到滿足大小要求
- (BOOL)removeItemsToFitSize:(int)maxSize {
    if (maxSize == INT_MAX) return YES;
    if (maxSize <= 0) return [self removeAllItems];
    
    int total = [self _dbGetTotalItemSize];
    if (total < 0) return NO;
    if (total <= maxSize) return YES;
    
    NSArray *items = nil;
    BOOL suc = NO;
    do {
        int perCount = 16;
        //數(shù)據(jù)庫查找并排序相關(guān)信息
        items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
        //遍歷刪除數(shù)據(jù)庫,如果文件系統(tǒng)中有對應(yīng)數(shù)據(jù)就刪除掉
        for (YYKVStorageItem *item in items) {
            if (total > maxSize) {
                if (item.filename) {
                    [self _fileDeleteWithName:item.filename];
                }
                suc = [self _dbDeleteItemWithKey:item.key];
                total -= item.size;
            } else {
                break;
            }
            if (!suc) break;
        }
    } while (total > maxSize && items.count > 0 && suc);
    if (suc) [self _dbCheckpoint];
    return suc;
}

上面的代碼就是操作數(shù)據(jù)庫和文件系統(tǒng)的代碼捧存,不再贅述了粪躬,不過,從寫文件的函數(shù)可以發(fā)現(xiàn)矗蕊,如果選擇保存在文件系統(tǒng)和數(shù)據(jù)庫中短蜕,那么value即會(huì)被寫入文件系統(tǒng)也會(huì)被存儲在操作系統(tǒng)中,關(guān)于YYKVStorage的代碼不再講解了傻咖,讀者可以自行查閱朋魔。

接下來看幾個(gè)YYDiskCache中比較重要的方法:

//YYDiskCache初始化構(gòu)造函數(shù)
- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
    self = [super init];
    if (!self) return nil;
    
    YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
    if (globalCache) return globalCache;
    
    /*
    根據(jù)傳入的threadhold判斷緩存對象存儲類型
    如果為0就所有數(shù)據(jù)存入文件中,如果為NSUIntegerMax存入數(shù)據(jù)庫中
    其他值就混合存儲
    */
    YYKVStorageType type;
    if (threshold == 0) {
        type = YYKVStorageTypeFile;
    } else if (threshold == NSUIntegerMax) {
        type = YYKVStorageTypeSQLite;
    } else {
        type = YYKVStorageTypeMixed;
    }
    
    //創(chuàng)建一個(gè)YYKVStorage對象
    YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
    if (!kv) return nil;
    
    _kv = kv;
    _path = path;
    _lock = dispatch_semaphore_create(1);
    _queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
    _inlineThreshold = threshold;
    _countLimit = NSUIntegerMax;
    _costLimit = NSUIntegerMax;
    _ageLimit = DBL_MAX;
    _freeDiskSpaceLimit = 0;
    _autoTrimInterval = 60;
    
    //開啟遞歸清除緩存
    [self _trimRecursively];
    _YYDiskCacheSetGlobal(self);
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
    return self;
}

上面的代碼是YYDiskCache的初始化構(gòu)造函數(shù)卿操,主要是確定存儲類型警检、創(chuàng)建YYKVStorage對象并創(chuàng)建數(shù)據(jù)庫表和文件目錄,開啟定時(shí)清除緩存操作害淤。

//宏定義扇雕,使用信號量充當(dāng)鎖的作用
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)

//YYDiskCache獲取緩存對象
- (id<NSCoding>)objectForKey:(NSString *)key {
    if (!key) return nil;
    //加鎖,通過YYKVStorage獲取key對應(yīng)的數(shù)據(jù)
    Lock();
    YYKVStorageItem *item = [_kv getItemForKey:key];
    Unlock();
    if (!item.value) return nil;
    
    id object = nil;
    //如果有自定義的unarchiveBlock就執(zhí)行反序列化操作
    if (_customUnarchiveBlock) {
        object = _customUnarchiveBlock(item.value);
    } else {
        @try {
            object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
        }
        @catch (NSException *exception) {
            // nothing to do...
        }
    }
    if (object && item.extendedData) {
        [YYDiskCache setExtendedData:item.extendedData toObject:object];
    }
    return object;
}

//遞歸定時(shí)清除緩存
- (void)_trimRecursively {
    __weak typeof(self) _self = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        [self _trimInBackground];
        [self _trimRecursively];
    });
}

//調(diào)用YYKVStorage的方法刪除緩存直到滿足要求
- (void)_trimToCost:(NSUInteger)costLimit {
    if (costLimit >= INT_MAX) return;
    [_kv removeItemsToFitSize:(int)costLimit];
    
}

上面就是YYDiskCache如何通過YYKVStorage存儲緩存對象到數(shù)據(jù)庫和文件系統(tǒng)的大致操作窥摄。其他源碼篇幅問題就不再講述了镶奉,讀者可自行閱讀。

SDWebImageYYCache的磁盤緩存最大的區(qū)別就是應(yīng)用場景崭放,SDWebImage存儲的都是圖片哨苛,圖片一般都比較大,所以直接采用文件系統(tǒng)能夠保證讀性能币砂,YYCache作為第三方庫建峭,需要緩存任意類型的對象,所以提供了數(shù)據(jù)庫和文件系統(tǒng)結(jié)合的方式實(shí)現(xiàn)决摧,所以具體選擇什么樣的緩存策略需要考慮具體的應(yīng)用場景亿蒸。

備注

由于作者水平有限凑兰,難免出現(xiàn)紕漏,如有問題還請不吝賜教边锁。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末姑食,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子砚蓬,更是在濱河造成了極大的恐慌矢门,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灰蛙,死亡現(xiàn)場離奇詭異,居然都是意外死亡隔躲,警方通過查閱死者的電腦和手機(jī)摩梧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宣旱,“玉大人仅父,你說我怎么就攤上這事』胍鳎” “怎么了笙纤?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長组力。 經(jīng)常有香客問我省容,道長,這世上最難降的妖魔是什么燎字? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任腥椒,我火速辦了婚禮,結(jié)果婚禮上候衍,老公的妹妹穿的比我還像新娘笼蛛。我一直安慰自己,他們只是感情好蛉鹿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布滨砍。 她就那樣靜靜地躺著,像睡著了一般妖异。 火紅的嫁衣襯著肌膚如雪惋戏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天随闺,我揣著相機(jī)與錄音日川,去河邊找鬼。 笑死矩乐,一個(gè)胖子當(dāng)著我的面吹牛龄句,可吹牛的內(nèi)容都是我干的回论。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼分歇,長吁一口氣:“原來是場噩夢啊……” “哼傀蓉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起职抡,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤葬燎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后缚甩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谱净,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年擅威,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了壕探。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,722評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡郊丛,死狀恐怖李请,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情厉熟,我是刑警寧澤导盅,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站揍瑟,受9級特大地震影響白翻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜月培,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一嘁字、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧杉畜,春花似錦纪蜒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至灭袁,卻和暖如春猬错,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背茸歧。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工倦炒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人软瞎。 一個(gè)月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓逢唤,卻偏偏與公主長得像拉讯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子鳖藕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容