阅读源代码:SDWebImage 3.8.2

简单的介绍

SDWebImage 提供图片的异步下载和缓存,对外通过 categories 封装 UIImageViewUIButtonMKAnnotationView 的接口供使用者使用。
SDWebImage 的磁盘缓存有对有效期和最大容量的限制处理,内存缓存在系统报内存警告的时候会清除。它还保障几个相同的 URL 不会被重复下载,不可用的 URL 不会一次次的被重试,主线程绝不会被阻塞。

直观的感受

SDWebImage 的接口简单易用,开发者一句代码就能使用,代码: - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; ,复杂的逻辑和实现都被隐藏在这句代码的后面。
各模块独立,Cache缓存模块提供库的内存缓存和可选的磁盘缓存支持,Downloader下载模块提供基于 NSURLSessionNSOperation 的下载器。SDWebImageManager 则将 Cache 和 Downloader 两个模块很好的整合在一起,提供基于缓存的图片加载功能。最后是使用 categories 封装了常用 UI 控件的接口。
GitHub 上拥有万的 star,无数的使用者,在项目中久经考验,是程序猿学习的好材料。

从‘头’开始

这个头就是最常用的类 UIImageView+WebCache ,类里面暴露了很多加载图片的接口:

- (void)sd_setImageWithURL:(NSURL *)url;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
...

但是归根到底都是调用的这句,我们来看这里面的代码。

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

代码的第一句是

[self sd_cancelCurrentImageLoad];

看方法名就知道它的作用,就是取消这个视图 ImageView 正在加载图片的操作,如果这个 ImageView 正在加载图片,保障在开始新的加载图片任务之前,取消掉正在进行的加载操作。

看下具体的实现代码 UIImageView+WebCache

- (void)sd_cancelCurrentImageLoad {
    [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
- (void)sd_cancelCurrentAnimationImagesLoad {
    [self sd_cancelImageLoadOperationWithKey:@"UIImageViewAnimationImages"];
}

两个 key 说明有两个不一样的加载方式,一个是单张图片的,另一个是连续下载多张,放到 NSArray<UIImage *> *animationImages 中。

看下取消操作的代码实现,UIView (WebCacheOperation):

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

- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
    [self sd_cancelImageLoadOperationWithKey:key];
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    [operationDictionary setObject:operation forKey:key];
}
- (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];
    }
}

代码中,通过 objc_setAssociatedObject 关联对象的方法,给 UIImageView 动态添加了一个 NSMutableDictionary 的属性。通过 key-value 维护这个 ImageView 已经有了哪些下载操作,如果是数组就是 UIImageViewAnimationImages 否则就是 UIImageViewImageLoad 。最后获得的都是遵从了 <SDWebImageOperation> 协议的对象,可以统一调用定义好的方法 cancel,达到取消下载操作的目的,如果 operation 都被取消了,则删除对应 key 的值。

继续看 - (void)sd_setImageWithURL: placeholderImage: options: ; 里的代码

if (!(options & SDWebImageDelayPlaceholder)) {
    dispatch_main_async_safe(^{
        self.image = placeholder;
    });
}

如果加载图片的选项不是 SDWebImageDelayPlaceholder 则会在主线程中先设置 placeholder 的占位图,

SDWebImageDelayPlaceholder 的情况后面说。dispatch_main_async_safe 是一个宏定义,我们可以参考这种写法。

#define dispatch_main_sync_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_sync(dispatch_get_main_queue(), block);\
    }

看起来很简洁。

下面这段就是这个类中比较关键的代码了,

// check if activityView is enabled or not
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);
        }
    });
}];
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

先检查 activityView 是否可用,可用的话给 ImageView 正中间添加一个活动指示器,并旋转,加载图片完成或失败都会清除掉。__weak __typeof(self)wself = self; 避免循环引用,接下来就是调用 SDWebImageManager 的方法 downloadImageWithURL: options: progress: completed: ,在该方法的 completed block 回调中,如果 option 是 SDWebImageAvoidAutoSetImage ,就是要求不要给 ImageView 自动设置图片,则只回调 completedBlock 然后 return,否则有 image 就设置给 ImageView 。没有 image 通常就是错误情况,如果 option 是 SDWebImageDelayPlaceholder 则设置占位图(可以设置成提示用户图片没加载出来的图片),最后回调 completedBlock。上面代码最后一句是把这个 operation 存到 ImageView 的 NSMutableDictionary 中,为了之前提到的 [self sd_cancelCurrentImageLoad]; 操作准备的。

UIImageView+WebCache 类中的代码还是很容易的,逻辑很清晰,没有难懂的地方。接下来我们去看 SDWebImageManager 的核心方法:

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

Cache 和 Downloader 的管理者 SDWebImageManager

我们看这个方法前几句:

// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, XCode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}

// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}

注释中说 NSString 替换 NSURL 做为参数传进来是很常见的错误,但奇怪的是 XCode 没有类型错误的警告。这个我试过是有警告提示的,可能更早一些的 XCode 版本是这样的。这几句就是参数的校验,没什么好说的。

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

这个类的作用是管理多个模块的取消操作,具体是怎么实现的,后面的代码会提到,__weak 修饰是为了防止循环引用。

下面就是文章最开始提到的功能之一,不可用的 URL 不会一次次重试的功能实现:

BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
    isFailedUrl = [self.failedURLs containsObject:url];
}

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

在 downloader 的下载方法的 completedBlock 中会将下载失败的 URL ,维护到 Set 集合中(黑名单),代码会在后面提到。这段代码的意思是如果发现 url 长度为 0 ,或者是下载失败过的 url ,且没有要求重试则直接创建 NSError 并 回调 completedBlock 。

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

manager 维护了一个数组 self.runningOperations ,将所有操作放进去,便于管理。(比如统一调用 cancel )

下面是比较核心的代码:

NSString *key = [self cacheKeyForURL:url];
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {...}

通过 url 获取用来缓存的 key,尝试去缓存中取图片。queryDiskCacheForKey: done: 方法返回了一个 NSOperation 对象并赋值给了 SDWebImageCombinedOperationcacheOperation ,这个类(SDWebImageCombinedOperation)中还有一个属性 cancelBlock 也会包括一些取消操作。它还实现了协议 <SDWebImageOperation> ,这个协议里只需要实现一个方法,就是 - (void)cancel;

@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end

SDWebImageCombinedOperation 的实现类中,实现了这个 cancel 方法,并调用了这些取消操作。

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

这样,调用者只要调用 operation 的 cancel() ,就可以统一对多个模块类做取消操作。

然后看下查询缓存的方法 - (NSOperation *)queryDiskCacheForKey: done: 的实现代码:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }

    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // First check the in-memory cache...
    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 {
            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;
}

会先到内存缓存中去查找,如果命中则直接回调,没有命中继续在磁盘缓存中查找。查找任务是异步的,在一个串行队列中,先生成一个 NSOperation 对象返回,也就是赋值给了 operation.cacheOperation 。在查找任务中,会先检测这个 operation 有没有被取消,如果取消则直接 return,这就实现了 SDWebImageCombinedOperation 可以取消查找缓存的操作。之后的代码就是去磁盘缓存中查找图片,且如果需要内存缓存就存进去,最后回调 doneBlock。这段代码被放入到了 @autoreleasepool 中包裹起来,是因为查找出来的图片可能会比较大,占用较多的内存,保障能够及时的回收它。关于 @releasepool 的原理参考这篇文章

现在回到 SDWebImageManager 中继续看,在 queryDiskCacheForKey: 的 doneBlock 中,

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

如果操作被取消,则删除掉 self.runningOperations 的操作,然后 return。

接下来会有三个条件分支,我们一个个来看,第一个是:

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

image 为空意味着缓存没有命中,SDWebImageRefreshCached 则是就算缓存命中也要下载图片更新缓存,SDWebImageManager 这个类还定义了一个协议并实现一个代理。

@protocol SDWebImageManagerDelegate <NSObject>
@optional
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
@end

第一个方法的意思是,是否下载图片,YES 就是下载,NO 就是不下载。第二个方法是,对下载好的图片 image 做 transform 处理,比如可以改成圆角图片等,然后返回,这样缓存的图片也会是 transform 之后的图片。
理解了代理方法的意思就可以理解这个条件了,如果缓存没有命中,或需要刷新已有缓存 且 没有实现 imageManager:shouldDownloadImageForURL 的方法(默认是 YES,可以下载图片)则去下载图片。如果实现了这个代理方法返回的是 YES,也会去下载图片。

看第一个条件里的代码,首先:

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

缓存如果命中,且需要更新缓存,则先将缓存图片通过 completedBlock 回调出去,在继续下载图片。

在往下就是缓存没有命中或需要更新缓存的情况,所以需要下载图片,但之前先将 SDWebImageManager 里 option 的条件映射成 SDWebImageDownloder 里的 option 的条件,下载使用的方法是 SDWebImageDownloder 里的 - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock; 方法。具体实现我们在分析 SDWebImageDownloder 时会说,先看 completedBlock 里的逻辑。

__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
}

这里注意避免循环引用,imageDownloaderSDWebImageManager 强引用,downloadImageWithURL 的 completedBlock 会被 imageDownloader 的属性 URLCallbacks 数组强引用保存起来,至于为什么这么做后面会讲到。

然后是发生错误的处理情况:

else if (error) {
    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) {
        @synchronized (self.failedURLs) {
            [self.failedURLs addObject:url];
        }
    }
}

发生错误,并回调。将在确定条件下失败的 URL 放入黑名单,不会反复请求。(通常是 URL 的问题,而不是网络问题)

看下 else 之后的代码,稍长一些:

else {
    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
    }
    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];
                [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
            }

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

如果 option 是 SDWebImageRetryFailed 则这个 url 从黑名单中删除,给它一个重试的机会。有 downloadedImage 说明图片下载成功,如果是要进行 transform ,则调用 delegate 方法,获取 transform 之后的图片,进行缓存,再调用 completedBlock 。如果不需要 transform 则直接缓存后回调 completedBlock。

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

这是下载图片的取消操作,调用 NSOperation 的 cancel,从 self.runningOperations 中删除 operation。赋值给 cancelBlock ,交给 SDWebImageCombinedOperation 对象管理。

看前面提到的三个分支条件的第二个:

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

有 image 说明缓存命中,且没有要重下图片的情况,则直接回调 completedBlock 就可以了。

第三个分支条件,它的意思是既没有缓存图片,代理 delegate 也不允许下载图片,那就只能直接回调 completedBlock ,图片参数传 nil 了。

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

到此,SDWebImageManager 的核心方法:

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

就介绍完了。

图片下载器 SDWebImageDownloader

我们还是从 SDWebImageDownloader 的核心方法入手:

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

该方法的大部分代码都放到了一个 createCallback 的回调中:

- (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:^{
        ...
    }];

    return operation;
}

那我们只能先去探究下 - (void)addProgressCallback: completedBlock: forURL: 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.
if (url == nil) {
    if (completedBlock != nil) {
        completedBlock(nil, nil, nil, NO);
    }
    return;
}

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
    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 参数不能为空,因为它要作为存储 callbacks 字典的 key,如果它为 nil 则会马上调用 completed block 返回 nil 图片和 nil 数据。
self.URLCallbacks 是一个 NSMutableDictionary ,它以 URL 作为 key ,维护一个可变数组 callbacksForURL,这个数组里又会存放一个一个的 NSMutableDictionary 用来存储两个 callback 回调方法,分别是以 kProgressCallbackKey 为 key 的 progressBlock 和 以 kCompletedCallbackKey 为 key 的 completedBlock 。代码里还有一个 BOOL first 的变量,如果发现 self.URLCallbacks 中没有这个 URL 的回调数组,那这个 URL 此时就是第一次请求(此时没有相同的 URL 在请求),会调用 createCallback(); 来创建下载的操作,而发现 self.URLCallbacks 中有这个 URL 的回调数组的话,则将对应的那两个回调方法存进 NSMutableDictionary ,在放到之前的回调数组中,且不会再调用 createCallback(); ,这样相同的 URL 不会重复请求下载。当第一个请求下载成功之后,会遍历这个回调数组,将数组里所有的 callback 都执行一遍。这么做的目的就是防止同时有多个相同 URL 的请求发生。
这段代码使用 dispatch_barrier_sync 将任务放入一个并发队列,目的是在并发队列中,这个任务执行时,不允许别的任务同时执行。因为 downloadImageWithURL: 方法要返回一个遵从<SDWebImageOperation> 的对象,所以要同步执行而不能异步。

现在就可以看一看 createCallback(); 中到底都做了什么。

NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
    timeoutInterval = 15.0;
}

// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
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;
}
operation = [[wself.operationClass alloc] initWithRequest:request
                                                inSession:self.session
                                                  options:options
                                                 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                     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) {
                                                    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];
                                                    });
                                                }];
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;
}

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

先创建了 NSMutableURLRequest 请求对象,然后调用 SDWebImageDownloaderOperation (继承 NSOperation)的方法:

- (id)initWithRequest:(NSURLRequest *)request
            inSession:(NSURLSession *)session
              options:(SDWebImageDownloaderOptions)options
             progress:(SDWebImageDownloaderProgressBlock)progressBlock
            completed:(SDWebImageDownloaderCompletedBlock)completedBlock
            cancelled:(SDWebImageNoParamsBlock)cancelBlock;

在 progressBlock 的回调方法里,会通过 sself.URLCallbacks 取出这个 URL 所有 kProgressCallbackKey 的回调方法,并将获取到的 receivedSizeexpectedSize 的值传入这些方法中调用。
在 completedBlock 的回调方法里和 progressBlock 中的一样,取出 kCompletedCallbackKey 对应的回调方法,将获取到的 image ,data,error,finished 的值传入方法中调用,还会删除 sself.URLCallbacks 中这个 URL 的回调数组 ,保障这个 URL 下次可以重新创建新请求。
在 cancelBlock 中则只是删除掉 sself.URLCallbacks 中这个 URL 的回调数组。
在往下是,给 operation 设置是否应该解压图片的属性,解压图片会提高下载和缓存的性能,但是会消耗较多的内存,如果程序因为占用内存过多而闪退则要把这个属性设置成 NO。
设置 operation 请求的 NSURLCredential ,用于在请求过程中,服务端要求验证客户端的凭证 - (void)URLSession: task: didReceiveChallenge: completionHandler:
再往后,是设置 NSOperation 的操作优先级。[wself.downloadQueue addOperation:operation]; 是将操作任务加到 NSOperationQueue 队列中,开始任务。最后是设置操作的执行顺序,默认是 FIFO 的先进先出的模式,也可以改成 LIFO 后进先出的栈模式,实现的方法就是添加依赖,前面的操作依赖后面的操作。设置完之后,则 return 这个 operation。
到此,SDWebImageDownloader 的这个核心方法就介绍完了。
还有一点,下载的请求是使用的 NSURLSessionSDWebImageDownloaderNSURLSession 的 delegate 设置成自己,统一接收这些回调方法。在这些回调方法中,会返回一个 NSURLSessionDataTask 通过这个 dataTask 的 taskIdentifier ,我们就可以在 self.downloadQueue.operations 中找到回调方法对应的 operation (SDWebImageDownloaderOperation),每个 operation 中都有这些代理方法,这样在 SDWebImageDownloader 统一接收的回调中用找到的 operation 调用当前的这个代理方法,把参数传到对应的 operation 中。
代码如下:

- (SDWebImageDownloaderOperation *)operationWithTask:(NSURLSessionTask *)task {
    SDWebImageDownloaderOperation *returnOperation = nil;
    for (SDWebImageDownloaderOperation *operation in self.downloadQueue.operations) {
        if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
            returnOperation = operation;
            break;
        }
    }
    return returnOperation;
}
#pragma mark NSURLSessionDataDelegate

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];

    [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
...

SDWebImageDownloaderOperation- (id)initWithRequest:(NSURLRequest *)request inSession: options: progress: completed: cancelled:; 是下载图片的关键代码,下面就来看下 SDWebImageDownloaderOperation 这个类。

操作单元 SDWebImageDownloaderOperation

SDWebImageDownloaderOperation 继承自 NSOperation ,并且实现了 <SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate> 这三个协议。继承 NSOperation 的子类执行任务的代码都写在 - (void)start; 或者 - (void)main; 中,我们就从 SDWebImageDownloaderOperation 重写的 - (void)start; 方法入手。

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif

这段代码的意思是,如果程序进入后台会给程序一段时间,完成未完成的任务,如果时间到了任务还是没有完成则取消这个任务。

NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    sessionConfig.timeoutIntervalForRequest = 15;
    
    /**
     *  Create the session for this task
     *  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
     *  method calls and completion handler calls.
     */
    self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                      delegate:self
                                                 delegateQueue:nil];
    session = self.ownedSession;
}

self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
self.thread = [NSThread currentThread];

self.unownedSession 是从 SDWebImageDownloader 中传进来的,而如果没有传进来 self.unownedSession 则自己创建一个 self.ownedSession,这个 self.ownedSession 设置的代理是自己,回调的代理方法直接调用这个类里的, 而self.unownedSession 传进来的这种,代理方法就是通过上面介绍过的方式调用到这个类的。上面注释的意思是为 task 创建一个 session,delegateQueue 中传入一个 nil,这样 session 就会创建一个串行的操作队列来执行所有的代理方法和完成处理的调用。
继续看代码:

[self.dataTask resume];

if (self.dataTask) {
    if (self.progressBlock) {
        self.progressBlock(0, NSURLResponseUnknownLength);
    }
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
    });
}
else {
    if (self.completedBlock) {
        self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
    }
}

resume 开启这个任务,调用下 self.progressBlock 传入初始的值,然后在主线程发送一个开始下载的通知,如果没有 self.dataTask 则调用 self.completedBlock 返回一个 NSError

下面我们在简单说下 SDWebImageDownloaderOperation 类中这两个 NSURLSessionTaskDelegateNSURLSessionDataDelegate 协议的代理方法。

#pragma mark NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler

在这个方法里检查下 response 的状态码,不正确的话取消任务,completedBlock 回调 NSError 。正确的话,获取下载数据的总大小 expectedContentLength ,并调用 self.progressBlock 。还会创建保存数据流的 NSMutableData 对象,self.imageData = [[NSMutableData alloc] initWithCapacity:expected];

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data

将每次接收到的数据 data 拼接到之前创建好的 self.imageData 中去,[self.imageData appendData:data]; 。如果 option 的要求是 SDWebImageDownloaderProgressiveDownload 则在这里把已有的数据 self.imageData 转成 image ,通过 self.completedBlock 回调出去,注意 finished 参数是 NO。

if (self.progressBlock) {
    self.progressBlock(self.imageData.length, self.expectedSize);
}

调用 self.progressBlock 将下载进度回调出去。

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;

请求完成的回调方法,有 error 则 self.completedBlock(nil, nil, error, YES); ,没有则将 self.imageData 转成 image 回调出去 completionBlock(image, self.imageData, nil, YES); ,当然这里面涉及很多处理的细节和其他情况的判断,就先不说了。

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler;

这段则是针对不同的鉴定场景返回不一样的策略。

至此,SDWebImageDownloaderOperation 中的这些代理方法就简单的介绍完了。

图片缓存 SDImageCache

SDImageCache 包括内存缓存和磁盘缓存,内存缓存使用的是继承自 NSCacheAutoPurgeCache ,而磁盘缓存就是基于文件的读写。
先查看 SDImageCache 的接口,看下都包括哪些功能,然后一一讲解代码。
存储的功能:

- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key;

这四个方法的前两个直接调用的第三个,所以我们从第三个方法入手。
看代码:

// if memory cache is enabled
if (self.shouldCacheImagesInMemory) {
    NSUInteger cost = SDCacheCostForImage(image);
    [self.memCache setObject:image forKey:key cost:cost];
}

如果内存缓存可用,就将图片通过 NSCache 的接口 - (void)setObject: forKey: cost: ; 存入。计算 cost 的方法是:

FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
    return image.size.height * image.size.width * image.scale * image.scale;
}

也就是一张图片的像素数量。
如果需要存入磁盘,一般情况下我们是将 imageData 直接存入的,但是如果 recalculate 的值是 YES ,或者没有 imageData,那我们就需要将 image 转成 NSData 存入磁盘。具体的实现是判断这个 image 有没有透明通道或者它的前八个字节是不是规定的 PNG 那固定的八个字节,如果是则就调用 UIImagePNGRepresentation 方法转成 NSData ,如果不是那就调用 UIImageJPEGRepresentation 这个方法。有了 data 之后,就要调用那四个存储方法的第四个 storeImageDataToDisk
通过 key 和 _diskCachePath 得到缓存文件的具体路径,在使用 NSFileManager- (BOOL)createFileAtPath: contents: attributes: ; 方法,将数据写入磁盘中。

// disable iCloud backup
    if (self.shouldDisableiCloud) {
        [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
    }

这段代码是避免该文件被 iCloud 备份。
这些读写操作都放到了 SDImageCache 的一个串行队列中,ioQueue 。我觉得是因为 _fileManager 是自己创建的:

dispatch_sync(_ioQueue, ^{
    _fileManager = [NSFileManager new];
});

是为了保障它的线程安全,在 SDImageCache 这个类的所有文件读写操作,都会放到 ioQueue 这个队列执行。而 [NSFileManager defaultManager] 是系统提供,本身就是线程安全的。

查询的功能:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;

第一个查询方法,在讲 SDWebImageManager 时已经讲过了。
第二个方法,就是调用的 NSCache 中的 - (nullable ObjectType)objectForKey: 的方法。
第三个方法中,会先到内存缓存去查找,如果没有命中,则去磁盘缓存中查找,大概就是通过 key 获取具体的路径找到对应的文件取出 NSData ,在经过一些处理转成 image 返回。

删除的功能:

- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;

前三个方法都是调用的第四个,所以我们看第四个方法就好了。
如果有内存缓存则调用 NSCache 中的 - (void)removeObjectForKey: ,如果 fromDisk 为 YES,则调用 NSFileManager- (BOOL)removeItemAtPath: error: 方法,删除指定缓存文件的路径即可。

清除的功能:

- (void)clearMemory;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk;

第一个方法直接调用 NSCache- (void)removeAllObjects; 。第二个方法,直接调用了 NSFileManager- (BOOL)removeItemAtPath: error: 删除指定缓存目录的路径即可。第三个方法调用的第二个方法。

清理的功能:

- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
- (void)cleanDisk;

清理缓存就是清理掉一些过期的文件和超最大缓存大小限制的文件。
看第一个方法,首先获取磁盘缓存的路径 URL。然后通过以下代码获取所有缓存文件的一些属性:

NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

// This enumerator prefetches useful properties for our cache files.
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                           includingPropertiesForKeys:resourceKeys
                                                              options:NSDirectoryEnumerationSkipsHiddenFiles
                                                         errorHandler:NULL];

这些属性分别是,是否是目录,文件的修改日期和文件大小。
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge]; 这一句则是获取缓存过期的日期。
然后 for-in 遍历 fileEnumerator

NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
    NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];

    // Skip directories.
    if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
        continue;
    }

    // Remove files that are older than the expiration date;
    NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
    if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
        [urlsToDelete addObject:fileURL];
        continue;
    }

    // Store a reference to this file and account for its total size.
    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
    currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
    [cacheFiles setObject:resourceValues forKey:fileURL];
}

获取文件路径的属性字典,如果是目录则跳过,比较修改日期和过期日期哪个更晚一些,如果是过期日期则说明该文件过期,放入 urlsToDelete 数组中。将文件大小累加到 currentCacheSize 上,并将不是过期的这些缓存文件记录到 cacheFiles 中,key 是文件的 URL ,value 是对应的属性字典。
之后,遍历 urlsToDelete 数组删除这些过期文件:

for (NSURL *fileURL in urlsToDelete) {
    [_fileManager removeItemAtURL:fileURL error:nil];
}

然后,判断没有过期的这些文件的总大小有没有超过最大的缓存大小 self.maxCacheSize
如果有的话,将 cacheFiles 里的 value 按照文件的修改日期进行排序,返回一个排好序的数组。取 self.maxCacheSize 大小的一半,作为清理缓存的界限 const NSUInteger desiredCacheSize = self.maxCacheSize / 2;。遍历排序后的数组,一个个文件删除,删除一个就从之前的总缓存文件大小的值减去删除后的文件大小,再比较有没有小于清理缓存的界限值 desiredCacheSize。如果小于了,则跳出循环。最后在主线程回调 completionBlock(); 。这样就达到了清理磁盘缓存的目的。

计算缓存大小:

- (NSUInteger)getSize;
- (NSUInteger)getDiskCount;
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;

第一个方法就是遍历缓存目录的所有文件,获取这些文件路径,通过 [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil] 获得一个字典在通过 fileSize 方法获取文件大小,累加起来就是缓存的大小。
第二个和第三个方法都是获取指定缓存路径的 NSDirectoryEnumerator 遍历取对应的值,和上面相差不大,不在赘述。

查询缓存是否存在:

- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
- (BOOL)diskImageExistsWithKey:(NSString *)key;

这些方法的实现基本就是调用 exists = [[NSFileManager defaultManager] fileExistsAtPath:[self defaultCachePathForKey:key]]; 这个方法,不在赘述。

最后说下 clearMemorycleanDiskbackgroundCleanDisk 的调用时机,在 - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory{} 这个初始化方法中,注册了三个通知分别是:

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(clearMemory)
                                             name:UIApplicationDidReceiveMemoryWarningNotification
                                           object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(cleanDisk)
                                             name:UIApplicationWillTerminateNotification
                                           object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(backgroundCleanDisk)
                                             name:UIApplicationDidEnterBackgroundNotification
                                           object:nil];

报内存警告时调用 clearMemory 清除内存缓存,程序即将终止的时候调用 cleanDisk 清理过期或超大小限制的磁盘缓存,而程序进入后台的时候,调用 backgroundCleanDisk ,在后台执行 cleanDiskWithCompletionBlock 清理任务。

至此,SDImageCache 的大部分方法就讲解完了。

结束

SDWebImage 这个库的基本思路就说完了,其实里面还有诸多的细节需要学习,比如图片处理,性能优化,内存管理等。阅读优秀的开源代码,会有一种探索的乐趣,不论从大的整体结构还是小的实现细节上,都能学到很多东西。

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

推荐阅读更多精彩内容