SDWebImage源码解读

我今天又来读SDWebImage了。前几天读了MBProgressHUD,SDWebImage比MBProgressHUD要难,慢慢来吧。

UIImageView+WebCache

先用起来写个demo

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *urlStr = @"http://upload-images.jianshu.io/upload_images/276769-f7b02c377c44f9ea.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240";
    NSURL *url = [NSURL URLWithString:urlStr];
    [self.imageView sd_setImageWithURL:url placeholderImage:[UIImage imageNamed:@"button_downloadBgPaper_done"]];
}

点到sd_setImageWithURL里看看它怎么实现的

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
} 

发现好多头文件开放的接口最后都调用到最长的这个方法。为用户提供了多个选择

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

咱们直接进最长的这个方法吧,这个方法首先执行

    # //取消当前下载
    [self sd_cancelCurrentImageLoad];
  

先去sd_cancelCurrentImageLoad里看看

- (void)sd_cancelCurrentImageLoad {
    
# //    UIView+WebCacheOperation 分类中调用
    [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}

UIView+WebCacheOperation

这个分类的作用是为每个view(咱们一般都是UIImageview)绑定一个字典,每个字典里🈶️关于这个view下载的操作。也许有一个操作,也许有多个操作。方便拿到view正在进行的操作,取消操作。

#import <UIKit/UIKit.h>
#import "SDWebImageManager.h"

@interface UIView (WebCacheOperation)

/**
 *  Set the image load operation (storage in a UIView based dictionary)
 *  @param operation the operation
 *  @param key       key for storing the operation
 */
 # 设置图像加载操作(在基于UIView的字典中存储)
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key;

/**
 *  Cancel all operations for the current UIView and key
 *  @param key key for identifying the operations
 */
 # 用这个key找到当前UIView上面的所有操作并取消
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key;

/**
 *  @param key key for identifying the operations
 */
#  只需删除与当前UIView和key对应的操作,而不取消它们
- (void)sd_removeImageLoadOperationWithKey:(NSString *)key;

@end

UIView+WebCacheOperation.m文件内获取operationDictionary会用到runtime,为分类添加一个字典的属性。

static char loadOperationKey;
#pragma mark- 用runTime获取operationDictionary。
- (NSMutableDictionary *)operationDictionary {
    
# //    objc_getAssociatedObject(id object, const void *key)意思是通过这个key从这个object获取到属性
    NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
    if (operations) {
        return operations;
    }
    operations = [NSMutableDictionary dictionary];
#   policy:关联策略。有五种关联策略。
#   OBJC_ASSOCIATION_ASSIGN 等价于 @property(assign)。
#   OBJC_ASSOCIATION_RETAIN_NONATOMIC等价于 @property(strong, nonatomic)。
#   OBJC_ASSOCIATION_COPY_NONATOMIC等价于@property(copy, nonatomic)。
#   OBJC_ASSOCIATION_RETAIN等价于@property(strong,atomic)。
#   OBJC_ASSOCIATION_COPY等价于@property(copy, atomic)。
    objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return operations;
}

取消imageview上的所有的下载任务。

- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
    
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    id operations = [operationDictionary objectForKey:key];
    if (operations) {
# //        如果imageview上有多个任务的情况,从数组中挨个取消
        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];
    }
}

咱们继续这个最长的方法,研究下去

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
    
   #  //1.取消当前下载
    [self sd_cancelCurrentImageLoad];
    
# //   2.将url作为imageview的属性,每个imageview都绑定一个url
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
# //   3.位运算符,!(options & SDWebImageDelayPlaceholder)的意思是如果options不是SDWebImageDelayPlaceholder就给imageview赋值占位图。
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }
    
# //   4.如果url不是空的,那么就下载图片
    if (url)
    {

        # // 4.1 [self.imageView setShowActivityIndicatorView:YES];如果你想要有菊花,就通过这个方法,让sd给你设置。
        if ([self showActivityIndicatorView]) {
            # //将菊花加到imageview的中间,然后转。
            [self addActivityIndicator];
        }
        
       #  //4.2 下载图片
        __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) {
            
# //            4.3 移除菊花
            [wself removeActivityIndicator];
# //            4.4 如果imageview是nil的话,就不做操作了
            if (!wself) return;
            # //同步方法
            dispatch_main_sync_safe(^{
                if (!wself) return;
# //    4.5 如果image有值,options是SDWebImageAvoidAutoSetImage不让sd自动设置image就直接回调然后不做其他操作直接return。
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
# //              4.6 如果image有值那么就设置,并将其标记为需要重新布局
                else if (image) {
                    wself.image = image;
                    [wself setNeedsLayout];
# //              4.7 如果image为nil那么就设置占位图,前提是options是SDWebImageDelayPlaceholder,延迟设置占位图,并将其标  记为需要重新布局
                } else {
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
 #//              4.8 如果finished是yes,表明下载完成,就回调
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
# //      4.9为imageview绑定下载的操作,之前在1的时候就取消加在imagview上的操作。现在是把新的操作加上去
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    }
    
   #  //5.url是空的
    else
    {
        
        dispatch_main_async_safe(^{
        #     //5.1 移除菊花
            [self removeActivityIndicator];
         #    //5.2 如果完成的block不是nil,那么就回调
            if (completedBlock) {
                
           #      //5.3 创建error
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                
# //                5.4回调completedBlock给用户
# //                typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);
# //                SDImageCacheTypeNone,缓存的类型为None
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
        
    }
    
}

SDWebImageManager

作用:SDWebImageManager是UIImageView + WebCache分类后面真正操作的类,它将异步下载器(SDWebImageDownloader)与图像缓存存储(SDImageCache)绑定。您可以直接使用这个类来从缓存中(如果之前下载过)获取到image。

先看看这个SDWebImageOptions枚举

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    /**
    默认情况下,如果一个url在下载的时候失败了,那么这个url会被加入黑名单并且library不会尝试再次下载,这个flag会阻止library把失败的url加入黑名单(简单来说如果选择了这个flag,那么即使某个url下载失败了,sdwebimage还是会尝试再次下载他
     */
    SDWebImageRetryFailed = 1 << 0,

    /**
    默认情况下,图片会在交互发生的时候下载(例如你滑动tableview的时候),这个flag会禁止这个特性,导致的结果就是在scrollview减速的时候,才会开始下载(也就是你滑动的时候scrollview不下载,你手从屏幕上移走,scrollview开始减速的时候才会开始下载图片
     */
    SDWebImageLowPriority = 1 << 1,

    /**
     * 这个flag禁止磁盘缓存,只有内存缓存
     */
    SDWebImageCacheMemoryOnly = 1 << 2,

    /**
      默认情况下,图像只有在完全下载后才会显示
      这个flag会在图片下载的过程中就显示(就像你用浏览器浏览网页的时候那种图片下载,一截一截的显示)
     */
    SDWebImageProgressiveDownload = 1 << 3,

    /**
    一个图片缓存了,还是会重新请求.并且缓存侧略依据NSURLCache而不是SDWebImage.URL不变,图片会更新时使用,
     比如服务器给你同一个url,但是图片换了,用这种可以解决。每次都更新,不会用cache的
     */
    SDWebImageRefreshCached = 1 << 4,

    /**
     *启动后台下载,假如你进入一个页面,有一张图片正在下载这时候你让app进入后台,图片还是会继续下载(这个估计要开backgroundfetch才有用)
     */
    SDWebImageContinueInBackground = 1 << 5,

    /**
     * 可以控制存在NSHTTPCookieStore的cookies
     * NSMutableURLRequest.HTTPShouldHandleCookies = YES;
     */
    SDWebImageHandleCookies = 1 << 6,

    /**
     * 允许不安全的SSL证书,在正式环境中慎用
     */
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,

    /**
     默认情况下,image在装载的时候是按照他们在队列中的顺序装载的(就是先进先出).这个flag会把他们移动到队列的前端,并且立刻装载,而不是等到当前队列装载的时候再装载.
     */
    SDWebImageHighPriority = 1 << 8,
    
    /**
    :默认情况下,占位图会在图片下载的时候显示.这个flag开启会延迟占位图显示的时间,等到图片下载完成之后才会显示占位图.
     */
    SDWebImageDelayPlaceholder = 1 << 9,

    /**
    是否transform图片
     */
    SDWebImageTransformAnimatedImage = 1 << 10,
    
    /**
    默认情况下,image在下载后添加到imageView。 但是在某些情况下,我们希望在设置图像之前应用过滤器或使用交叉淡入淡出的动画添加)如果要在成功时手动设置完成的图像,请使用此标志
     */
    SDWebImageAvoidAutoSetImage = 1 << 11
};

看下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;
}

看SDWebImageManager里的下载图片的方法

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

内部的实现细节,判断completedBlock和url的合法性

 // 如果completedBlock为nil,那么就奔溃
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

//    非常常见的错误是使用NSString对象而不是NSURL发送URL。 由于某些奇怪的原因,XCode不会对此类型不匹配发出任何警告。 这里我们通过允许URL作为NSString传递来故障保护此错误
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // 阻止应用程序崩溃参数类型错误,如发送NSNull而不是NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

然后定义了一个局部变量operation,因为这个operation要在block修改,所以定义为__block类型
对block的讲解 http://www.jianshu.com/p/ce479906bc0a
__block修饰的局部变量在block里能被更改,原理就是block拿到的是局部变量的地址,是引用传递。不加__block的是值传递,所以更改不了局部变量。如果局部变量是对象,那么__block修饰后,block对它有强引用,有可能会发生循环引用问题,所以sd又用__weak修饰了下,防止循环引用。

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

SDWebImageCombinedOperation

SDWebImageCombinedOperation保存了缓存的操作cacheOperation,可以取消查询缓存任务。
还保存了取消下载的block。
并且SDWebImageCombineOperation遵循<SDWebImageOperation>协议,所以operation可以作为返回值返回,当调用了cancel方法,就同时取消查询缓存和下载的任务。可以说它是一个任务管理器。管理着缓存和下载操作。

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>

@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic) NSOperation *cacheOperation;

@end
@implementation SDWebImageCombinedOperation

- (void)setCancelBlock:(SDWebImageNoParamsBlock)cancelBlock {
    // 检查操作是否已被取消,然后我们调用cancelBlock
    if (self.isCancelled) {
        if (cancelBlock) {
            cancelBlock();
        }
        _cancelBlock = nil; // don't forget to nil the cancelBlock, otherwise we will get crashes
    } else {
        _cancelBlock = [cancelBlock copy];
    }
}

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

@end

继续往下说,看看url是不是在黑名单

    BOOL isFailedUrl = NO;
//    @synchronized() 的作用是创建一个互斥锁,保证在同一时间内没有其它线程对self.failedURLs对象进行修改,起到线程的保护作用
//    failedURLs是一个集合,存放下载失败的url,不用数组是因为NSSet里面不含有重复的元素,同一个下载失败的url只会存在一个
    @synchronized (self.failedURLs) {
//        如果这个集合里有当前的url,那么isFailedUrl是true。说明黑名单里有这个url。
        isFailedUrl = [self.failedURLs containsObject:url];
    }
//  如果url长度是0,options不是SDWebImageRetryFailed(下载失败了但是继续下载),isFailedUrl是true,返回error直接。
    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;
    }

把operation加入到self.runningOperations的数组里面
通过获取url的字符串作为缓存的key。

//    把operation加入到self.runningOperations的数组里面
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
//    通过获取url的字符串作为缓存的key。
    NSString *key = [self cacheKeyForURL:url];

- (NSString *)cacheKeyForURL:(NSURL *)url {
    if (!url) {
        return @"";
    }
//    如果实现了self.cacheKeyFilter,那就用自己实现的规则。如下面的demo
//    [[SDWebImageManager sharedManager] setCacheKeyFilter:^(NSURL *url) {
//        url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
//        return [url absoluteString];
//    }];
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    } else {
        return [url absoluteString];
    }
}

拿到缓存的key之后,然后咱们就去缓存里查查是不是已经下载了这张图片。

    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
       
//       回调查询的结果,SD在这个block里写了好多的代码,咱们一会说。
    }];

看看如何查询的queryDiskCacheForKey
NSCache不了解可以看这个文章http://www.jianshu.com/p/47400383dfe0

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
//    1. 如果doneBlock是nil,那么就不查寻了。因为没人用查询的结果
    if (!doneBlock) {
        return nil;
    }
//2. 如果key是nil,那么返回没有查询到的结果
    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }
 
//  3. 先去内存里查找是不是有这张图片,如果有就直接返回block。
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }
//  4. 如果内存里没有找到,那么就去磁盘里找,如果找到了,先放入内存的缓存中,然后回调doneBlock
//  4.1 创建一个任务
    NSOperation *operation = [NSOperation new];
    
//  4.2 在串行队列里异步查询磁盘的图片
//  @property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;   #define SDDispatchQueueSetterSementics strong
//  创建一个串行队列,一个一个的查
//  _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
    dispatch_async(self.ioQueue, ^{
//        如果operation被取消那么不查了
        if (operation.isCancelled) {
            return;
        }
//  创建自动释放池,及时释放局部对象。防止内存爆
        @autoreleasepool {
//            通过key去找image
            UIImage *diskImage = [self diskImageForKey:key];
//            如果找到了就写到内存中, (self.shouldCacheImagesInMemory == use memory cache [defaults to YES])
            if (diskImage && self.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
//          @property (strong, nonatomic) NSCache *memCache;  NSCache不了解可以看这个文章http://www.jianshu.com/p/47400383dfe0
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
//          回到主线程回调
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

咱们这次看下queryDiskCacheForKey的block里的内容。咱们去缓存里看了有没有这个图片,然后回调后,接下来sd通过返回的查询结果,来看看是用缓存的图片,还是下载图片。我把SDWebImageManager里的这个- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;都贴出来吧,整体看看。

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
    // 如果completedBlock为nil,那么就奔溃
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

//    非常常见的错误是使用NSString对象而不是NSURL发送URL。 由于某些奇怪的原因,XCode不会对此类型不匹配发出任何警告。 这里我们通过允许URL作为NSString传递来故障保护此错误
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // 阻止应用程序崩溃参数类型错误,如发送NSNull而不是NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
//  http://www.jianshu.com/p/14efa33b3562对block的讲解
//  __block修饰的局部变量在block里能被更改,原理就是block拿到的是局部变量的地址,是引用传递。不加__block的是值传递,所以更改不了局部变量。如果局部变量是对象,那么__block修饰后,block对它有强引用,有可能会发生循环引用问题,所以sd又用__weak修饰了下,防止循环引用。
    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    
    BOOL isFailedUrl = NO;
//    @synchronized() 的作用是创建一个互斥锁,保证在同一时间内没有其它线程对self.failedURLs对象进行修改,起到线程的保护作用
//    failedURLs是一个集合,存放下载失败的url,不用数组是因为NSSet里面不含有重复的元素,同一个下载失败的url只会存在一个
    @synchronized (self.failedURLs) {
//        如果这个集合里有当前的url,那么isFailedUrl是true。说明黑名单里有这个url。
        isFailedUrl = [self.failedURLs containsObject:url];
    }
//  如果url长度是0,options不是SDWebImageRetryFailed(下载失败了但是继续下载),isFailedUrl是true,返回error直接。
    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;
    }

//    把operation加入到self.runningOperations的数组里面
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
//    通过获取url的字符串作为缓存的key。
    NSString *key = [self cacheKeyForURL:url];
    
//    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
//       
////       回调查询的结果
//    }];

    
    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
        
//        1.如果外部把operation给取消了,那么就返回
        if (operation.isCancelled) {
    
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }

            return;
        }

//       2. (!image || options & SDWebImageRefreshCached)  如果没有找到图片,或者是options是SDWebImageRefreshCached,都需要去下载图片
        
//        (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]) 代理允许下载,SDWebImageManagerDelegate的delegate不能响应imageManager:shouldDownloadImageForURL:方法或者能响应方法且方法返回值为YES.也就是没有实现这个方法就是允许的,如果实现了的话,返回YES才是允许
        
        if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            
//            如果缓存里有图片,但是options是SDWebImageRefreshCached的话,先在主线程完成一次回调,使用的是缓存中找的图片
            if (image && options & SDWebImageRefreshCached) {
                dispatch_main_sync_safe(^{
                    //如果缓存中找到了图像,但提供了SDWebImageRefreshCached,请通知有关缓存的图像
                    //并尝试重新下载它,以让NSURLCache有机会从服务器刷新它。
                    completedBlock(image, nil, cacheType, YES, url);
                });
            }

//          如果没有在缓存中找到图片,或者是options是SDWebImageRefreshCached,仍需要下载图片
            
//          通过options来确定downloaderOptions
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            
            if (image && options & SDWebImageRefreshCached) {
                //  如果image已经被缓存但是设置了需要请求服务器刷新的选项,强制关闭渐进式选项
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // 如果image已经被缓存但是设置了需要请求服务器刷新的选项,忽略从NSURLCache读取的image
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            
//          通过self.imageDownloader去下载图片
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                
//              强引用operation防止释放
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                
                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
//                    如果我们调用completedBlock,这个block会和另外一个completedBlock争夺一个对象,因此这个block被调用后会覆盖新的数据
                }
                
                else if (error) {
//                  如果下载出错了,就回调错误,
                    dispatch_main_sync_safe(^{
                        if (strongOperation && !strongOperation.isCancelled) {
                            completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
                        }
                    });
//                  将下载失败的url添加到failedURLs中
                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
//                  如果设置了下载失败后重试。将url从failedURLs移除,再次下载
                    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
//                       图片刷新遇到了NSSURLCache中有缓存的状况,不调用完成回调
                    }
//                  如果图片下载成功并且设置了需要变形Image的选项且变形的代理方法已经实现
                    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), ^{
//                          获取变形后的image
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
//                              将变形的image缓存起来
                                [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
                            }
//                          主线程回调变形的image
                            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];
                        }
//                      回调
                        dispatch_main_sync_safe(^{
                            if (strongOperation && !strongOperation.isCancelled) {
                                completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                            }
                        });
                    }
                }
//              从正在进行runningOperations中移除当前的任务
                if (finished) {
                    @synchronized (self.runningOperations) {
                        if (strongOperation) {
                            [self.runningOperations removeObject:strongOperation];
                        }
                    }
                }
            }];
            
//          为operation的取消任务的block赋值,如果调用了operation的cancel方法,那么就取消下载的任务,从runningOperations移除当前任务
            operation.cancelBlock = ^{
                [subOperation cancel];
                
                @synchronized (self.runningOperations) {
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    if (strongOperation) {
                        [self.runningOperations removeObject:strongOperation];
                    }
                }
            };
        }
//      3. 如果在缓存里找到了图片,代理允许下载 或者没有设置SDWebImageRefreshCached选项
        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];
            }
        }
//     4. 如果在缓存里没有找到图片,代理不允许下载,直接回调。
        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];
            }
        }
    }];
    
    

    return operation;
}

这个方法是核心呀。
1.首先创建一个SDWebImageCombinedOperation的operation来管理缓存任务和下载任务。

2.然后去缓存里找这个图片,- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock。

  1. 利用查找的结果来看,是否要下载图片,还是利用缓存。不过中间有好多的options,所以判断了很多情况,大体就是下载和缓存。

SDWebImageDownloader

SDWebImageManager用的下载的类就是SDWebImageDownloader。
SDWebImageDownloader 负责的是下载图片。
SDWebImageDownloaderOperation 负责的是执行下载的任务。

在上一篇中SDWebImageManager 通过调用下面的方法去下载图片

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock

这个方法就是SDWebImageDownloader的,所以我们走进SDWebImageDownloader去看看吧。

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    
    //这个属于默认的使用模式了,前往下载,返回进度block信息,完成时调用completedBlock
    SDWebImageDownloaderLowPriority = 1 << 0,
    
    //渐进式下载 ,如果设置了这个选项,会在下载过程中,每次接收到一段返回数据就会调用一次完成回调,回调中的image参数为未下载完成的部分图像,可以实现将图片一点点显示出来的功能
    SDWebImageDownloaderProgressiveDownload = 1 << 1,
    
    /**
     * 通常情况下request阻止使用NSURLCache.这个选项会默认使用NSURLCache
     */
    SDWebImageDownloaderUseNSURLCache = 1 << 2,
    
    /**
     *  如果从NSURLCache中读取图片,会在调用完成block的时候,传递空的image或者imageData
     */
    
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    /**
     * 系统为iOS 4+时候,如果应用进入后台,继续下载.这个选项是为了实现在后台申请额外的时间来完成请求.如果后台任务到期,操作也会被取消
     */
    
    SDWebImageDownloaderContinueInBackground = 1 << 4,
    
    /**
     *  通过设置 NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式来处理存储在NSHTTPCookieStore的cookies
     */
    SDWebImageDownloaderHandleCookies = 1 << 5,
    
    /**
     *  允许不受信任的SSL证书,在测试环境中很有用,在生产环境中要谨慎使用
     */
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
    
    /**
     * 将图片下载放到高优先级队列中
     */
    SDWebImageDownloaderHighPriority = 1 << 7,
};

一些属性

/**
 * 解压缩已下载和缓存的图像可以提高性能,但可以消耗大量内存。
   *默认为YES。 如果由于内存消耗过多而遇到崩溃,请将其设置为NO。
 */
@property (assign, nonatomic) BOOL shouldDecompressImages;

//最大的并发数,同时下载几个图片
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;

/**
 * 当前在下载队列的操作总数
 */
@property (readonly, nonatomic) NSUInteger currentDownloadCount;


/**
 *  下载操作的超时时间,默认是15s
 */
@property (assign, nonatomic) NSTimeInterval downloadTimeout;


/**
 * 枚举类型,代表着操作下载的顺序, SDWebImageDownloaderFIFOExecutionOrder,默认值。 所有下载操作将以队列样式(先进先出)执行,
  SDWebImageDownloaderLIFOExecutionOrder 所有下载操作将以堆栈样式(后进先出)执行
 */
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;

/**
   SDWeImageDownloder是一个单例
 */
+ (SDWebImageDownloader *)sharedDownloader;

/**
 *  为request操作设置默认的URL凭据,具体实施为:在将操作添加到队列之前,将操作的credential属性值设置为urlCredential.
 */
@property (strong, nonatomic) NSURLCredential *urlCredential;

/**
 * Set username
 */
@property (strong, nonatomic) NSString *username;

/**
 * Set password
 */
@property (strong, nonatomic) NSString *password;

/**
 * Set filter to pick headers for downloading image HTTP request.
 * 设置一个过滤器,为下载图片的HTTP request选取header.意味着最终使用的headers是经过这个block过滤之后的返回值。
 * This block will be invoked for each downloading image request, returned
 * NSDictionary will be used as headers in corresponding HTTP request.
 */
@property (nonatomic, copy) SDWebImageDownloaderHeadersFilterBlock headersFilter;

这次看下载的方法

- (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:^{

    //这里面都是创建下载的回调
}];
}

先看看addProgressCallback这个方法,这个方法涉及到gcd知识看我这个文章http://www.jianshu.com/p/ede9f401dc80
dispatch_barrier_sync和dispatch_barrier_async看http://blog.csdn.net/u013046795/article/details/47057585

self.URLCallbacks是一个可变字典,key是url,value是一个可变数组callbacksForURL。callbacksForURL的元素是一个可变字典callbacks,这个callbacks里放着下载的两个block,一个是进度的block,一个是完成的block。

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
//    如果图片的url是空的就直接返回
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }
//  self.barrierQueue是一个并发队列 _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
//  一个执行完了,之后执行另一个。
    dispatch_barrier_sync(self.barrierQueue, ^{
        
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // Handle single download of simultaneous download request for the same URL
//        处理同一下载请求的单一下载相同的URL
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;

        if (first) {
            createCallback();
        }
    });
}

如果url第一次绑定它的回调,也就是第一次使用这个url创建下载任务则执行一次创建回调
在创建回调中 创建下载操作(下载操作并不是在这里创建的),dispatch_barrier_sync执行确保同一时间只有一个线程操作URLCallbacks属性,也就是确保了下面创建过程中在给operation传递回调的时候能取到正确的self.URLCallbacks[url]值,同事确保后面有相同的url再次创建的时候if (!self.URLCallbacks[url])分支不再进入,first==NO,也就不再继续调用创建回调,这样就确保了同一个url对应的图片不会重复下载

然后把整体的下载方法放出来

#pragma mark- 下载图片
- (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;
        }

        //如果options是SDWebImageDownloaderUseNSURLCache,那么就用NSURLCache,默认是没有NSURLCache缓存的,只有SDImageCache缓存
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        
//        通过设置 NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式来处理存储在NSHTTPCookieStore的cookies
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (wself.headersFilter) {
            request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = wself.HTTPHeaders;
        }
        
        // 创建SDWebImageDownLoaderOperation操作对象(下载的操作就是在SDWebImageDownLoaderOperation类里面进行的)
//        传入了进度回调,完成回调,取消回调
        operation = [[wself.operationClass alloc] initWithRequest:request
                                                        inSession:self.session
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                             
                          //progress block回调的操作,跟之前的一样,再倒回来
                         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) {
                        
                       //completed block 回调的操作
                        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:^{
//                      取消的回调
                        SDWebImageDownloader *sself = wself;
                        if (!sself) return;
                        dispatch_barrier_async(sself.barrierQueue, ^{
                            [sself.URLCallbacks removeObjectForKey:url];
                        });
 }];
//      上面的就是创建了个SDWebImageDownloaderOperation。
//       设置是否需要解压
        operation.shouldDecompressImages = wself.shouldDecompressImages;
//      配置operation
        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;
        }
//      将operation添加到队列里。
        [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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容