SDWebImage探究(八) —— 深入研究图片下载流程(二)之开始下载并返回下载结果的总的方法

版本记录

版本号 时间
V1.0 2018.02.11

前言

我们做APP,文字和图片是绝对不可缺少的元素,特别是图片一般存储在图床里面,一般公司可以委托第三方保存,NB的公司也可以自己存储图片,ios有很多图片加载的第三方框架,其中最优秀的莫过于SDWebImage,它几乎可以满足你所有的需求,用了好几年这个框架,今天想总结一下。感兴趣的可以看其他几篇。
1. SDWebImage探究(一)
2. SDWebImage探究(二)
3. SDWebImage探究(三)
4. SDWebImage探究(四)
5. SDWebImage探究(五)
6. SDWebImage探究(六) —— 图片类型判断深入研究
7. SDWebImage探究(七) —— 深入研究图片下载流程(一)之有关option的位移枚举的说明

头文件的引入

如果你的空间是UIIMageView,那么需要引入的头文件时#import "UIImageView+WebCache.h";但是如果是UIButton,那么需要引入的头文件是#import "UIButton+WebCache.h"。这里就以UIIMageView为例就行说明,UIButton那个是类似的。


下载接口

SDWebImage为下载图片提供了很多接口,一共如下所示:

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

这里大家可以看到:

  • 上面7个接口都可以下载图片,且都是异步的。
  • 第一个方法最简单,只需要一个url地址;最后一个是5个参数,是条件最多的方法,大家可以根据需要选择需要的方法,一般我们选择第2个方法的情况居多。
  • 不管你用的哪个方法,最后在代码的实现上,你都是调用的是最后一个包含5个参数的那个方法,只不过没有的参数传为了nil或者0,比如方法1的实现如下所示。
- (void)sd_setImageWithURL:(nullable NSURL *)url {
    [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}
  • 对于包含5个参数的最后那个方法,需要重点注意的就是options那个参数,这里是一个枚举,很多东西都可以在里面设置,当你调用方法1的时候,options默认传0,下面我们就看一下0是什么意思。
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    /**
     * By default, when a URL fail to be downloaded, the URL is blacklisted so the library won't keep trying.
     * This flag disable this blacklisting.
     */
    SDWebImageRetryFailed = 1 << 0,

    /**
     * By default, image downloads are started during UI interactions, this flags disable this feature,
     * leading to delayed download on UIScrollView deceleration for instance.
     */
    SDWebImageLowPriority = 1 << 1,

    /**
     * This flag disables on-disk caching
     */
    SDWebImageCacheMemoryOnly = 1 << 2,

    /**
     * This flag enables progressive download, the image is displayed progressively during download as a browser would do.
     * By default, the image is only displayed once completely downloaded.
     */
    SDWebImageProgressiveDownload = 1 << 3,

    /**
     * Even if the image is cached, respect the HTTP response cache control, and refresh the image from remote location if needed.
     * The disk caching will be handled by NSURLCache instead of SDWebImage leading to slight performance degradation.
     * This option helps deal with images changing behind the same request URL, e.g. Facebook graph api profile pics.
     * If a cached image is refreshed, the completion block is called once with the cached image and again with the final image.
     *
     * Use this flag only if you can't make your URLs static with embedded cache busting parameter.
     */
    SDWebImageRefreshCached = 1 << 4,

    /**
     * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
     * extra time in background to let the request finish. If the background task expires the operation will be cancelled.
     */
    SDWebImageContinueInBackground = 1 << 5,

    /**
     * Handles cookies stored in NSHTTPCookieStore by setting
     * NSMutableURLRequest.HTTPShouldHandleCookies = YES;
     */
    SDWebImageHandleCookies = 1 << 6,

    /**
     * Enable to allow untrusted SSL certificates.
     * Useful for testing purposes. Use with caution in production.
     */
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,

    /**
     * By default, images are loaded in the order in which they were queued. This flag moves them to
     * the front of the queue.
     */
    SDWebImageHighPriority = 1 << 8,
    
    /**
     * By default, placeholder images are loaded while the image is loading. This flag will delay the loading
     * of the placeholder image until after the image has finished loading.
     */
    SDWebImageDelayPlaceholder = 1 << 9,

    /**
     * We usually don't call transformDownloadedImage delegate method on animated images,
     * as most transformation code would mangle it.
     * Use this flag to transform them anyway.
     */
    SDWebImageTransformAnimatedImage = 1 << 10,
    
    /**
     * 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
     */
    SDWebImageAvoidAutoSetImage = 1 << 11,
    
    /**
     * By default, images are decoded respecting their original size. On iOS, this flag will scale down the
     * images to a size compatible with the constrained memory of devices.
     * If `SDWebImageProgressiveDownload` flag is set the scale down is deactivated.
     */
    SDWebImageScaleDownLargeImages = 1 << 12
};

这里0的意思就是SDWebImageRetryFailed,它的意思是默认情况下,当下载失败后就会将url放入黑名单不会再次下载了,这里传0,就是默认不成立,意思就是下载失败还是要继续下载的,其他的options大家都需要重点看一下,后面涉及的时候会和大家接着说。


调用接口后的第一个方法

上面调用完接口后,我们看框架调用了这个方法。作者给放入在#import "UIView+WebCache.h"文件中了。

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock

这个和上面的方法很相似,不同的是sd_internalSetImageWithURL,可能是作者将其作为内部方法加入的internal作为区别。

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock {
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    if (url) {
        // check if activityView is enabled or not
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
        
        __weak __typeof(self)wself = self;
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            [sself sd_removeActivityIndicator];
            if (!sself) {
                return;
            }
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                    completedBlock(image, error, cacheType, url);
                    return;
                } else if (image) {
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];
                } else {
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

下面我们就看一下这个方法里面都做了什么?

1. 取消对应key正在下载的图片

我们看一下最前面的三行代码

NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

这里手下你是获取validOperationKey的值,它是根据方法中的参数operationKey进行确定的,如果你插件接口,就会发现,每次你调用下载接口这个operationKey传入的都是nil,这里用一个三目运算符,如果为nil,那么就把值NSStringFromClass([self class]赋给它,这里是用UIImageView调用的,所以validOperationKey的值就是UIImageView

然后执行第二句[self sd_cancelImageLoadOperationWithKey:validOperationKey];取消对应的key的图像下载。具体如何取消下载的,我们会单独发文进行说明,这里限于篇幅,就先写这么多了。

继续看下面这个objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);这个是运行时的比较典型的应用,因为分类是不能利用setter或者getter。这个时候我们就需要利用运行时,将key对应的值绑定到当前对象中,当我们想用的时候也是根据key和当前对象或者绑定的值。

我们先看一下API

/** 
 * Returns the value associated with a given object for a given key.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * 
 * @return The value associated with the key \e key for \e object.
 * 
 * @see objc_setAssociatedObject
 */
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

然后在看一下作者要使用的地方。

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

上面这个就是将值url绑定到对象self中,key是imageURLKey

- (nullable NSURL *)sd_imageURL {
    return objc_getAssociatedObject(self, &imageURLKey);
}

上面这个就是根据key imageURLKey获取和self绑定的值。我们通过控制台输出如下:

(lldb) po objc_getAssociatedObject(self, &imageURLKey);
http://image.xxxx.com/6e51869946890531e1b24012a9b489ea-100_100.jpg

这里作者就将key将url绑定到self中,所以利用此方法获取的也是url的值,也就是图像的下载地址。这样以后我们在这个文件中想获取到图像的下载地址就很方便了,直接调用这个方法就可以,达到了一般类中类似属性或者成员变量的那种全局的效果。具体这么做有什么用,我后面会另外分一个篇幅进行说明。

2. 与options相关的逻辑处理

我们先看一下这段代码

    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }

这里要先看一下枚举值SDWebImageDelayPlaceholder

    /**
     * By default, placeholder images are loaded while the image is loading. This flag will delay the loading
     * of the placeholder image until after the image has finished loading.
     */
    SDWebImageDelayPlaceholder = 1 << 9,

这个枚举值的意思是,默认下,当图像下载的时候显示占位图,一旦设置这个option,意思就是在图像下载之前,不显示占位图。我们接着看,到这里,如果你调用下载图片的接口的时候如果传入了options这个枚举参数,那么这里就进行了对比,如果不是SDWebImageDelayPlaceholder值,那么就调用方法- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock,进行设置占位图。如果是SDWebImageDelayPlaceholder值,那么这个if里面就不会执行,也就是说不会设置占位图。

这里还有一个地方值得我们去学习,就是这种带参数的宏定义。

dispatch_main_async_safe

这里是按照下面这个进行定义的。

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }

这里面进行了判断,strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0,经过判断值为0表示相等,就是主线程,执行block。每个线程我们create以后都会分配给一个可以区分的label,是一个字符串,通过传入DISPATCH_CURRENT_QUEUE_LABEL查询当前线程的label值,所以这个就很好理解了,通过主线程的label与当前线程的label进行对比,如果相等,就执行block,如果不相等,就直接在主线程执行。下面我们看一个函数const char * dispatch_queue_get_label(dispatch_queue_t _Nullable queue);,可以帮助大家更好的理解这个函数。

/*!
 * @function dispatch_queue_get_label
 *
 * @abstract
 * Returns the label of the given queue, as specified when the queue was
 * created, or the empty string if a NULL label was specified.
 *
 * Passing DISPATCH_CURRENT_QUEUE_LABEL will return the label of the current
 * queue.
 *
 * @param queue
 * The queue to query, or DISPATCH_CURRENT_QUEUE_LABEL.
 *
 * @result
 * The label of the queue.
 */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_PURE DISPATCH_WARN_RESULT DISPATCH_NOTHROW
const char *
dispatch_queue_get_label(dispatch_queue_t _Nullable queue);

3. 与url相关的if - else逻辑处理

上面执行完毕,接下来就是后面的与url值相关的if- else逻辑处理,可以看见,处理完毕了,该方法也就结束了,可以预见这里面的逻辑嵌套的应该很复杂。

  • 当url存在不为空

1)首先利用SDWebImageManager单利进行下载任务。

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock 

这个方法是异步的,当返回成功后,就会在主线程进行不同条件的标记刷新和返回对应需要的数据。

2)成功返回后,先移除加载的动画,这里做了容错的处理。

 __weak __typeof(self) wself = self;

... ... 
//结果返回中
__strong __typeof (wself) sself = wself;
if (!sself) {
    return;
}

这样的处理是需要我们多学习和加入到我们的代码开发中的。

接着我们看在主线程中都做了什么,其实就是下面这个多条件分支的代码。

if (!sself) {
    return;
}
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
    completedBlock(image, error, cacheType, url);
    return;
} 
else if (image) {
    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
    [sself sd_setNeedsLayout];
} 
else {
    if ((options & SDWebImageDelayPlaceholder)) {
        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        [sself sd_setNeedsLayout];
    }
}

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

我们一起开看一下。

a)如果返回的image不为空,并且option是SDWebImageAvoidAutoSetImage且完成的completedBlock不为空时,返回的是completedBlock(image, error, cacheType, url);,给调用接口进行使用。这里的SDWebImageAvoidAutoSetImage的定义如下所示,其实就是下载后不要自动给UIImageView赋值image,这里选择的就是手动。

    /**
     * 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
     */
    SDWebImageAvoidAutoSetImage = 1 << 11,

b)如果只有image不为空,其他几个条件最少有一个不满足的时候,就会走到下一个分支,这里就是直接将下载后的图片给UIImageView,并且标记为需要刷新。

[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];

这里传入的setImageBlock为nil,所以这里就是单纯的赋值和刷新界面。

c) 以上条件都不满足的情况下,这个时候判断options。如果options是SDWebImageDelayPlaceholder也就是延迟显示占位图的情况,那么就是调用

[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];

调用和上面相同的方法显示占位图,这里给UIImageView传入的就是placeholder,并标记为需要刷新。

d)单独的一个判断,如果completedBlock不为空以及 finished == YES的情况下,那么就返回completedBlock(image, error, cacheType, url),这时候返回的image有可能是空的。

e)根据指定的validOperationKey绑定这个下载的operation

这里就是一句代码

[self sd_setImageLoadOperation:operation forKey:validOperationKey];

这里为什么要绑定,后面会分开一篇文章进行讲解。大家先记住,下载成功回调绑定了这个下载操作。

  • 如果url为空,在主线程中进行了操作。
dispatch_main_async_safe(^{
    [self sd_removeActivityIndicator];
    if (completedBlock) {
        NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
        completedBlock(nil, error, SDImageCacheTypeNone, url);
    }
});

这里做的首先是移除下载的动画,并进行判断completedBlock不为nil的时候直接返回completedBlock(nil, error, SDImageCacheTypeNone, url);,这里的block的image为nil,并给出了一个error。

这里错误error的错误域是:

NSString *const SDWebImageErrorDomain = @"SDWebImageErrorDomain";

就是一个常量的字符串。

错误码就是-1了,缓存类型为SDImageCacheTypeNone,既然nil为空没有下载下来图片,当然就不会有缓存了。

到此为止,下载图片的第一个方法就解析完了,大家觉得简单吗?不,还有很多没和大家说,包括如何进行下载的,下载后的解码以及缓存等很多细节都是要进行详细解析的,这里只是给大家一个基本的流程和概念,后面会分几篇进行详细的说明。一定会给大家讲的清楚和明白。

后记

本篇已结束,后面更精彩~~~

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

推荐阅读更多精彩内容