【源码解读】SDWebImage ─── 总结

SDWebImage是一个提供UIImageView和UIButton类异步加载图片并且缓存的框架,接口简洁,类的分工十分明确。框架文件也不少,但主要围绕下载缓存的实现去解读就容易理清楚。

我们以UIImageView的下载为例来探究。

UIImageView+WebCache

当我们使用图片异步加载时,是用分类UIImageView+WebCache中的接口。该分类提供了多种设置图片的方式,但是最终都会调用如下方法(这也是一个常见的设计思想)。

//设置图片
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

当然,可以设置单张图片,也可以设置gif图片(多张图片)。也可以取消对应图片的下载。

//设置gif图片(多张图片)
- (void)sd_setAnimationImagesWithURLs:(NSArray *)arrayOfURLs;
//取消图片下载
- (void)sd_cancelCurrentImageLoad;
//取消gif图片下载(多张图片)
- (void)sd_cancelCurrentAnimationImagesLoad;

除了设置图片外,UIImageView+WebCache还提供设置菊花指示器的功能(UIActivityIndicatorView),以便在加载时可以控件中看到旋转的菊花指示器,起到提示用户的功能。

//设置是否显示
- (void)setShowActivityIndicatorView:(BOOL)show;
//设置样式
- (void)setIndicatorStyle:(UIActivityIndicatorViewStyle)style;

了解完UIImageView+WebCache的功能,我们主要来看看怎么设置单张图片的?

//设置单张图片的主要调用入口
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
    //取消当前的下载???
    [self sd_cancelCurrentImageLoad];
    //关联对象,相当于有个url保存在self中
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //如果不是下载好再加载占位符的模式  就在主线程设置占位符
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }
    
    if (url) {

        // 添加菊花
        if ([self showActivityIndicatorView]) {
            [self addActivityIndicator];
        }

        __weak __typeof(self)wself = self;
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            //移除菊花
            [wself removeActivityIndicator];
            //如果控制器被释放,就不用继续执行
            if (!wself) return;
            dispatch_main_sync_safe(^{
                //如果控制器被释放,就不用继续执行
                if (!wself) return;
                
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)//有图片,非自动设置图片的模式,传出结果就好
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image) {//有图片,自动设置图片
                    wself.image = image;
                    [wself setNeedsLayout];//必要吗?
                } else {//没图片
                    //下载好再加载占位符的模式,设置占位图片(其他模式肯定已经设置好了)
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        //UIView的operations字典保存UIImageViewImageLoad和UIImageViewAnimationImages的operation
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    } else {
        //提示错误
        dispatch_main_async_safe(^{
            [self removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

当给UIImageView设置单张图片时,会进行如下操作:
① 取消当前的下载操作。
② 通过动态关联,将imageURL保存起来,成为UIImageView的属性。
③ 根据传入的模式来设置占位符。
④ 通过判断url来决定返回错误或者通过url去获取,获取的话就是交给SDWebImageManager了。

下载时,会将对应的下载操作保存起来,在UIView+WebCacheOperation中,动态关联了一个operations的字典,用来保存一个下载单张图操作,和一个下载图片组操作。之所以把保存operation的功能放在UIView的分类中,也是为了能让其他UIView的子类,比如UIButton使用。
那为什么要保存对应的操作呢?我想作者是把UIImageView分成可以同时设置单张图片和图片组两种方式,这两张设置不冲突,但是你设置单张图片的时候,就必须把你上次设置单张图片的操作取消。

//动态关联对象,解决分类创建不了属性的问题,提供operations属性的功能
//这边有点类似于懒加载(先获取,如果没有再创建)
- (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;
}

前面讲完了怎么设置图片的方法,最主要的通过url获取图片的方法(下载或者缓存)还是在SDWebImageManager中。

SDWebImageManager

SDWebImageManager是一个管理类,里面有这么两个重要的属性(缓存和下载器)。

@property (strong, nonatomic, readwrite) SDImageCache *imageCache;
@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;

SDWebImageManager是一个单例对象,在创建时也自动创建了缓存和下载器两个对象。

+ (id)sharedManager {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

- (instancetype)init {
    SDImageCache *cache = [SDImageCache sharedImageCache];
    SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
    return [self initWithCache:cache downloader:downloader];
}

- (instancetype)initWithCache:(SDImageCache *)cache downloader:(SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
        _imageCache = cache;
        _imageDownloader = downloader;
        _failedURLs = [NSMutableSet new];
        _runningOperations = [NSMutableArray new];
    }
    return self;
}

关于缓存和下载器具体的功能和设计可以看我的这两篇文章。
【源码解读】SDWebImage ─── 缓存的设计
【源码解读】SDWebImage ─── 下载器的设计

SDWebImageManager提供的功能其实就是缓存和下载器的组合:你提供一个url,我去缓存(imageCache)中(先内存缓存再磁盘缓存)寻找,如果没找到就让下载器(imageDownloader)去下载。

当我们通过SDWebImageManager获取图片时,调用的是:

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

我们不仅需要传入url,下载中的回调progressBlock,完成后的回调completedBlock,还要传入options,这个options可以是单个也可以是多个,主要用来控制图片的设置过程(包括缓存和下载),比如传入SDWebImageAvoidAutoSetImage就需要我们手动去将下载完成后的图片赋值给UIImageView。可以说options是为了满足一些特殊的需求,当options为nil时就是一些通用的过程。

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    SDWebImageRetryFailed = 1 << 0,//失败后重试
    SDWebImageLowPriority = 1 << 1,//低优先级,比如UIScrollow滚动时不下载
    SDWebImageCacheMemoryOnly = 1 << 2,//只进行内存缓存
    SDWebImageProgressiveDownload = 1 << 3,//渐进式下载,图片渐进式显示
    SDWebImageRefreshCached = 1 << 4,//刷新缓存
    SDWebImageContinueInBackground = 1 << 5,//后台下载
    SDWebImageHandleCookies = 1 << 6,//存储Cookies
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,//允许非法证书
    SDWebImageHighPriority = 1 << 8,//放在高优先级的队列
    SDWebImageDelayPlaceholder = 1 << 9,//延迟占位图片出现的时间,等下载完成
    SDWebImageTransformAnimatedImage = 1 << 10,//gif的动画????
    SDWebImageAvoidAutoSetImage = 1 << 11//下载完不自动设置图片
};

接下来我们来看SDWebImageManager具体是怎么设计的?
当我们传入url,下载中的回调progressBlock,完成后的回调completedBlock,以及相关的options来SDWebImageManager获取图片时,会进行如下操作(后面附上源码和解析)。
① 判断不可以下载的情况(url不可用,该url曾经下载失败过)
这边也可以看出如果不是设置成SDWebImageRetryFailed,一旦url下载失败过,就会加入failedURLs,下次下载该url时,直接返回错误信息。

② 到这一步证明url可以下载。创建一个SDWebImageCombinedOperation对象,并添加进runningOperations中。
SDWebImageCombinedOperation是一个组合操作(里面有cacheOperation的属性),后面会将imageCache查询获取的操作赋值给cacheOperation。
runningOperations是为了取消操作,判断是否有正在进行的操作。

③ 将imageCache查询获取的操作赋值给组合操作operation中的cacheOperation,并将该operation返回给UIImageView的分类。

在imageCache查询结果的回调中,主要分成三种情况:

  • 如果缓存中没有图片
    通过options去设置imageDownloader的downloaderOptions,然后通过imageDownloader创建一个下载队列subOperation。
    imageDownloader下载完成的回调又有以下三种情况:
    1)被取消,不需要做什么
    2)有错误,返回对应错误,并添加进failedURLs
    3)成功下载,没有下载图片的情况就是NSURLCache有缓存,有下载图片的情况,如果有实现转换图片的代理,就先转换,再通过imageCache储存起来,并且在主线程回调completedBlock。

  • 如果缓存中有图片
    在主线程回调completedBlock,将image传出去。将operation从runningOperations移除。

  • 如果缓存没有图片,下载操作也不被允许(通过代理来设置)
    在主线程回调completedBlock,将nil和SDImageCacheTypeNone传出去。将operation从runningOperations移除。

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
    /*  容错机制  */
    //completedBlock不能为空
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    //如果误传NSString类型,就转成NSURL类型
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // 如果传的是其他乱七八糟的类型,比如NSNull,就置为nil
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    //创建一个混合操作(其实是缓存操作)
    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    BOOL isFailedUrl = NO;
    //加锁,获取不可用集合里是否有包含该url
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }

    //如果url为空,或者(options为不是重新下载错误url且是错误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;
    }

    //到这步,证明可以下载
    //加锁,添加到正在下载的集合runningOperations中
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    
    //通过url获取key(没设置过滤器的情况就是装成NSString)
    //过滤器的作用就是让用户来自定义装换的标准
    NSString *key = [self cacheKeyForURL:url];

    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
        //如果操作被取消,就从当前运行的操作数组中剔除
        if (operation.isCancelled) {
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }

            return;
        }

#pragma mark - 如果没有图片 || 需要更新缓存的类型 && 下载代理能响应方法
        if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            //如果有图片,但是是需要更新缓存的类型
            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);
                });
            }

            // download if no image or requested to refresh anyway, and download allowed by delegate
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;//低优先级
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;//渐进式下载
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;//使用NSURLCache
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;//后台下载
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;//使用cookies
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;//允许非法SSL
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;//高优先级
            //如果有图片,但是是需要更新缓存的类型
            if (image && options & SDWebImageRefreshCached) {
                // 因为有图片了,只是更新缓存,所以关掉渐进式下载(减去该枚举)
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // 忽略 NSURLCache的响应(加上该枚举)
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            
            /* ===============下载的Block============== */
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                //1.被取消
                if (!strongOperation || strongOperation.isCancelled) {
                    // Do nothing if the operation was cancelled
                    // See #699 for more details
                    // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
                }
                else if (error) {//2.有错误
                    dispatch_main_sync_safe(^{
                        if (strongOperation && !strongOperation.isCancelled) {
                            completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
                        }
                    });

                    if (   error.code != NSURLErrorNotConnectedToInternet//无连接
                        && error.code != NSURLErrorCancelled//连接取消
                        && error.code != NSURLErrorTimedOut//超出时间
                        && error.code != NSURLErrorInternationalRoamingOff//网络中断
                        && error.code != NSURLErrorDataNotAllowed//不允许数据
                        && error.code != NSURLErrorCannotFindHost//不能发现主地址
                        && error.code != NSURLErrorCannotConnectToHost) {//不能连接主地址
                        //如果不是以上特殊情况,就判定该url不可用
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {//3.正常下载完成
                    //如果是重试错误的url,就从失败的url数组中剔除该url
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    
                    //是否有缓存在磁盘
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    // 重新下载选项 && 缓存有图片 && 没有下载图片
                    if (options & SDWebImageRefreshCached && image && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    }
                    //如果有下载图片&&(不是gif)&&有响应图片转换的代理
                    else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            //获取变形(转换)后的图片
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            //如果变形后图片有值且下载完成
                            if (transformedImage && finished) {
                                //是否有变形
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                //将图片储存起来,根据是否转换来决定是否重新计算size
                                [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
                            }

                            //操作没取消的话就在主线程回调completedBlock
                            dispatch_main_sync_safe(^{
                                if (strongOperation && !strongOperation.isCancelled) {
                                    completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
                                }
                            });
                        });
                    }
                    else {//有下载图片
                        //缓存图片
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                        }
                        //操作没取消的话就在主线程回调completedBlock
                        dispatch_main_sync_safe(^{
                            if (strongOperation && !strongOperation.isCancelled) {
                                completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                            }
                        });
                    }
                }

                //如果下载完成,把下载操作从下载操作数组中剔除
                if (finished) {
                    @synchronized (self.runningOperations) {
                        if (strongOperation) {
                            [self.runningOperations removeObject:strongOperation];
                        }
                    }
                }
            }];

            /* ===============取消的Block============== */
            
            operation.cancelBlock = ^{
                [subOperation cancel];
                
                @synchronized (self.runningOperations) {
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    if (strongOperation) {
                        [self.runningOperations removeObject:strongOperation];
                    }
                }
            };
        }
#pragma mark - 如果缓存中有图片
        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 {
#pragma mark - 缓存没有图片,下载操作也不允许
            // 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];
            }
        }
    }];

    return operation;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容