优雅的SDWebImage

简述

Asynchronous image downloader with cache support as a UIImageView category.
上面的是关于SDWebImage的官方说明, 可以翻译为: 一个支持缓存的, 采用UIImageView分类形式的图片下载器.
共包含以下功能:

  1. 以UIImageView的分类, 来支持网络图片的加载和缓存管理
  2. 一个异步的图片加载器
  3. 一个异步的内存+磁盘图片缓存
  4. 支持GIF
  5. 支持WebP
  6. 后台图片解压缩处理
  7. 确保同一个URL的图片不被多次下载
  8. 确保虚假的URL不会被反复加载
  9. 确保下载及缓存时, 主线程不被阻塞
  10. 使用GCD和ARC
  11. 支持ARM64

源码分析

以UIImageView图片加载为例, 最常用的方法就是如下方法(类目 UIImageView + WebCache 里):
 - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder

内部调用以下方法:

 - (void)sd_setImageWithURL:(NSURL *)url
                  placeholderImage:(UIImage *)placeholder
                  options:(SDWebImageOptions)options
                  progress:(SDWebImageDownloaderProgressBlock)progressBlock
                  completed:(SDWebImageCompletionBlock)completedBlock

对这个方法进行如下结构解析:

取消当前图片加载

为防止同一个UIImageView对象同时加载多个image, 需首先取消当前UIImageView对象的图片加载, 如下:

[self sd_cancelCurrentImageLoad];

下载图片操作的存储和取消单独放在一个UIView的类目里, 这样的设计让代码更简洁, 耦合性更低. UIView+WebCacheOperation, 这个类目主要有三个方法,

  1. 添加下载操作
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
    [self sd_cancelImageLoadOperationWithKey:key];
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    [operationDictionary setObject:operation forKey:key];
}
  1. 取消下载操作
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
    // Cancel in progress downloader from queue
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    id operations = [operationDictionary objectForKey:key];
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }
}
  1. 移除下载操作
- (void)sd_removeImageLoadOperationWithKey:(NSString *)key {
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    [operationDictionary removeObjectForKey:key];
}

上述代码普遍使用了关联对象,主要会用到以下两个方法:

  1. void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
    用来给对象添加成员关联对象
  2. id objc_getAssociatedObject(id object, void *key)
    通过key值获取对应的关联对象

一般来说, 关联对象在OC中主要解决如下这样的问题:
我们需要在类目中保存一些状态,可以使用成员变量的形式来实现,但是,这些状态又是相对独立的, 只在这个类目中会被用到。所以,我们想把这个成员变量隐藏在这个类目中,对于其他的类目,这个成员变量都不可见。
如果类目允许扩展成员变量的话,这个就很好解决, 直接在类目的实现文件里声明一个成员变量即可。这样既对外部隐藏这个成员变量,又能在这个类目中使用它。很完美的解决方案。但是不幸的是,OC不允许在类目中扩展成员变量。所以不得不在类的声明中添加需要的成员变量,而且还需要把它暴露出来,以使你的分类能够使用它。这样,随着类目越来越多,你不得不在类中声明越来越多原本需要对外隐藏的成员变量。
以上代码所用的关联对象就很好的解决了这个问题,不需要在声明中创建对外可见的成员变量operationDictionary,只用key: loadOperationKey 关联了字典operationDictionary,就可以用如下的方式来使用此对象。

NSMutableDictionary *operationDictionary = [self operationDictionary]; 

[operationDictionary setObject:operation forKey:key];

具体可参考雷纯峰的博客:http://blog.leichunfeng.com/blog/2015/06/26/objective-c-associated-objects-implementation-principle/

取消图片加载操作的内部逻辑是:
利用key: UIImageViewImageLoad 去操作字典中查找, 如果存在下载图片的操作则取消.
代理 SDWebImageOperation 只有一个 -cancel 方法, 目的是为服从此代理的所有 Operation 类都提供一个统一的方法名, 而不需要在 SDWebImageCombinedOperation 和 SDWebImageDownloaderOperation 的头文件中再单独声明取消方法. - sd_cancelImageLoadOperationWithKey 方法中的 operation属于 SDWebImageCombinedOperation 类, 它的取消方法如下:

- (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;
    }
}

上面的方法包含两个取消操作

  1. [self.cacheOperation cancel], 这个是取消查找存储(缓存)图片操作
  2. self.cancelBlock(), 这个block 为如下代码:
operation.cancelBlock = ^{
                [subOperation cancel];
                
                @synchronized (self.runningOperations) {
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    if (strongOperation) {
                        [self.runningOperations removeObject:strongOperation];
                    }
                }
            };

其中 subOperation 属于SDWebImageDownloaderOperation类, 为下载图片的操作类, 它将执行最终的取消下载方法, 如下:

- (void)cancel {
    @synchronized (self) {
        if (self.thread) {
            [self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO];
        }
        else {
            [self cancelInternal];
        }
    }
}

如上所示, 如果operations对象在单独的线程里, 需要单独处理线程, 取消下载后, 要停止当前线程的runloop, CFRunLoopStop(CFRunLoopGetCurrent()), 具体如下代码:

- (void)cancelInternalAndStop {
    if (self.isFinished) return;
    [self cancelInternal];
    CFRunLoopStop(CFRunLoopGetCurrent());
}

一系列的操作, 最终会通过苹果提供的NSURLConnection类的方法 cancel 来完成本次取消下载任务, 如下代码所示:

- (void)cancelInternal {
    if (self.isFinished) return;
    [super cancel];
    if (self.cancelBlock) self.cancelBlock();

    if (self.connection) {
        // 取消下载的操作最终会来到这里
        [self.connection cancel];
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });

        // As we cancelled the connection, its callback won't be called and thus won't
        // maintain the isFinished and isExecuting flags.
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished = YES;
    }

    [self reset];
}

以上就是 [self sd_cancelCurrentImageLoad] 这个方法的逻辑步骤.

②. 然后通过runtime把图片的url地址和UIImageView对象设置依赖关系, 代码如下:

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

接下来紧跟着两个判断语句:
③. 第一个是判断如果只有placeHolder, 那么就显示它.
④. 第二判断语句内部是真正的下载.下面来详细分析:
如果图片url不为空, 进入处理下载图片的逻辑:
1> 首先判断是否需要显示图片加载Indicator, 代码如下:

if ([self showActivityIndicatorView]) {
         [self addActivityIndicator];
}

这里也用到了关联对象的方法.
第一步:
使用key: TAG_ACTIVITY_SHOW 保存 传入的布尔值(转化为NSNumber类型保存), 调用共有方法:

- (void)setShowActivityIndicatorView:(BOOL)show{
    objc_setAssociatedObject(self, &TAG_ACTIVITY_SHOW, [NSNumber numberWithBool:show], OBJC_ASSOCIATION_RETAIN);
}

第二步:
通过key: TAG_ACTIVITY_SHOW 获取传入的是否显示图片加载的布尔值.

- (BOOL)showActivityIndicatorView{
    return [objc_getAssociatedObject(self, &TAG_ACTIVITY_SHOW) boolValue];
}

2> 通过单例SDWebImageManager,创建一个下载操作operation,这个操作operation不是真正的 NSOperation 类,而是一个服从代理 SDWebImageOperation 的继承 NSObject的类 SDWebImageCombinedOperation,这个类的作用是用来组合查找存储图片操作 cacheOperation 和 下载图片操作 subOperation(属于类SDWebImageDownloaderOperation) ,当需要取消这两个操作时,如上分析,只需要调用 operation (类SDWebImageCombinedOperation) 的 -cancel 方法。
通过 id<SDWebImageOperation> operation 接收 SDWebImageManager 的下载操作,为了能够确保单个 UIImageView 同一时刻只有单个对应的图片被下载,需要保存这个 operation。这个时候你会想到什么类,没错,真是苹果提供的字典类。
把operation放进字典, 对应的key 为“UIImageViewImageLoad”。在下载图片前要在字典里通过key查找是否有正在进行的下载操作,如果有就取消。
图片下载就在这个operation的block里执行,并通过它向外输出下载图片最终的信息。在这里要插入一个runtime的关联对象的概念,在把 operation对象 放字典里这个过程中, 作者使用了runtime的关联对象的方法, 代码如下:

    - (NSMutableDictionary *)operationDictionary {
    NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
    if (operations) {
        return operations;
    }
    operations = [NSMutableDictionary dictionary];
    objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return operations;
}

SDWebImageManager的下载操作

  • 如下方法
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock

内部逻辑如下

  • 判断URL格式如果是字符串类型,转换成URL类型
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }
  • 防止传入NSNull导致程序崩溃
if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
  • 确保虚假的URL不会被反复下载
BOOL isFailedUrl = NO;
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }
  • 这几种情况下执行是下载错误, 通过block携带错误信息并返回, 不执行之后的下载动作
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;
    }
  • 把刚刚创建的这个操作放在“正在运行的操作数组”里
@synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
  • 根据是否有过滤条件返回对应的key
NSString *key = [self cacheKeyForURL:url];
- (NSString *)cacheKeyForURL:(NSURL *)url {
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    }
    else {
        return [url absoluteString];
    }
}
  • 从缓存或磁盘中查找图片
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }

    if (!key) {
        // 如果key为nil, 则返回传空递信息回去
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // First check the in-memory cache...
    // 1. 首先查找缓存中是否有图片, 如果有则返回这个图片和图片存储类型
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            // 2. 从磁盘中查找图片, 并且判断是否需要保存, 然后返回
            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;
}

上面代码使用@autoreleasepool {},目的是在doneBlock执行完毕后,立即销毁临时变量 diskImage,优化内存管理。

执行上述代码的 doneBlock ,

  • 如果操作被取消了,移除操作,并返回
if (operation.isCancelled) {
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }

            return;
        }
  • 如果从缓存或者文件中查找到图片并返回,执行如下逻辑
else if (image) {
            dispatch_main_sync_safe(^{
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (strongOperation && !strongOperation.isCancelled) {
                    completedBlock(image, nil, cacheType, YES, url);
                }
            });
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }
  • 如果未查找到图片且进入不了下载操作,执行如下逻辑
else {
            // Image not in cache and download disallowed by delegate
            dispatch_main_sync_safe(^{
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (strongOperation && !weakOperation.isCancelled) {
                    completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
                }
            });
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }

接下来分析下载逻辑

  • 下载前的 判断语句
if ((!image || options & SDWebImageRefreshCached) 
&& 
(![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] 
|| [self.delegate imageManager:self shouldDownloadImageForURL:url]))

上述判断语句,一个和查找图片有关,一个和是否允许下载的代理有关,以下几种情况会进入下载逻辑:

num 可下载
1 无代理方法,且未找到图片
2 无代理方法,找到图片,但需更新存储
3 有代理方法,允许下载图片,且未找到图片
4 有代理方法,找到图片,但需更新存储
  • 找到图片,到需要更新存储,先走如下逻辑,然后再下载更新图片
if (image && options & SDWebImageRefreshCached) {
                dispatch_main_sync_safe(^{
                    // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
                    // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
                    completedBlock(image, nil, cacheType, YES, url);
                });
            }

options & SDWebImageRefreshCached 相当于 options
== SDWebImageRefreshCached

downloadImagewithURL:options:progress:completed:方法里面来处理的, 该方法调用了addProgressCallback:andCompletedBlock:forURL:createCallback方法来将请求的信息存入管理器中,同时在回调block中创建新的操作, 配置之后将其放入downloadQueue操作队列中, 最后方法返回新创作的操作. 其具体实现如下:


 - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options 
  progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
    __block SDWebImageDownloaderOperation *operation;
    __weak __typeof(self)wself = self;

    [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
        NSTimeInterval timeoutInterval = wself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // 1. 创建请求对象, 并根据options参数设置其属性
        // 为了避免潜在的重复缓存(NSURLCache + SDImageCache), 如果没有明确告知需要缓存, 则禁用图片请求的缓存操作
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url 
 cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (wself.headersFilter) {
            request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = wself.HTTPHeaders;
        }
        
        // 2. 创建SDWebImageDownloaderOperation操作对象,并进行配置
        // 配置信息包括是否需要认证,优先级
        operation = [[wself.operationClass alloc] initWithRequest:request
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                             // 3. 从管理器的callbacksForURL中找出该URL所有的进度处理回调并调用
                                                             SDWebImageDownloader *sself = wself;
                                                             if (!sself) return;
                                                             __block NSArray *callbacksForURL;
                                                             dispatch_sync(sself.barrierQueue, ^{
                                                                 callbacksForURL = [sself.URLCallbacks[url] copy];
                                                             });
                                                             for (NSDictionary *callbacks in callbacksForURL) {
                                                                 dispatch_async(dispatch_get_main_queue(), ^{
                                                                     SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                                                     if (callback) callback(receivedSize, expectedSize);
                                                                 });
                                                             }
                                                         }
                                                        completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                                                             // 4. 从管理器的callbacksForURL中找出该URL所有的完成处理回调并调用
                                                             // 如果finished为YES, 则将该url对应的回调信息从URLCallbacks中删除
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            __block NSArray *callbacksForURL;
                                                            dispatch_barrier_sync(sself.barrierQueue, ^{
                                                                callbacksForURL = [sself.URLCallbacks[url] copy];
                                                                if (finished) {
                                                                    [sself.URLCallbacks removeObjectForKey:url];
                                                                }
                                                            });
                                                            for (NSDictionary *callbacks in callbacksForURL) {
                                                                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                                if (callback) callback(image, data, error, finished);
                                                            }
                                                        }
                                                        cancelled:^{
                                                            // 5. 取消操作讲该url对应的回调信息从URLCallBacks中删除
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            dispatch_barrier_async(sself.barrierQueue, ^{
                                                                [sself.URLCallbacks removeObjectForKey:url];
                                                            });
                                                        }];
        operation.shouldDecompressImages = wself.shouldDecompressImages;
        
        if (wself.urlCredential) {
            operation.credential = wself.urlCredential;
        } else if (wself.username && wself.password) {
            operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }

        // 6. 将操作加入到操作队列downloadQueue中
        // 如果是LIFO顺序, 则将新的操作作为原队列中最后一个操作的依赖,然后将新操作设置为最后一个操作
        [wself.downloadQueue addOperation:operation];
        if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [wself.lastAddedOperation addDependency:operation];
            wself.lastAddedOperation = operation;
        }
    }];

    return operation;
}

在上面的代码中创建完请求对象, 并且配置好请求信息之后, 真正的下载操作是在SDWebImageDownLoadeerOperation类中完成的, 本类的初始化方法传入了三个block, progress, completed, 和cancelled, 分别用来处理进度 下载完成以及取消操作.
本类使用了foundation框架提供的NURLConnection类而不是7.0之后推出的NSURLSession类来执行请求的. 本类是NSOperation的子类, 执行请求是通过触发本类的start方法.

最近通过阅读这个框架的源码, 感受最深的是设计之巧妙, 我用伪代码用自己的方式试着实现一遍图片下载, 然后再和这个框架的设计方法进行比较, 发现真是有天壤之别.

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

推荐阅读更多精彩内容