SDWebImage源码详解 - 缓存

SDWebImage源码详解 - 缓存

缓存的实现可以显著的减少网络流量的消耗,先将下载的图片缓存到本地,下次获取同一张图片的时候,可以直接在本地缓存中获取,而不用访问服务器重新获取图片,这样不仅可以减少网络流量的消耗,并且提升了用户体验(图片加载速度快)。SDWebImage的缓存由SDImageCache类来实现,这是一个单例类,该类负责处理内存缓存及一个可选的磁盘缓存,其中磁盘缓存的写操作是异步的,这样就不会对UI操作造成影响。此外还提供了若干属性和接口来配置和操作缓存对象。
先来看看SDImageCache的头文件内容

//定义三个枚举常量,以控制缓存的存储选项
typedef NS_ENUM(NSInteger, SDImageCacheType) {
    //不使用缓存策略,从网络下载
    SDImageCacheTypeNone,
    //从磁盘中缓存中获取图片
    SDImageCacheTypeDisk,
    //从内存中获取图片
    SDImageCacheTypeMemory
};

//回调函数类型变量
typedef void(^SDWebImageQueryCompletedBlock)(UIImage *image, SDImageCacheType cacheType);

typedef void(^SDWebImageCheckCacheCompletionBlock)(BOOL isInCache);

typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);

@interface SDImageCache : NSObject
//是否在缓存之前解压图片,此项操作可以提升性能,但是会消耗较多的内存,默认是YES。注意:如果内存不足,可以置为NO
@property (assign, nonatomic) BOOL shouldDecompressImages;

//是否禁止iCloud备份,默认是YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;

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

//内存最大容量
@property (assign, nonatomic) NSUInteger maxMemoryCost;

//内存对象的最大数目
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;

//磁盘缓存保留的最长时间
@property (assign, nonatomic) NSInteger maxCacheAge;

//磁盘缓存最大容量,以字节为单位
@property (assign, nonatomic) NSUInteger maxCacheSize;

//返回缓存对象的单例
+ (SDImageCache *)sharedImageCache;

//以ns为缓存空间名字初始化缓存
- (id)initWithNamespace:(NSString *)ns;

//在directory目录下,以ns为缓存空间名字初始化缓存
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;

//返回磁盘缓存空间的路径
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace;

//添加只读内存空间路径,一般用在图片已经下载置相应的缓存目录
- (void)addReadOnlyCachePath:(NSString *)path;

//以key为键值将图片image存储置缓存中
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;

//以key为键值将图片image存储置缓存中,toDisk控制是否写入磁盘缓存
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;

//以key为键值将图片image存储置缓存中,toDisk控制是否写入磁盘缓存,此外如果recalculate为YES或imageData有数据,则将imageData存储置磁盘缓存中
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;

//在内存或磁盘缓存中以key为键值查找图片缓存,如果找到则执行doneBlock回调
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;

//在内存缓存中查找图片缓存,并返回图片对象
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;

//在硬盘缓存中查找图片缓存,并返回图片对象
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;

//在内存或硬盘缓存中删除指定key缓存
- (void)removeImageForKey:(NSString *)key;

//在内存或硬盘缓存中删除指定key缓存,完成后执行响应回调
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;

//在内存或硬盘缓存中删除指定key缓存,fromDisk控制是否删除磁盘缓存对象
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
//在内存或硬盘缓存中删除指定key缓存,完成后执行响应回调,fromDisk控制是否删除磁盘缓存对象
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;

//清除内存缓存
- (void)clearMemory;

//清除磁盘缓存,完成后执行回调
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk;

//清除过期缓存,如果缓存容量超过限制,则清除部分缓存直至达到预期目标为止
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
- (void)cleanDisk;

//返回磁盘缓存的大小
- (NSUInteger)getSize;
//返回磁盘缓存对象的数目
- (NSUInteger)getDiskCount;

//异步计算磁盘缓存所需大小
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;

//异步查看磁盘缓存中是否存在指定key的图片,完成后执行回调
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
- (BOOL)diskImageExistsWithKey:(NSString *)key;

//返货指定路径path下的key对象的缓存路径
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;

//返回默认路径下key对象的缓存路径
- (NSString *)defaultCachePathForKey:(NSString *)key; 

从头文件可以看出,SDWebImage的缓存对象提供了几个属性(缓存时间,缓存大小限制等)和若干函数来对缓存对象进行操作(获取、移除及清空图片)。对于这么多的函数,有些其实仅仅是调用而已,只需关注几个主要函数即可,稍后我们将会针对几个主要函数进行讲解。
</br>
SDWebImage缓存的主要实现分别采用了内存缓存和磁盘缓存,内存缓存使用NSCash对象来实现,NSCache是一个类似于集合的容器。它存储key-value对,这一点类似于NSDictionary类,NSCache类的详细用法,这里不过多介绍,以后有机会专门介绍。磁盘缓存则使用NSFileManager对象来实现。图片存储的位置是位于app的Cache文件夹下。另外,SDImageCache还定义了一个串行队列,来异步存储图片。接下我们就代码的执行流程来详细的看一下代码的实现:

初始化缓存空间
//获取内存对象的单例
+ (SDImageCache *)sharedImageCache {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}   

ImageCache单例对象由函数new来初始换,而new函数默认调用init函数。

- (id)init {
    return [self initWithNamespace:@"default"];
}

- (id)initWithNamespace:(NSString *)ns {
    //获取磁盘缓存的路径
    NSString *path = [self makeDiskCachePath:ns];
    return [self initWithNamespace:ns diskCacheDirectory:path];
}

- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
    if ((self = [super init])) {
        NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];

        //初始化PNG图片的签名数据
        kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];

        // 创建IO 串行对垒
        _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

        // 初始化最大缓存时间
        _maxCacheAge = kDefaultCacheMaxCacheAge;

        // 初始化内存缓存
        _memCache = [[AutoPurgeCache alloc] init];
        _memCache.name = fullNamespace;

        // 保存磁盘缓存的目录路径
        if (directory != nil) {
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
        } else {
            NSString *path = [self makeDiskCachePath:ns];
            _diskCachePath = path;
        }

        // 设置默认属性
        _shouldDecompressImages = YES;
        _shouldCacheImagesInMemory = YES;
        _shouldDisableiCloud = YES;

        dispatch_sync(_ioQueue, ^{
            _fileManager = [NSFileManager new];
        });
    
#if TARGET_OS_IPHONE
        // 注册系统通知事件
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(clearMemory)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(cleanDisk)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundCleanDisk)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
#endif
    }

    return self;
}

通过代码可以看出,ImageCache对象的初始化工作,分别创建了内存缓存空间和磁盘缓存空间,这里面有一个函数-(NSString *)makeDiskCachePath:(NSString*)fullNamespace木有出现,这个函数的主要作用就是返回app的缓存目录。

-(NSString *)makeDiskCachePath:(NSString*)fullNamespace{
    //获取app的缓存文件夹
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    //返回缓存文件夹下以fullNamespace命名的路径
    return [paths[0] stringByAppendingPathComponent:fullNamespace];
}

保存图片

虽然ImageCache对外提供了许多保存图片置缓存的函数,但是这么多函数都调用一个基础函数- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;,具体实现如下:

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }
    // 如果保存置内存缓存属性为YES,则将图片保留在内存缓存中
    if (self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    //如果需要保存在磁盘缓存中,则将写人磁盘缓存的队列放入创建的串行队列ioQueue中
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{
            NSData *data = imageData;
            
            //如果recalculate为YES或者data数据为空,但是image有数据,则对iamge图片做处理
            //如果recalculate为YES并且data数据非空,则直接对data数据进行保存
            if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
                 // 需要确定图片是PNG还是JPEG。PNG图片容易检测,因为有一个唯一签名。
                         // PNG图像的前8个字节总是包含以下值:137 80 78 71 13 10 26 10
                // 在imageData为nil的情况下假定图像为PNG。我们将其当作PNG以避免丢失透明度。
                        //而当有图片数据时,我们检测其前缀,确定图片的类型
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                BOOL imageIsPng = hasAlpha;

                // But if we have an image data, we will look at the preffix
                if ([imageData length] >= [kPNGSignatureData length]) {
                    imageIsPng = ImageDataHasPNGPreffix(imageData);
                }

                if (imageIsPng) {
                    data = UIImagePNGRepresentation(image);
                }
                else {
                    data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                }
#else
                data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
            }
            //创建缓存文件并存储图片
            if (data) {
                //创建保留缓存文件的上层目录
                if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                }

                //以图片的URL做MD5转换后的文件名创建缓存文件
                NSString *cachePathForKey = [self defaultCachePathForKey:key];
                NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
                [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];

                // 是否启用iCloud云备份
                if (self.shouldDisableiCloud) {
                    [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
                }
            }
        });
    }
}

查询图片

ImageCache对外提供了三个查询缓存图片的接口函数


//在内存和磁盘缓存中查找key指定的图片
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;

//在内存缓存中查找key指定的图片
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;

//先在内存缓存中查找,然后在磁盘缓存中查找key指定的图片
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;

这里看一下第一个函数的实现,其他类似

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }

    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // 先在内存缓存中查找
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }
    //如果内存缓存中没有找到,则去磁盘缓存中去查找- (UIImage *)diskImageForKey:(NSString *)key
    //在磁盘缓存中找到后,同时更新置内存缓存中
    //有回调则调用doneBlock回调
    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

移除图片

ImageCache对外提供了四个删除缓存图片的函数,

- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;

移除函数比较简单,也有一个基础函数- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;,这个函数比较简单,删除内存缓存,删除磁盘下的缓存文件,看一看代码就明白什么意思,这里就不过多说明

清理缓存

清理缓存图片的清理操作有内存清理和磁盘缓存清理,而磁盘缓存又可以分为完全清空和部分清理。完全清空操作是直接把缓存的文件夹移除,清空操作有以下三个方法:

- (void)clearMemory;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk

这三个函数比较简单,也不过多介绍
接下来我们详细介绍一下部分清理,部分清空针对磁盘缓存,根据我们设定的一些参数值来移除一些文件,这里主要有两个指标:文件的缓存有效期及最大缓存空间大小。文件的缓存有效期可以通过maxCacheAge属性来设置,默认是1周的时间。如果文件的缓存时间超过这个时间值,则将其移除。而最大缓存空间大小是通过maxCacheSize属性来设置的,如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序排序,循环移除那些较早的文件,直到磁盘缓存的实际大小小于或等于我们设置的空间预设目标,这里设为最大缓存大小的一半。清理的操作在-cleanDiskWithCompletionBlock:方法中,其实现如下:

- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        // 该枚举器预先获取缓存文件的有用的属性,文件夹,修改时间,文件大小
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;

        // 枚举缓存文件夹中所有文件,
            //该迭代有两个目的:移除比过期日期更老的文件;存储文件属性以备后面执行基于缓存大小的清理操作
        NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];

            // 跳过文件夹
            if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // 将需要删除的文件,加入需要删除的数组urlsToDelete中
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }

            //存储有效期内的文件大小,留作备用
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
            [cacheFiles setObject:resourceValues forKey:fileURL];
        }
        
        //删除过期缓存
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        // 如果磁盘缓存的大小大于我们配置的最大大小,则执行基于文件大小的清理,首先删除最老的文件
        if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
            // 以设置的最大缓存大小的一半作为清理目标
            const NSUInteger desiredCacheSize = self.maxCacheSize / 2;

            // 按照最后修改时间来排序剩下的缓存文件
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }];

            // 循环删除文件,直到缓存总大小降到我们期望的大小
            for (NSURL *fileURL in sortedFiles) {
                if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        //有回调则执行回调
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

到这里缓存的实现就讲解的差不多了,这里我们主要分析了SDWebImage的SDImageCache缓存类的相关操作,着重介绍了几个主要的操作,另外SDImageCache还提供了一些其他的辅助方法如获取缓存大小、缓存中图片的数量、判断缓存中是否存在某个key指定的图片,具体的实现可以参照源码,实现都不怎么复杂。
</br>
下一节我们主要介绍一下异步下载器的实现。

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

推荐阅读更多精彩内容