通俗易懂的SDWebImage源码解析

  • Asynchronous image downloader with cache support as a UIImageView category
    *一个异步的图片下载与缓存的UIImageViewCategory

对于它究竟是如何工作的,相信大家应该或多或少都已经有所了解。但是它内部是怎么实现,又有那些细节,我却一直犯懒没有真正的好好去研究过,最近不是很忙,于是我就仔细的研究了一下它的实现细节,这里我源码的版本为4.0.0,也就是当前最新的版本。

**在这里我推荐大家去github下载对应的源码,一边看blog一边看对应的源码,最好再做上自己的注释,这样会看的更快,且做做笔记会加深自己的映像,SDWebImage:github地址:https://github.com/rs/SDWebImage **

好了,废话不多说,直接来看代码吧。
下面的代码是我们经常使用的SDWebImage的方法之一,给imageView传入对应的图片url和占位图片,它就帮我们实现了图片的所有操作。

点进它的具体实现,可以看到它是一个UIImageView的分类,分类的调用方法如下,我已经给对应的参数做出了对应的翻译:

/**
 * 使用一个url,占位图片和自定义选项来设置imageView
 * 下载是异步且缓存的
 * url                          图像的url
 * placeholder                  占位图片,初始化时被设置,在请求结束时消失
 * options                      在下载图片的时候使用的选项,看SDWebImageOptions有哪些可能的值
 * progressBlock                当图像下载时候调用的block,这个block在一个后台队列执行
 * completedBlock               当操作结束时调用的block,这个block没有返回值,把请求到的图像作为第一个参数,如果发生错误的话,第一个参数为空,第二个参数会包含一个NSError对象,第三个参数是一个bool值,指是从本地缓存还是从网络来重新获取图像,第四个参数是图片原始的url
 */
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
                       setImageBlock:nil
                            progress:progressBlock
                           completed:completedBlock];
}

相信大家对上面的参数,并不陌生,即使曾经没研究过,看到对应的名称和注释也能大概猜出它们的作用,这里唯一不太了解的应该是options的含义了。

options是一个枚举,下面是options对应的值,作用已经添加在注释中了

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

    /**
     * UI交互期间下载
     * 导致延迟下载在UIScrollView减速的时候,(也就是你滑动的时候scrollview不下载,你手从屏幕上移走,scrollview开始减速的时候才会开始下载图片)
     */
    SDWebImageLowPriority = 1 << 1,

    /**
     * 只进行内存缓存,不进行磁盘缓存
     */
    SDWebImageCacheMemoryOnly = 1 << 2,

    /**
     * 这个标志可以渐进式下载,显示的图像是逐步在下载(就像你用浏览器浏览网页的时候那种图片下载,一截一截的显示
     */
    SDWebImageProgressiveDownload = 1 << 3,

    /**
     * 即使图像缓存,也要遵守HTTP响应缓存控制,如果需要,可以从远程位置刷新图像
     * 磁盘缓存将由NSURLCache而不是SDWebImage处理,导致轻微的性能降低。
     * 这个选项帮助处理在同样的网络请求地址下图片的改变
     * 如果刷新缓存的图像,完成的block会在使用缓存图像的时候调用,还会在最后的图像被调用
     * 当你不能使你的URL静态与嵌入式缓存
     */
    SDWebImageRefreshCached = 1 << 4,

    /**
     * 在iOS4以上,如果app进入后台,也保持下载图像,这个需要取得用户权限
     * 如果后台任务过期,操作将被取消
     */
    SDWebImageContinueInBackground = 1 << 5,

    /**
     * 操作cookies存储在NSHTTPCookieStore通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES
     */
    SDWebImageHandleCookies = 1 << 6,

    /**
     * 允许使用无效的SSL证书
     * 用户测试,生成情况下小心使用
     */
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,

    /**
     * 优先下载
     */
    SDWebImageHighPriority = 1 << 8,
    
    /**
     * 在加载图片时加载占位图。 此标志将延迟加载占位符图像,直到图像完成加载。
     */
    SDWebImageDelayPlaceholder = 1 << 9,

    /**
     * 我们通常不调用transformDownloadedImage代理方法在动画图像上,大多数情况下会对图像进行耗损
     * 无论什么情况下都使用
     */
    SDWebImageTransformAnimatedImage = 1 << 10,
    
    /**
     * 图片在下载后被加载到imageView。但是在一些情况下,我们想要设置一下图片(引用一个滤镜或者加入透入动画)
     * 使用这个来手动的设置图片在下载图片成功后
     */
    SDWebImageAvoidAutoSetImage = 1 << 11,
    
    /**
     * 图像将根据其原始大小进行解码。 在iOS上,此标记会将图片缩小到与设备的受限内存兼容的大小。
     */
    SDWebImageScaleDownLargeImages = 1 << 12
};

看完上面的枚举值,大家应该还是不知道有什么作用,没关系,接着往下看。

继续往后可以看到,最终它真正调用的是UIView+WebCache.h的方法,这里就是要详细讲解的第一个方法,在下面的代码中我已经贴了一些注释来方便讲解:

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock {
    // 获取可用的operationKey
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    // 取消该key对应的任务
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    // 给该视图的实例对象设置一个属性
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    // 如果options不为SDWebImageDelayPlaceholder,那么先把placeholder设置到该视图上
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    if (url) {
        // check if activityView is enabled or not
        // 如果有url,且设置显示ActivityIndicator,那么显示
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
        
        __weak __typeof(self)wself = self;
        // ⚠️这里的operation不是继承自NSOperation的,我们可以把它看做一个关联视图操作的对象,我们称它为op对象
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            // 图像下载成功后,移除ActivityIndicator
            [sself sd_removeActivityIndicator];
            if (!sself) {
                return;
            }
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }
                // 如果有image且options为SDWebImageAvoidAutoSetImage且有completedBlock
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                    // 在这里获取到图片,且做一些加工的操作
                    completedBlock(image, error, cacheType, url);
                    return;
                } else if (image) {
                    // 如果有image,设置视图的图像
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    // 标记设为需要布局
                    [sself sd_setNeedsLayout];
                } else {
                    // image已经尝试获取过了,但是没有从网络端获取到
                    // 如果options为SDWebImageDelayPlaceholder,当前视图设置为占位图片
                    // 标记设为需要布局
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                // 有completedBlock且下载finished为yes,将需要的参数传出去
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        // 将现在的op对象加到对应的视图实例中
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        // 如果url为空,抛出错误
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

下面我们将一步步来解释这些代码的含义:
1.首先先获取validOperationKey,如果为空,那么就获取到当前类的名称,查看UIImageView+WebCache.h对应的传入参数,可以发现UIImageView传入的对应validOperationKeynil,也就是说默认情况下,如果我们不直接给validOperationKey赋值,它就为nil,那么这里获得的validOperationKey一般也就是对应类的class,也就是说如果是UIImageView调用这个方法,那么对应的validOperationKey也就是UIImageView

NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);

2.取消该key对应的任务

[self sd_cancelImageLoadOperationWithKey:validOperationKey];

什么情况,怎么还没开始做事情就开始取消了?

在这里我们做一个标记,一会来解释
⚠️标记1:- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key是什么意思,为什么一来就取消,有什么作用?

3.给该视图的实例对象设置一个属性,这里的知识是使用了runtime,如果对runtime不够了解的,可以参看资料:让你快速上手Runtime

通俗点讲:这里的作用就是给UIView的实例添加了@property (nonatomic, strong) NSString *url;,只是这个属性的获取方式是通过key/value的方式来获得的,url这个value对应的key&imageURLKey

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

4.接下来就是设置placeholder,如果不想让SDWebImage来帮你设置占位图片,就给它传入setImageBlock来自定义设置占位图片。

// 如果options不为SDWebImageDelayPlaceholder,那么先把placeholder设置到该视图上
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }

这里有两个需要讲解的

  • options & SDWebImageDelayPlaceholder: &是按位与
    举个例子:a & b a=1 b=2 a== 0000 0001(二进制) b== 0000 0010(二进制) a & b = 0000 0000(二进制)
    放在这里就是,如果options中包含SDWebImageDelayPlaceholder,那么就不设置占位图。
  • dispatch_main_async_safe:这是一个定义的宏
    如果当前是主进程,就直接执行block,否则把block放到主进程运行。为什么要判断是否是主进程?因为iOS上任何UI的操作都在主线程上执行,所以主进程还有一个名字,叫做“UI进程”。

ifndef dispatch_main_async_safe

define dispatch_main_async_safe(block)\

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

endif


5.下面的操作是根据url来加载网络图片,分为有`url`有值和`url`无值的情况
```obj
  if (url) {
      // check if activityView is enabled or not
      if ([self sd_showActivityIndicatorView]) {
          [self sd_addActivityIndicator];
      }
      
      __weak __typeof(self)wself = self;
      id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
          __strong __typeof (wself) sself = wself;
          [sself sd_removeActivityIndicator];
          if (!sself) {
              return;
          }
          dispatch_main_async_safe(^{
              if (!sself) {
                  return;
              }
              if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                  completedBlock(image, error, cacheType, url);
                  return;
              } else if (image) {
                  [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                  [sself sd_setNeedsLayout];
              } else {
                  if ((options & SDWebImageDelayPlaceholder)) {
                      [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                      [sself sd_setNeedsLayout];
                  }
              }
              if (completedBlock && finished) {
                  completedBlock(image, error, cacheType, url);
              }
          });
      }];
      [self sd_setImageLoadOperation:operation forKey:validOperationKey];
  } else {
      dispatch_main_async_safe(^{
          [self sd_removeActivityIndicator];
          if (completedBlock) {
              NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
              completedBlock(nil, error, SDImageCacheTypeNone, url);
          }
      });
  }
  • 先来分析url无值的情况,也就是上面代码中的else,可以很清晰的看到先会调用[self sd_removeActivityIndicator];,根据名字我们大概能猜到是移除一个ActivityIndicator,然后会使用完成的block在主线程抛出一个NSError对象。

  • 现在来看url有值的情况,首先

          // 如果有url,且设置显示ActivityIndicator,那么显示
          if ([self sd_showActivityIndicatorView]) {
              [self sd_addActivityIndicator];
          }
    

    然后通过SDWebImageManager的单例对象调用下面的方法,返回了一个名为operationid类型的对象

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

    先不看这个方法的实现,先猜一猜这个方法是做什么的?

我想你肯定已经猜到了,这个方法就是下载图片且给UIImageView设置图片的方法

现在先来看看这个方法完成的block中的代码:

            __strong __typeof (wself) sself = wself;
            // 图像下载成功后,移除ActivityIndicator
            [sself sd_removeActivityIndicator];
            // 如果self为nil,直接返回
            if (!sself) {
                return;
            }

然后如果获取到图片,options中包含SDWebImageAvoidAutoSetImage,且完成的block不为空的情况下,直接调用完成block返回

// 如果有image且options为SDWebImageAvoidAutoSetImage且有completedBlock
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
  // 在这里获取到图片,且做一些加工的操作
  completedBlock(image, error, cacheType, url);
  return;
}

如果没有获取到optionsSDWebImageAvoidAutoSetImage,但是获取到了image,直接设置对应视图的image

else if (image) {
    // 如果有image,设置视图的图像
    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
    // 标记设为需要布局
    [sself sd_setNeedsLayout];
}

然后就是当image没有获取到的时候的操作,如果之前设置的optionsSDWebImageDelayPlaceholder(也就是延迟加载占位图),那么现在也应该把占位图设置上了

else {
    // image已经尝试获取过了,但是没有从网络端获取到
    // 如果options为SDWebImageDelayPlaceholder,当前视图设置为占位图片
    if ((options & SDWebImageDelayPlaceholder)) {
        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        [sself sd_setNeedsLayout];
    }

最后,在所有的判断结束以后,通过completedBlock将对应的参数传递出去

// 有completedBlock且下载finished为yes,将需要的参数传出去
if (completedBlock && finished) {
  completedBlock(image, error, cacheType, url);
}

在url不为nil的逻辑代码的最后,将前面生成的operation和最开始获取到的validOperationKey设置到对应的视图,也就是下面的代码!!!

在这里我们再做一个标记,下面来解释
⚠️标记2:- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key又是什么意思,和上面的标记1有什么关系?

// 将现在的op对象加到对应的视图实例中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

上面对对应的逻辑进行大概的梳理,大家应该学习到了一些,但是有些地方肯定还是不清楚,所以看下面吧

下面是解决问题的时间

第一个问题

  • 首先看到⚠️标记1和上面的⚠️标记2
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

在所有操作刚开始执行的时候,视图就执行了个取消的操作,最后又给视图增加了一个operation,这到底是怎么回事?

1.根据经验,如果要给一个UIImageView设置image,那么肯定要获取到对应的image,如果这是一个网络图片,那么肯定是要将这个图片下载,然后下载好了,再将图片设置到对应的UIImageView,相信大家对这个逻辑是没有异议的。

2.现在下载图片对应的操作就是id <SDWebImageOperation> operation来执行,一开始的取消操作就是取消了这样一个任务
注意:这里的operation可不是继承自NSOperation的对象,而是一个继承自NSObject的对象,你可以将它看做一个操作图片更新的对象

3.看一下- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key对应的实现,首先通过[self operationDictionary]获取到存有operation的字典(这里的字典也是通过runtime动态来添加的),然后通过对应的key取出对应的operation,调用cancel来取消对应的操作,然后通过key移除对应的operation

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // Cancel in progress downloader from queue
    // operation的字典
    SDOperationsDictionary *operationDictionary = [self operationDictionary];
    id operations = operationDictionary[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];
    }
}

4.接着看一下对应的设置方法,设置方法中先调用了sd_cancelImageLoadOperationWithKey,然后再将对应的operation添加到了字典中

- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key {
    if (key) {
        [self sd_cancelImageLoadOperationWithKey:key];
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self operationDictionary];
            operationDictionary[key] = operation;
        }
    }
}

下面我举个例子来讲一下这么做的作用:

在常用的tableViewcell上有图片是再常见不过的了,如下所示的这种cell

cell.png

  • 在我们使用SDWebImage给上面的cell中的imageview设置网络图片的时候,图片的下载是异步的,那么如果现在给当前cell设置的为cell.imageviewa.png,随着tableView的滑动,这个cell会被复用,复用后现在cell.imageviewb.png,这里的a.pngb.png都是从网络上异步下载的,不是本地的资源图片
  • 一开始cellindex为1,imagea,复用以后cellindex为6,imageb,按道理来说图片应该先为a,然后为b,但是a很大,b很小,b都已经下载好了,a还没有下载好,当滑动到显示index为6的cell的时候,cell的图片先显示的b,因为b已经下载好了,过了一会,a也下载好了
    那么神奇的事情发生了,index为6的cell中的图片ab覆盖了,应该显示b的变成显示a
  • 整个数据都乱了,这实在太可怕了

如果上面我举的例子没看懂,请反复多看几遍!!

好,我现在认为你已经看懂了~

[self sd_cancelImageLoadOperationWithKey:validOperationKey];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

上面👆的两个方法就是为了防止这种情况的发生,因此先取消对应的图片操作,再重新添加,刚开始先通过key获取operation,如果有operation对象---->取消。当重新产生一个operation对象以后,还是看对应的字典中有没有,有----> 取消(因为现在还没将新产生的operation添加到字典中),没有--->operationDictionary[key] = operation;,将这个operation放到字典中,这样就可以保证一个视图对象只有一个operation在操作图像
在这里也就是说如果设置了cell的网络图片为b,那么就取消掉之前的a的相关操作,这样就不会出现显示错乱的问题了。
作者的想法真的是很聪明呀!

第二个问题

在SDWebImage中常常可以看到options & SDWebImageRefreshCached这种写法,查看SDWebImageRefreshCached的定义可以看到SDWebImageRefreshCached = 1 << 4
例如:a=1 b=2 a== 0000 0001(二进制) b== 0000 0010(二进制) a & b = 0000 0000 (二进制) 十进制为0
也就是说SDWebImageRefreshCached是将1左移4位的一个值,二进制表示为00010000,十进制为16

在接下来的代码中还会看到downloaderOptions | SDWebImageDownloaderLowPriority,这种写法是按位或,也是位运算的一种:
例如:a=5,b=11; 5 ==0000 0101 (二进制) 10==0000 1011(二进制) a | b== 0000 1111(二进制) 十进制为15

如果想了解更多的相关知识,可以参考这篇博客:按位与,按位或

总结

我用了一张流程图来表示这篇文章的内容,方便大家查看

流程图.png

以上是一些我的个人理解,如果有什么不对的地方也希望大家能够指出,互相学习!
这是SDWebImage源码解析的第一篇,下一篇将会对下面产生operation的方法进行分析,欢迎大家关注!

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

推荐阅读更多精彩内容