【转】SDWebImage源码阅读(二)

1. 解决上一篇遗留的坑


上一篇中对sd_setImageWithURL函数简单分析了一下,还留了一些坑。不过因为我们现在对这个函数有一个大概框架了,我们就按顺序一个个来解决。

首先是这一句代码:

objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

就是给UIImageView当前这个对象添加一个NSString的关联对象url。相当于现在这个图片的url属性绑定到了UIImageView对象上。如果对这个函数有疑问,请移步我的这篇博客

下面简单的部分我就不说了,直接跳到

if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
    completedBlock(image, error, cacheType, url);
    return;
}

首先是SDWebImageAvoidAutoSetImage,我们看看它的注释

/**
 * By default, image is added to the imageView after download. But in some cases, we want to
 * have the hand before setting the image (apply a filter or add it with cross-fade animation for instance)
 * Use this flag if you want to manually set the image in the completion when success
 */

翻译过来就是说,默认情况下是等image完全从网络端下载完后,就会直接将结果设置到UIImageView。但是有些人想在获取到图片后,对图片做一些处理,比如使用filter去渲染图片或者给图片加个cross-fade animation(淡出动画)显示出来。那你就设置这个选项。然后得手动去处理图片下载完成后的事情。

上面说了要手动处理了,很自然你就会想到,这个手动处理就是compeletedBlock啊!当然,除了有这个枚举选项时需要手动处理,其实只要你自定义了compeletedBlock,都会调用你自定义处理的函数。你说我怎么知道的?你看下面的代码,如果你自定义了下载完成后的处理方式,并且也确实下载完成了(finished为YES),就执行自定义方式:

if (completedBlock && finished) {
    completedBlock(image, error, cacheType, url);
}

最后还剩下一个情况,就是url不存在的情况(注意上面讲的是if(url){…},下面讲的是在else{…}):

dispatch_main_async_safe(^{
    [self removeActivityIndicator];
    NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
    if (completedBlock) {
        completedBlock(nil, error, SDImageCacheTypeNone, url);
    }
});

首先自定义一个NSError的对象,表示url为空的错误。然后传给compeletedBlock。

知识点:NSError构造方法errorWithDomain

+ (instancetype)errorWithDomain:(NSString *)domain code:(NSInteger)code userInfo:(nullable NSDictionary *)dict;

其实目前来说,我心中还有两个最大的疑惑,一个就是operation怎么执行的,一个就是如何自定义compeletedBlock

2. operation执行过程


我们可以看到这里downloadImageWithURL其实是SDWebImageManager的方法

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

我们进去该函数实现,快两百行的代码了。好吧,先歇着,我们看看注释(我直接贴出我翻译过后的注释)。

/**
 * 如果图片不在缓存中,根据指定的URL下载图片,否则使用缓存中的图片。.
 *
 * @param url            图片的URL
 * @param options        该请求所要遵循的选项。(前面已经介绍了两个)
 * @param progressBlock  当图片正在下载时调用该block。
 * @param completedBlock 当操作完成后调用该block。
 *
 *   该参数是必须的。(指的是completedBlock)
 * 
 *   该block没有返回值并且用请求的UIImage作为第一个参数。
 *   如果请求出错,那么image参数为nil,而第二参数将包含一个NSError对象。
 *
 *   第三个参数是'SDImageCacheType'枚举,表明该图片重新获取方式是从本地缓存(硬盘)或者
 *   从内存缓存,还是从网络端重新获取image一遍。.
 *
 *   当options设为SDWebImageProgressiveDownload并且此时图片正在下载,finished将设为NO
 *   因此这个block会不停地调用直到图片下载完成,此时才会设置finished为YES.
 *
 * @return 返回一个遵循SDWebImageOperation协议的NSObject. 应该是一个SDWebImageDownloaderOperation的实例
 */

上面的注释有几个不认识的概念。一个是SDImageCacheType,另一个是options中的SDWebImageProgressiveDownload,还有一个SDWebImageDownloaderOperation

2.1 SDImageCacheType


typedef NS_ENUM(NSInteger, SDImageCacheType) {
    /**
     * 该图片无法从SDWebImage的缓存中获取,必须从web端下载。
     */
    SDImageCacheTypeNone,
    /**
     * 图片从硬盘缓存(disk cache)中获取
     */
    SDImageCacheTypeDisk,
    /**
     * 图片从硬盘缓存(disk cache)中获取
     */
    SDImageCacheTypeMemory
};

具体缓存实现方式我放在SDWebImage源码阅读(五)了。

2.2 SDWebImageProgressiveDownload


如果在加载图片中设定了该选项,那么图片会随着下载的进度一点点地显示出来。缺省情况下,图片是下载完成后一次显示出来的。

2.3 SDWebImageDownloaderOperation


看到这个类,我内心是愉快的。之前我不是说这个opertion应该和NSOperation有些关系吗?这个类就是NSOperation的子类啊,并且遵循SDWebImageOperation协议。这下SDWebImageDownloaderOperation将NSOperation和SDWebImageOperation联系在了一起,我们可以看下它的声明:

@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageOperation>

所以不用说,这个类一定是个重头戏。

但是我们搜索SDWebImageDownloaderOperation,发现SDWebImageManager中的downloadImageWithURL函数并没有返回SDWebImageDownloaderOperation。这一点很让人疑惑。不过我们发现SDWebImageDownloaderOperation遵循SDWebImageOperation协议,会不会和downloadImageWithURL的返回值id<SDWebImageOperation>有关系?而且看到这,我有些迷糊了。函数返回值为id<protocol>,这是什么返回值?什么时候需要这样用?感觉源码阅读进行不下去了。。。先不急,既然注释说返回的是SDWebImageDownloaderOperation,那么肯定就是啦。

我先在所有工程中搜索SDWebImageDownloaderOperation,发现在SDWebImageDownloader中也有一个downloadImageWithURL函数。而且里面就定义了一个opertion,这个opertion就是SDWebImageDownloaderOperation 类型,并且这个函数也是返回operation的。

__block SDWebImageDownloaderOperation *operation;

回到我们的SDWebImageManager中的downloadImageWithURL函数中,搜索downloadImageWithURL,找找看是不是有蛛丝马迹。果然,下面这段代码:

id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished)

此处的downloadImageWithURL是SDWebImageDownloader的一个方法。

好,但此为止,我们也只是觉得上面那些东西有联系,但是联系并不是很清晰。而且已经有了一个downloadImageWithURL,还要弄一个干什么?
太多问题了,我现在也只能大概猜测到底怎么回事了,在继续探索之前,我们先整理这里面的关系:


上面文字部分,我用标注了两个问题,我们先解决第一个

问题一:函数返回值为id<protocol>,这是什么返回值?

其实返回的是一个id类型,只是这个id类型一定要遵循里面的protocol,比如id<SDWebImageOperation>,那么因为SDWebImageDownloaderOperation遵循SDWebImageOperation协议,所以可以作为返回类型。

问题二:已经有了一个downloadImageWithURL,还要弄一个干什么?

这个说实话,我也不是很清楚,只能找这两个函数之间的关联了。其实更准确地说是找SDWebImageManager中downloadImageWithURL中的subOperation(SDWebImageDownloaderOperation)和operation的关系。根据这个思路,我发现了subOperation只在这个函数里面出现了两次。第一次是定义的地方,第二次就是:

operation.cancelBlock = ^{
    [subOperation cancel];
                
    @synchronized (self.runningOperations) {
        [self.runningOperations removeObject:weakOperation];
    }
};

无语,你辛辛苦苦弄了个subOperation,结果就亮了个像,还是cancel,就没了。太没人性了。不过是细想其实是有原因的,我在SDWebImageDownloaderOperation的downloadImageWithURL函数注释中找到了答案:

@return A cancellable SDWebImageOperation

一个cancellable的SDWebImageOperation,是不是和这里只用了cancel对应上了。虽然找到了点联系,不过还是流于表面,这与为什么这么做,这么做的理由还不是很清楚。

我们还是细细分析cancelBlock那段代码

首先是一个block,operation.cancelBlock有一个对应的setCancelBlock函数:

- (void)setCancelBlock:(SDWebImageNoParamsBlock)cancelBlock {
    // 检测self(是一个SDWebImageCombinedOperation类型的operation)是否取消了,如果取消了,就执行对应的cancelBlock函数。
    if (self.isCancelled) {
        if (cancelBlock) {
            cancelBlock();
        }
        _cancelBlock = nil; //不要忘了置cancelBlock为nil,否则会crash
    } else {
        _cancelBlock = [cancelBlock copy];
    }
}

也就是说operation如果取消了,那么就会执行subOperation的cancel函数。并且从runningOperations中移除该operation,因为是block,为了避免循环引用,所以使用了weakOperation。runningOperations大概从名字也能猜到,用来存储正在运行的operation。既然当前operation被取消了,肯定要从runningOperations移除的嘛!

注意此处的operation的类型是SDWebImageCombinedOperation,具体定义如下:

// 遵循SDWebImageOperation
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
// 注意SDWebImageCombinedOperation遵循SDWebImageOperation,所以实现了cancel方法
// 在cancel方法中,主要是调用了cancelBlock,这个设计很值得琢磨
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
// 根据cacheType获取到image,这里虽然名字用的是cache,但是如果cache没有获取到图片
// 还是要把image下载下来的。此处只是把通过cache获取image和通过download获取image封装起来
@property (strong, nonatomic) NSOperation *cacheOperation;
@end

还有一个问题就是@synchronized是什么?

知识点:@synchronized

避免多个线程执行同一段代码,主要防止当前operation会被多次remove,从而造成crash。这里括号内的self.runningOperations是用作互斥信号量。 即此时其他线程不能修改self.runningOperations中的属性。

虽然看懂了这段代码,可是后面不知道该看什么了。

所以我还是从头看这段代码(SDWebImageManager中downloadImageWithURL),看能不能找到点什么头绪:

// 如果调用此方法,而没有传completedBlock,那将是无意义的
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

不要将completedBlock参数设为nil,因为这样做是毫无意义的。如果你是想使用downloadImageWithURL来预先获取image,那就应该使用[SDWebImagePrefetcher prefetchURLs],而不是直接调用SDWebImageManager中的downloadImageWithURL函数。

// 使用NSString对象而非NSURL作为url是常见的错误. 因为某些奇怪的原因,Xcode不会报任何类型不匹配的警告,这里允许传NSString对象给URL。
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }
    // 防止传了一个NSNull值给NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

我觉得此处对于细节地处理很值得学习。

__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;

这个没啥好说的,避免循环引用,使用了weak。

    BOOL isFailedUrl = NO;
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        dispatch_main_sync_safe(^{
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
            completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
        });
        return operation;
    }

这个failedURLs从字面上理解就是一组下载失败的图片URL。所以这段代码也很好理解,就是如果这个图片url无法下载,那就使用completedBlock进行错误处理。那什么情况下算这个图片url无法下载呢?第一种情况是该url为空,另一种情况就是如果是failedUrl也无法下载,但是要避免无法下载就放入failedUrl的情况,就要设置options为SDWebImageRetryFailed。一般默认image无法下载,这个url就会加入黑名单,但是设置了SDWebImageRetryFailed会禁止添加到黑名单,不停重新下载。

如果该url可以下载,那么就添加一个新的operation到runningOperations中。

@synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }

剩下的100多行就是为了生成一个cacheOperation。那它到底是何方神圣?它是一个NSOperation,所以加入NSOperationQueue会自动执行。不过我还是全局搜索cacheOperation,发现它在SDWebImageCombinedOperation中的cancel方法中调用了:

- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock();
        
        // TODO: this is a temporary fix to #809.
        // Until we can figure the exact cause of the crash, going with the ivar instead of the setter
//        self.cancelBlock = nil;
        _cancelBlock = nil;
    }
}

还记得SDWebImageCombinedOperation遵循SDWebImageOperation协议吗?这就是SDWebImage实现的cancel。而cacheOperation是NSOperation,所以调用自身的cancel。注意是在这才会设置cancelled设为YES。

好,现在回来看这个queryDiskCacheForKey函数。在此之前,先看上面有段代码,用图片的url来获取cache对应的key,也就是说cache中如果已经有了该图片,那就返回该图片在cache中对应的key,你可以根据这个key去cache中获取图片。

NSString *key = [self cacheKeyForURL:url];

获取到key后,你就可以使用queryDiskCacheForKey函数去查找了:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    // 如果doneBlock不存在。那么就return nil。这个处理和downloadImageWithURL的completedBlock很类似
    if (!doneBlock) {
        return nil;
    }
    // 如果key为nil,说明cache中没有该image。所以doneBlock中传入SDImageCacheTypeNone,表示cache中没有图片,要从网络重新获取。
    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }
    // 如果key不为nil
    // 首先在内存cache中查找    
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    // 找到了,就传入SDImageCacheTypeMemory,说是在内存cache中获取的
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }
    // 否则,说明图片就在磁盘cache中。
    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }
        @autoreleasepool {
            UIImage *diskImage = [self diskImageForKey:key];
            // 如果磁盘中得到了该image,并且还需要缓存到内存中,为了同步最新数据
            if (diskImage && self.shouldCacheImagesInMemory) {
                // 后面细讲
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            // 传入SDImageCacheTypeDisk,说明是从磁盘中获取的
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });
    return operation;
}

其实这段代码如果不深究的话,也很容易理解的。我直接把说明写成注释在上面了。不过这里我还有个疑问,就是为啥operation要在查找硬盘缓存时,才创建了一个新的operation?这里我谈谈我的想法:因为“图片可以用”这个状态意味着图片必须在内存中了。图片在网络还是在硬盘,其实相对来说并没有本质区别,最后都是要加进内存的。所以这里就有一个加载到内存的过程,需要产生一个NSOperation,也就理所当然会发生cancel。我这也不是胡乱猜的,函数中有一个self.ioQueue。表明这是一个io序列(dispatch_queue_t)。

这下再回到downloadImageWithURL里面剩下的代码,就会很轻松了。因为它无非就是要处理上面那几种cache情况嘛。
我们还是一点点来看done^{}中的代码:
这段代码简单,不解释了,随时判断该operation是否已经cancel了。

if (operation.isCancelled) {
    @synchronized (self.runningOperations) {
        [self.runningOperations removeObject:operation];
    }
    return;
}

下面又是一段巨长的代码,我们先看看if中表示什么:

if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
    // ...
}

我们先看后面那个delegate方法:

/**
 * 当image无法在缓存中找到,调用该函数控制该image的下载
 *
 * @param imageManager 当前的`SDWebImageManager`
 * @param imageURL     需要下载的image的URL
 *
 * @return 返回NO表示当图片缓存未命中,反而阻止图片下载。如果该函数没实现,相当于返回YES。
 */
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;

这里要着重说明下此处return的含义。

注意在if里面最后一组||表达式,使用了短路判断(A||B,只要A为真,就不用判断B了),也就是说,如果delegate没有实现上面那个函数,整个表达式就为真,相当于该函数返回了YES。如果delegate实现了该函数,那就执行该函数,并且判断该函数执行结果。如果函数返回NO,那么整个if表达式都为NO,那么当图片缓存未命中时,图片下载反而被阻止。

目前我看的源码中并没有地方实现了该函数,所以就当if后半段恒为YES。我们主要还是看前面那个||表达式:

(!image || options & SDWebImageRefreshCached)

如果没有缓存到image,或者options中有SDWebImageRefreshCached选项,就执行if语句。现在我们深入看看if判断下的代码到底执行了什么,首先又是一个if语句:

if (image && options & SDWebImageRefreshCached) {
    dispatch_main_sync_safe(^{
        // 如果图片在缓存中找到,但是options中有SDWebImageRefreshCached
// 那么就尝试重新下载该图片,这样是NSURLCache有机会从服务器端刷新自身缓存。
        completedBlock(image, nil, cacheType, YES, url);
    });
}

下面的代码就表示开始要下载图片了。

首先定义了一个SDWebImageDownloaderOptions枚举值downloaderOptions,并根据options来设置downloaderOptions。基本上SDWebImageOptions和SDWebImageDownloaderOptions是一一对应的。只需要注意最后一个选项SDWebImageRefreshCached,这个得先强制关闭ProgressiveDownload方式。那后面的SDWebImageDownloaderIgnoreCachedResponse是什么意思呢?可能会有这样的疑惑,不是已经从imageCache中获取到了image了吗?还要Ignore干啥?这里简单提下,后面会详解:因为SDWebImage有两种缓存方式,一个是SDImageCache,一个就是NSURLCache,所以知道为什么这个选项是Ignore了吧,因为已经从SDImageCache获取了image,就忽略NSURLCache了

if (image && options & SDWebImageRefreshCached) {
        // 相当于downloaderOptions =  downloaderOption & ~SDWebImageDownloaderProgressiveDownload);
        downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
        // 相当于 downloaderOptions = (downloaderOptions | SDWebImageDownloaderIgnoreCachedResponse);
        downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}

然后生成了一个subOperation,这段代码也很长,我大致看了下SDWebImageDownloader中的downloadImageWithURL函数,感觉终于到了 “真正”下载的代码了。为什么这么说了?因为里面代码大部分都是iOS自带框架底层的代码了。总算到头了。不过这段代码我准备下一篇再看。

直接跳出这个subOperation的赋值语句,来到对应的else if语句:

else if (image) {
       // 从缓存中获取到了图片,而且不需要刷新缓存的
        // 直接执行completedBlock,其中error置为nil即可。
        dispatch_main_sync_safe(^{
            if (!weakOperation.isCancelled) {
                completedBlock(image, nil, cacheType, YES, url);
            }
        });
        // 执行完后,说明图片获取成功,可以把当前这个operation溢移除了。
        @synchronized (self.runningOperations) {
            [self.runningOperations removeObject:operation];
        }
    }
    else {
        // 又没有从缓存中获取到图片,shouldDownloadImageForURL又返回NO,不允许下载,悲催!
        // 所以completedBlock中image和error均传入nil。 
        dispatch_main_sync_safe(^{
            if (!weakOperation.isCancelled) {
                completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
            }
        });
        @synchronized (self.runningOperations) {
            [self.runningOperations removeObject:operation];
        }
 }

恩,SDWebImageManager中的downloadImageWithURL函数我们还剩下那个最精彩的SDWebImageDownloader中的downloadImageWithURL函数,留着下一篇阅读。

本文转载polobymulberry-博客园

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

推荐阅读更多精彩内容