SDWebImage源码刨根问底

前言:

SDWebImage是iOS中一款处理图片的框架, 使用它提供的方法, 一句话就能让UIImageView,自动去加载并显示网络图片,将图片缓存到内存或磁盘缓存,正好有阅读开源项目的计划,于是首选SDWebImage,本文向大家分享项目整体框架以及最基本相关的GCD与Block等相关知识和基本方法使用,以及设计的思想。

源码来源:SDWebImage

源码描述:

SDWebImage图片缓存框架,最常用的是使用UItableViewCell使用UIImageView的来下载的图片并缓存,功能官方的解释是这样的

This library provides a category for UIImageView with support for remote images coming from the web.*
SDImageView提供UIImageView、UIImage等分类支持从远程服务器下载并缓存图片

提供的功能如下:

AnUIImageViewcategory adding web image and cache management to the Cocoa Touch framework 一个带有管理网络图片下载和缓存的UIImageView类别

An asynchronous image downloader 一个异步图片下载器

An asynchronous memory + disk image caching with automatic cache expiration handling 一个提供内存和磁盘缓存图片,并且能够自动清理过期的缓存

Animated GIF support (支持GIF图片)

WebP format support 支持WebP

A background image decompression 图片后台解压图片(空间换时间,这种做法会使内存激增,所以SD中含有**图片是否解压的参数)

A guarantee that the same URL won't be downloaded several times 保证一个URL不会下载多次

A guarantee that bogus URLs won't be retried again and again 保证黑名单的URL不会返回加载

A guarantee that main thread will never be blocked 保证主线程不会堵塞

Performances! 高性能

Use GCD and ARC 使用GCD和ARC

Arm64 support 支持Arm64

SDWebImage项目图

项目图

Cache:

SDImageCache 图片缓存类

SDImageCache功能描述:

SDImageCache maintains a memory cache and an optional disk cache. Disk cache write operations are performed
asynchronous so it doesn’t add unnecessary latency to the UI.
<br />SDImageCache 维护一个内存缓存以及一个"可选"的磁盘缓存。磁盘缓存的写入操作是异步执行(缓存任务加入到串行队列),因此不会造成 UI 的延迟

缓存选项

在缓存的过程中,程序会根据设置的不同的缓存选项,而执行不同的操作。下载选项由枚举SDImageCacheType定义,具体如下

typedef NS_ENUM(NSInteger, SDImageCacheType) {
/**
 * The image wasn't available the SDWebImage caches, but was downloaded from the web.
 * 不使用 SDWebImage 缓存,从网络下载
 */
SDImageCacheTypeNone,
/**
 * The image was obtained from the disk cache.
 * 磁盘缓存图像
 */
SDImageCacheTypeDisk,
/**
 * The image was obtained from the memory cache.
 * 内存缓存图像
 */
SDImageCacheTypeMemory
};

查询图片

这些选项主要涉及到queryDiskCacheForKey方法使用内存缓存或磁盘缓存查询数据

 /**
 *  从磁盘查询数据
 *
 *  @param key       key
 *  @param doneBlock block回调
 *
 *  @return return value description
 */
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }
    // 如果key为空
    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, ^{  //  查询磁盘缓存,将缓存操作作为一个任务放入ioQueue
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            // 磁盘查询
            UIImage *diskImage = [self diskImageForKey:key];
            // 如果图片存在 并且要缓存到内存中 则将图片缓存到内存
            if (diskImage) {
                CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale * diskImage.scale;
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
             // 回调
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

如果使用SDImageCacheTypeDisk(磁盘缓存)查询的过程是这样的,传入的key,此时通过ImageFromMemoryCacheForKey: @property (strong, nonatomic) NSCache *memCache(苹果官方提供的缓存类NSCache对象); memCache内存缓存中查找,如果有,完成回调;如果没有,在到磁盘缓存中去找对应的图片,此时的传入的key是没有经过md5加密的,经过MD5加密后,磁盘缓存路径中去查找,找到之后先将图片缓存在内存中,然后在把图片返回:,具体过程

queryDiskCacheForKey方法过程

在diskImageForKey方法会处理完整的图片数据,对其进行适当的缩放与解压操作,以提供给完成回调使用。

NSCache

NSCache

An NSCache object is a collection-like container, or cache, that stores key-value pairs, similar to the NSDictionary class.

NSCache 用法与 NSMutableDictionary 的用法很相似,是以 key-value 的形式进行存储,通常会使用NSCache作为临时数据和昂贵的对象存储,重用这些对象可以优化性能。

  • NSCache 类使用了自动删除策略,当内存紧张时系统抛出 Received memory warning.通知,此时在添加数据时,数据为空。
  • NSCache可以设置对象上限限制,通过countLimit与 totalCostLimit两个属性来限制cache的数量或者限制cost最大开销。当缓存对象的数量和cost总和超过这些尺度时,NSCache会自动释放部分缓存,释放执行顺序符合LRU(近期最少使用算法),如下图所示
再次使用Key为0、2后对象释放情况
添加后不使用其他对象释放情况
  • NSCache是线程安全的,在多线程操作中,可以在不同的线程中添加、删除和查询缓存中的对象,不需要对Cache加锁。
  • NSCache的对象并不会对Key进行Copy拷贝操作 而是strong强引用,对象不需要实现NSCopying协议,NSCache也不会像NSDictionary一样复制对象。

缓存操作方式

创建串行队列

 // Create IO serial queue
    // 磁盘读写队列,串行队列,任务一个执行完毕才执行下一个,所以不会出现一个文件同时被读取和写入的情况, 所以用 dispatch_async 而不必使用 disathc_barrier_async
    _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

将缓存操作作为一个任务放入ioQueue串行队列中,开启线程异步执行任务

 dispatch_async(self.ioQueue, ^{  //  查询磁盘缓存,将缓存操作作为一个任务放入ioQueue

  ....
}  

ioQueue还作用在存储图片,在storeImage方法中异步存储图片

存储图片

当下载完图片后,会先将图片保存到 NSCache 中,并把图片像素(Width × height × scale2)大小作为该对象的 cost 值,同时如果需要保存到硬盘,会先判断图片的格式,PNG 或者 JPEG,并保存对应的 NSData 到缓存路径中,文件名为 URL 的 MD5 值:

/**
* 缓存图片
*
* @param image 图片
* @param recalculate 是否重新计算
* @param imageData imageData
* @param key 缓存的key
* @param toDisk 是否缓存到磁盘
*/
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}

        // 缓存到内存
        [self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale * image.scale];

        if (toDisk) {
            dispatch_async(self.ioQueue, ^{
                NSData *data = imageData;
                
                // if the image is a PNG
                if (image && (recalculate || !data)) {
    #if TARGET_OS_IPHONE
                    
                    //我们需要确定该图像是PNG或JPEG格式
                                    // PNG图像更容易检测到,因为它们具有独特的签名(http://www.w3.org/TR/PNG-Structure.html)
                                    //第一个8字节的PNG文件始终包含以下(十进制)值:
                                    //13780787113 102610
                    
                     //我们假设图像PNG,在例中为imageData是零(即,如果想直接保存一个UIImage)
                                    //我们会考虑它PNG以免丢失透明度
                    // We need to determine if the image is a PNG or a JPEG
                    // PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html)
                    // The first eight bytes of a PNG file always contain the following (decimal) values:
                    // 137 80 78 71 13 10 26 10

                    // We assume the image is PNG, in case the imageData is nil (i.e. if trying to save a UIImage directly),
                    // we will consider it PNG to avoid loosing the transparency
                    BOOL imageIsPng = YES;

                    // But if we have an image data, we will look at the preffix
                    // 但如果我们有一个图像数据,我们将看看前缀,png
                    if ([imageData length] >= [kPNGSignatureData length]) {// 将UIImage转化为NSData,(1)这里使用的是UIImagePNGRepresentation(返回指定的PNG格式的图片数据)或UIImageJPEGRepresentation(返回指定的JPEG格式的图片数据)这种方式的好处如果PNG/JPEG数据不能正确生成返回nil,可以进行校验  (2)第二种方式是通过[NSData dataWithContentsOfFile:image] 这种方式读取图片数据,图片的部分坏掉,并不能校验

                        imageIsPng = ImageDataHasPNGPreffix(imageData);
                    }

                    if (imageIsPng) {
                        // return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
                        data = UIImagePNGRepresentation(image);
                    }
                    else {
                        data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                    }
    #else
                    data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
    #endif
                }

                if (data) {
                    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                    }

                    [_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
                }
            });
        }
    }

移除图片

/**
 *  移除文件
 *
 *  @param key        key
 *  @param fromDisk   是否从磁盘移除
 *  @param completion block回调
 */    
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion {
    
    if (key == nil) {
        return;
    }
     // 如果有缓存 则从缓存中移除
    [self.memCache removeObjectForKey:key];
    // 从磁盘移除 异步操作
    if (fromDisk) {
        dispatch_async(self.ioQueue, ^{
            
            // 直接删除文件
            [_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
            
            if (completion) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completion();
                });
            }
        });
    } else if (completion){
        completion();
    }
    
}

清理图片

通过设置

  • UIApplicationDidReceiveMemoryWarningNotification  通知来释放内存 
  • UIApplicationWillTerminateNotification    通知清理磁盘
  • UIApplicationDidEnterBackgroundNotification  通知进入后台清理磁盘 
 // -接收到内存警告通知-清理内存操作 - clearMemory
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(clearMemory)
                                                 name:UIApplicationDidReceiveMemoryWarningNotification
                                               object:nil];

    // -应用程序将要终止通知-执行清理磁盘操作 - cleanDisk
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(cleanDisk)
                                                 name:UIApplicationWillTerminateNotification
                                               object:nil];

    // - 进入后台通知 - 后台清理磁盘 - backgroundCleanDisk
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(backgroundCleanDisk)
                                                 name:UIApplicationDidEnterBackgroundNotification
                                               object:nil];

SDWebCache会在系统发出内存警告或进入后台通知,清理磁盘缓存:

  • 删除早于过期日期的文件(默认7天过期),可以通过maxCacheAge属性重新设置缓存时间
  • 如果剩余磁盘缓存空间超出最大限额(maxCacheSize),再次执行清理操作,删除最早的文件(按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间,可以通过修改 maxCacheSize 来改变最大缓存大小。)
// 清理过期的缓存图片
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        
        // 获取存储路径
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        // 获取相关属性数组
        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];
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;

        // Enumerate all of the files in the cache directory.  This loop has two purposes:
        // 遍历缓存路径中的所有文件,此循环要实现两个目的
        //
        //  1. Removing files that are older than the expiration date.
        //     删除早于过期日期的文件
        //  2. Storing file attributes for the size-based cleanup pass.
        //     保存文件属性以计算磁盘缓存占用空间
        //
        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];
        }
        
        // 删除过期的文件
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        // If our remaining disk cache exceeds a configured maximum size, perform a second
        // size-based cleanup pass.  We delete the oldest files first.
        // 如果剩余磁盘缓存空间超出最大限额,再次执行清理操作,删除最早的文件
        if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
            // Target half of our maximum cache size for this cleanup pass.
            const NSUInteger desiredCacheSize = self.maxCacheSize / 2;

            // Sort the remaining cache files by their last modification time (oldest first).
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }];

            // Delete files until we fall below our desired cache size.
            // 循环依次删除文件,直到低于期望的缓存限额
            for (NSURL *fileURL in sortedFiles) {
                if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

Dowloader:

  • SDWebImageDownloader:管理着缓存SDImageCache和下载SDWebImageDownloader类相应设置下载对象。我们在这个类可以得到关于下载和缓存的相关状态,该类有12个关于管理的SDWebImageOptions操作类型。
  • SDWebImageDownloaderOperation:是一个继承自NSOperation并遵循SDWebImageOperation协议的类。

*/

SDWebImageDownloader 图片下载器

SDImageCache功能描述:

Asynchronous downloader dedicated and optimized for image loading.
<br />专为加载图像设计并优化的异步下载器

下载选项

在执行下载过程中,程序会根据设置的不同的缓下载选项,而对NSMutableURLRequest执行不同的操作;下载选项和执行顺序由枚举SDWebImageDownloaderOptions和SDWebImageDownloaderExecutionOrder组成,具体如下

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    /// 低优先权
    SDWebImageDownloaderLowPriority = 1 << 0,
     /// 下载显示进度
    SDWebImageDownloaderProgressiveDownload = 1 << 1,

    /**
     * By default, request prevent the of NSURLCache. With this flag, NSURLCache
     * is used with default policies.
     * 默认情况下,请求不使用 NSURLCache。使用此标记,会使用 NSURLCache 和默认缓存策略
     */
    SDWebImageDownloaderUseNSURLCache = 1 << 2,

    /**
     * Call completion block with nil image/imageData if the image was read from NSURLCache
     * 如果图像是从 NSURLCache 读取的,则调用 completion block 时,image/imageData 传入 nil
     *
     * (to be combined with `SDWebImageDownloaderUseNSURLCache`).
     * (此标记要和 `SDWebImageDownloaderUseNSURLCache` 组合使用)
     */

    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    /**
     * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
     * 在 iOS 4+,当 App 进入后台后仍然会继续下载图像。这是向系统请求额外的后台时间以保证下载请求完成的
     *
     * extra time in background to let the request finish. If the background task expires the operation will be cancelled.
     * 如果后台任务过期,请求将会被取消
     */

    SDWebImageDownloaderContinueInBackground = 1 << 4,

    /**
     * Handles cookies stored in NSHTTPCookieStore by setting
     * 通过设置
     * NSMutableURLRequest.HTTPShouldHandleCookies = YES;
     * 处理保存在 NSHTTPCookieStore 中的 cookies,通过设置 NSMutableURLRequest.HTTPShouldHandleCookies = YES 来处理存储在 NSHTTPCookieStore 的cookies
     */
    SDWebImageDownloaderHandleCookies = 1 << 5,

    /**
     * Enable to allow untrusted SSL ceriticates.
     * 允许不信任的 SSL 证书
     *
     * Useful for testing purposes. Use with caution in production.
     * 可以出于测试目的使用,在正式产品中慎用
     */
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

    /**
     * Put the image in the high priority queue.
     * 将图像放入高优先级队列
     */
    SDWebImageDownloaderHighPriority = 1 << 7,
    

};
// 下载执行顺序:1.FIFO先进先出,队列方式 2.LIFO 后进先出堆栈执行
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    /**
     * Default value. All download operations will execute in queue style (first-in-first-out).
     * 默认值。所有下载操作将按照队列的先进先出方式执行
     */
    SDWebImageDownloaderFIFOExecutionOrder,

    /**
     * All download operations will execute in stack style (last-in-first-out).
     * 所有下载操作将按照堆栈的后进先出方式执行
     */
    SDWebImageDownloaderLIFOExecutionOrder
};

下载block

当前下载进度、完成和设置过滤请求头的相关信息,都是由block来呈现
/**
* 下载进度block
*
* @param receivedSize 已收到数据大小
* @param expectedSize 应该受到数据大小
/
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
/
*
* 下载完成block
*
* @param image 下载好的图片
* @param data 下载的数据
* @param error 错误信息
* @param finished 是否完成
*/
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData data, NSError error, BOOL finished);
/

* 过滤请求头部信息block
*
* @param url URL
* @param headers 请求头部信息
*
* @return return value description
*/
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);

相关实例变量

设置图片的下载并发数量、当前下载的数量,将下载操作添加到队列和设置休眠状态都是放在是一个NSOperationQueue操作队列中来完成的

 /**
 *  设置并发下载数,默认为6
 */
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;

/**
 * Shows the current amount of downloads that still need to be downloaded
 * <br />显示仍需要下载的数量
 */

@property (readonly, nonatomic) NSUInteger currentDownloadCount;


/**
 *  The timeout value (in seconds) for the download operation. Default: 15.0.
 * <br />下载操作的超时时长(秒),默认:15秒
 */
@property (assign, nonatomic) NSTimeInterval downloadTimeout;


/**
 * Changes download operations execution order. Default value is `SDWebImageDownloaderFIFOExecutionOrder`.
 * <br />修改下载操作执行顺序,默认值是 `SDWebImageDownloaderFIFOExecutionOrder`(先进先出)
 */
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;


// 下载操作队列
@property (strong, nonatomic) NSOperationQueue *downloadQueue;
// 最后添加的操作 先进后出顺序顺序
@property (weak, nonatomic) NSOperation *lastAddedOperation;
// 图片下载类
@property (assign, nonatomic) Class operationClass;
// URL回调字典 以URL为key,你装有URL下载的进度block和完成block的数组为value (相当于下载操作管理器)
@property (strong, nonatomic) NSMutableDictionary *URLCallbacks;
// HTTP请求头
@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders;
// This queue is used to serialize the handling of the network responses of all the download operation in a single queue
// barrierQueue是一个并行队列,在一个单一队列中顺序处理所有下载操作的网络响应
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue

URLCallbacks: URL回调字典 以URL为key,装有URL下载的进度block和完成block的数组为value,由于创建一个barrierQueue 并行队列,所有下载操作的网络响应序列化处理是放在一个自定义的并行调度队列中来处理的,可能会有多个线程同时操作URLCallbacks属性,其声明及定义如下:

      /** 并行的处理所有下载操作的网络响应
     第一个参数:队列名称
     第二个参数:队列类型,NULL 则创建串行队列处理方式,DISPATCH_QUEUE_CONCURRENT则是并行队列处理方式
     _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);

为了保证URLCallbacks添加和删除线程安全性,SDWebImageDownloader将这些操作作为一个个任务放到barrierQueue队列中,并设置屏障(dispatch_barrier_sync)来确保同一时间只有一个线程操作URLCallbacks属性

// dispatch_barrier_sync 保证同一时间只有一个线程操作 URLCallbacks
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];
    // 将 进度block和完成block赋值
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    [callbacksForURL addObject:callbacks];
    // 已URL为key进行赋值
    self.URLCallbacks[url] = callbacksForURL;
    
    // 如果是第一次下载 则回调
    if (first) {
        // 通过这个回调,可以实时获取下载进度以及是下载完成情况
        createCallback();
    }
});

downloadImageWithURL: options: progress: completed:方法是该类的核心,调用了addProgressCallback来将请求的信息存入管理器(URLCallbacks)中,同时在创建回调的block中创建新的操作,配置之后将其放入downloadQueue操作队列中,最后方法返回新创建的操作,返回一个遵循SDWebImageOperation协议的对象,SDWebImageOperation协议定义了一个cancel取消下载的方法;
SDWebImageDownloaderOperation(下载操作类)继承自NSOperation并遵循SDWebImageOperation协议的类。

@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageOperation>

.
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
// 下载对象 block中要修改的变量需要__block修饰
__block SDWebImageDownloaderOperation *operation;
// weak self 防止retain cycle
__weak SDWebImageDownloader *wself = self; // 下面有几行代码中 有使用SDWebImageDownloader对象赋值给SDWebImageDownloader 对象,设置弱引用防止循环引用,
// 添加设置回调 调用另一方法,在创建回调的block中创建新的操作,配置之后将其放入downloadQueue操作队列中。
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
// 设置延时时长 为 15.0秒
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
        // 为防止重复缓存(NSURLCache + SDImageCache),如果设置了 SDWebImageDownloaderUseNSURLCache(系统自带的使用 NSURLCache 和默认缓存策略),则禁用 SDImageCache
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        // 是否处理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 类
        operation = [[wself.operationClass alloc] initWithRequest:request
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                             SDWebImageDownloader *sself = wself;
                                                             if (!sself) return;
                                                             // URL回调数组
                                                             NSArray *callbacksForURL = [sself callbacksForURL:url];
                                                             for (NSDictionary *callbacks in callbacksForURL) {
                                                                 SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                                                 if (callback) callback(receivedSize, expectedSize);
                                                             }
                                                         }
                                                        completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            NSArray *callbacksForURL = [sself callbacksForURL:url];
                                                            if (finished) {
                                                                [sself removeCallbacksForURL:url];
                                                            }
                                                            for (NSDictionary *callbacks in callbacksForURL) {
                                                               
                                                                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                                if (callback) callback(image, data, error, finished);
                                                            }
                                                        }
                                                        cancelled:^{
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            // 如果下载完成 则从回调数组里面删除
                                                            [sself removeCallbacksForURL:url];
                                                        }];
        
        // 如果设置了用户名 & 口令
        if (wself.username && wself.password) {
            // 设置 https 访问时身份验证使用的凭据
            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;
        }
    }];

    return operation;
}

SDWebImageDownloaderOperation

该类只向外提供了一个方法,初始化方法initWithRequest:options:progress:completed:cancelled:。该类通过NSURLConnection来获取数据,通过 NSNotificationCenter来告诉其他类下载的相关进程,其实现
NSURLConnectionDelegate

// 本类中用到NSURLConnectionDataDelegate 代理方法同时NSURLConnectionDataDelegate 又用到NSURLConnectionDelegate代理方法:
/** NSURLConnectionDataDelegate 
 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
 
 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
 
 - (void)connectionDidFinishLoading:(NSURLConnection *)connection;
 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
 */

NSURLConnectionDelegate

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;

- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;

- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;

主要分析-connection:didReceiveData:和- (void)connection:didReceiveResponse:两个主要方法。
-connection:didReceiveData:方法的主要任务是接收数据,每次接收到数据时,都会用现有的数据创建一个CGImageSourceRef对象以做处理。在首次获取到数据时(width+height==0)会从这些包含图像信息的数据中取出图像的长、宽、方向等信息以备使用。而后在图片下载完成之前,会使用CGImageSourceRef对象创建一个图片对象,经过缩放、解压缩操作后生成一个UIImage对象供完成回调使用。当然,在这个方法中还需要处理的就是进度信息。如果我们有设置进度回调的话,就调用这个进度回调以处理当前图片的下载进度。

/**
 *  接收到数据
 *<#data description#>
 *  @param connection <#connection description#>
 *  @param data       
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    // 追加数据
    [self.imageData appendData:data];

    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
        // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
        // Thanks to the author @Nyx0uf

        // Get the total bytes downloaded
        // 获取已下载的图片大小
        const NSInteger totalSize = self.imageData.length;

        // Update the data source, we must pass ALL the data, not just the new bytes
        // 更新数据源,我们必须传入所有的数据 并不是这次接受到的新数据
        CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
        // 如果宽和高都为0 即第一次接受到数据
        if (width + height == 0) {
            // 获取图片的高、宽、方向等相关数据 并赋值
            CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
            if (properties) {
                NSInteger orientationValue = -1;
                CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
                if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
                val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
                if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
                val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
                if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
                CFRelease(properties);

                // When we draw to Core Graphics, we lose orientation information,
                // which means the image below born of initWithCGIImage will be
                // oriented incorrectly sometimes. (Unlike the image born of initWithData
                // in connectionDidFinishLoading.) So save it here and pass it on later.
                // 当我们绘制 Core Graphics 时,我们将会失去图片方向的信息,这意味着有时候由initWithCGIImage方法所创建的图片的方向会不正确(不像在 connectionDidFinishLoading 代理方法里面 用 initWithData 方法创建),所以我们先在这里保存这个信息并在后面使用。
                orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
            }

        }
        // 已经接受到数据 图片还没下载完成
        if (width + height > 0 && totalSize < self.expectedSize) {
            // Create the image
            // 先去第一张 部分图片
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#ifdef TARGET_OS_IPHONE
            // Workaround for iOS anamorphic image
            // 对iOS变形图片工作
            if (partialImageRef) {
                const size_t partialHeight = CGImageGetHeight(partialImageRef);
                CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
                CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
                CGColorSpaceRelease(colorSpace);
                if (bmContext) {
                    CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
                    CGImageRelease(partialImageRef);
                    partialImageRef = CGBitmapContextCreateImage(bmContext);
                    CGContextRelease(bmContext);
                }
                else {
                    CGImageRelease(partialImageRef);
                    partialImageRef = nil;
                }
            }
#endif
            // 存储图片
            if (partialImageRef) {
                UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
                // 获取key
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                // 获取缩放的图片
                UIImage *scaledImage = [self scaledImageForKey:key image:image];
                // 解压图片
                image = [UIImage decodedImageWithImage:scaledImage];
                CGImageRelease(partialImageRef);
                dispatch_main_sync_safe(^{
                    // 完成block回调
                    if (self.completedBlock) {
                        self.completedBlock(image, nil, nil, NO);
                    }
                });
            }
        }

        CFRelease(imageSource);
    }
    // 进度block回调
    if (self.progressBlock) {
        self.progressBlock(self.imageData.length, self.expectedSize);
    }
}

启动方法start,该方法使用runloop来保证图片滑动的流畅性

  // 重写NSOperation Start方法
- (void)start {
    @synchronized (self) {
        // 如果被取消了
        if (self.isCancelled) {
            // 则已经完成
            self.finished = YES;
            // 重置
            [self reset];
            return;
        }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
        // 后台处理
        if ([self shouldContinueWhenAppEntersBackground]) {
            // 1.防止Block的循环引用(技巧),   wself是为了block不持有self,避免循环引用,
            __weak __typeof__ (self) wself = self;
            self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
                // 而再声明一个strongSelf是因为一旦进入block执行,就不允许self在这个执行过程中释放。block执行完后这个strongSelf会自动释放,没有循环引用问题。
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    // 取消
                    [sself cancel];

                    [[UIApplication sharedApplication] endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        // 正在执行中
        self.executing = YES;
        self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
        self.thread = [NSThread currentThread];
    }
    // 开始请求
    [self.connection start];

    if (self.connection) {
        // 进度block回调
        if (self.progressBlock) {
            self.progressBlock(0, NSURLResponseUnknownLength);
        }
        // 通知 开始下载
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
        // 开始运行 runloop
        if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
            // Make sure to run the runloop in our background thread so it can process downloaded data
            // Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
            //       not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
            
            /**
             *  Default NSDefaultRunLoopMode(Cocoa) kCFRunLoopDefaultMode (Core Foundation) 最常用的默认模式
             
             空闲RunLoopMode
             当用户正在滑动 UIScrollView 时,RunLoop 将切换到 UITrackingRunLoopMode 接受滑动手势和处理滑动事件(包括减速和弹簧效果),此时,其他 Mode (除 NSRunLoopCommonModes 这个组合 Mode)下的事件将全部暂停执行,来保证滑动事件的优先处理,这也是 iOS 滑动顺畅的重要原因。
             当 UI 没在滑动时,默认的 Mode 是 NSDefaultRunLoopMode(同 CF 中的 kCFRunLoopDefaultMode),同时也是 CF 中定义的 “空闲状态 Mode”。当用户啥也不点,此时也没有什么网络 IO 时,就是在这个 Mode 下。
             
             */
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
        }
        else {
            CFRunLoopRun();
        }
        //  没有完成 则取消
        if (!self.isFinished) {
            [self.connection cancel];
            [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
        }
    }
    else {
        if (self.completedBlock) {
            self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
        }
    }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
    // 后台处理
    if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
        [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
        self.backgroundTaskId = UIBackgroundTaskInvalid;
    }
#endif
}

Util:

  • SDWebImageDecoder对图片的解压缩操作,通过指定decodedImageWithImage方法来解压图片,这样做的好处是防止图片加载时有延时(图片在UIImageView显示时会进行一个解压操作),但是用这个方法会导致内存暴涨等问题;
  • SDWebImagePrefetcher是预取图片类,通过startPrefetchingAtIndex方法可以指定开始预取URL数组的第几张图片。

SDWebImageDecoder类

decodedImageWithImage方法来解压图片,具体过程是这样的。

+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    if (image.images) {
        // Do not decode animated images 不对动画图片进行解压
        return image;
    }

    CGImageRef imageRef = image.CGImage; // 创建一个CGImage格式的图片来支持解压操作
    // 获得图片宽高
    CGSize imageSize = CGSizeMake(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef));
    // 获得一个图片矩形
    CGRect imageRect = (CGRect){.origin = CGPointZero, .size = imageSize};
    // 创建一个RGB绘制空间
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    // 通过imageRef获得Bitmap位图信息
    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
    
    // 设置图片蒙版信息(例如是否透明)
    int infoMask = (bitmapInfo & kCGBitmapAlphaInfoMask);
    BOOL anyNonAlpha = (infoMask == kCGImageAlphaNone ||
            infoMask == kCGImageAlphaNoneSkipFirst ||
            infoMask == kCGImageAlphaNoneSkipLast);

    // CGBitmapContextCreate doesn't support kCGImageAlphaNone with RGB.
    // CGBitmapContextCreate 不支持在RGB上使用kCGImageAlphaNone
    // https://developer.apple.com/library/mac/#qa/qa1037/_index.html
    if (infoMask == kCGImageAlphaNone && CGColorSpaceGetNumberOfComponents(colorSpace) > 1) {
        // Unset the old alpha info. 取消旧的alpha信息
        bitmapInfo &= ~kCGBitmapAlphaInfoMask;

        // Set noneSkipFirst. 设置新的alpha信息
        bitmapInfo |= kCGImageAlphaNoneSkipFirst;
    }
            // Some PNGs tell us they have alpha but only 3 components. Odd.
    else if (!anyNonAlpha && CGColorSpaceGetNumberOfComponents(colorSpace) == 3) {
        // Unset the old alpha info.
        bitmapInfo &= ~kCGBitmapAlphaInfoMask;
        bitmapInfo |= kCGImageAlphaPremultipliedFirst;
    }

    // It calculates the bytes-per-row based on the bitsPerComponent and width arguments.
    CGContextRef context = CGBitmapContextCreate(NULL,
            imageSize.width,
            imageSize.height,
            CGImageGetBitsPerComponent(imageRef),
            0,
            colorSpace,
            bitmapInfo);
    CGColorSpaceRelease(colorSpace);

    // If failed, return undecompressed image
    if (!context) return image;

    CGContextDrawImage(context, imageRect, imageRef);
    CGImageRef decompressedImageRef = CGBitmapContextCreateImage(context);

    CGContextRelease(context);

    UIImage *decompressedImage = [UIImage imageWithCGImage:decompressedImageRef scale:image.scale orientation:image.imageOrientation];
    CGImageRelease(decompressedImageRef);
    return decompressedImage;
}

当你用 UIImage 或 CGImageSource 的几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片

  • 为什么从磁盘里面取出图片后,block回调之前要解压图片呢?因为图片在UIImageView上面显示的时候需要解压,而这个解压操作是在主线程里面进行的,比较耗时,这样就会产生延时效果在后台解压能够解决这一问题,但是这种用空间换时间的方法也存在着内存暴增甚至崩溃等问题,所以自己得权衡一下。这就是为什么SDImageCache、SDWebImageDownloader、SDWebImageDownloaderOperation
    类中都有shouldDecompressImages (是否解压图片)
    值存在的原因

SDWebImagePrefetcher类

SDWebImagePrefetcher类主要提供startPrefetchingAtIndex方法来实现开始预取URL数组的预加载图片,具体实现是这样的。

/**
 *  开始预取URL数组的第几张图片
 *
 *  @param index index description
 */
- (void)startPrefetchingAtIndex:(NSUInteger)index {
    // 判断index是否越界
    if (index >= self.prefetchURLs.count) return;
    // 请求个数 +1
    self.requestedCount++;
    // 用SDWebImageManager 下载图片
    [self.manager downloadImageWithURL:self.prefetchURLs[index] options:self.options progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
        if (!finished) return;
        // 完成个数 +1
        self.finishedCount++;
        // 有图片
        if (image) {
            // 进度block回调
            if (self.progressBlock) {
                self.progressBlock(self.finishedCount,[self.prefetchURLs count]); 
            }
            NSLog(@"Prefetched %@ out of %@", @(self.finishedCount), @(self.prefetchURLs.count));
        }
        else {
            // 进度block回调
            if (self.progressBlock) {
                self.progressBlock(self.finishedCount,[self.prefetchURLs count]);
            }
            NSLog(@"Prefetched %@ out of %@ (Failed)", @(self.finishedCount), @(self.prefetchURLs.count));
            // 下载完成 但是没图片 跳过个数 +1
            // Add last failed
            // Add last failed
            self.skippedCount++;
        }
        // delegate 回调
        if ([self.delegate respondsToSelector:@selector(imagePrefetcher:didPrefetchURL:finishedCount:totalCount:)]) {
            [self.delegate imagePrefetcher:self
                            didPrefetchURL:self.prefetchURLs[index]
                             finishedCount:self.finishedCount
                                totalCount:self.prefetchURLs.count
            ];
        }
        // 如果预存完成个数大于请求的个数,则请求requestedCount最后一个预存图片
        if (self.prefetchURLs.count > self.requestedCount) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self startPrefetchingAtIndex:self.requestedCount];
            });
        }
        // 如果完成个数与请求个数相等 则下载已完成
        else if (self.finishedCount == self.requestedCount) {
            [self reportStatus];
            if (self.completionBlock) {
                self.completionBlock(self.finishedCount, self.skippedCount);
                self.completionBlock = nil;
            }
        }
    }];
}

知识点总结

  • PNG图片的判断,通过签名字节kPNGSignatureBytes数组来判断是否图片。

  • 在SDWebImageDownloaderOperation类,NSURLConnectionDataDelegate相关代理方法首先确保RunLoop运行在后台线程,当UI处于”空闲“(NSRunLoopDefault)把图片的下载操作加入到RunLoop,这样来保证混滑动图片的流畅性,所以当你把快速滑动UITableView时,图片不会立即显示,当处于空闲状态时图片才显示,原因就在这里。

先写到这里

参考资料

SDWebImage实现分析

SDWebImage源码浅析

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

推荐阅读更多精彩内容