通俗易懂的SDWebImage源码解析(二)

这是SDWebImage源码解析的第二篇文章,在这篇文章中我们将接着上一篇文章,从下面的方法开始讲起:

/**
 * 如果不存在于缓存中,请下载给定URL的图像,否则返回缓存的版本。
 * url 图像的url
 * options 指定用于此请求的选项的掩码
 * progressBlock 当图像下载中时调用的block,这个进程的回调是在一个后台队列执行
 * completedBlock 当操作完成的回调,这个参数是必须的
 *   completedBlock这个回调无返回值。
     第一个参数为请求到的图像。
     第二个参数为NSData。  
     第三个参数包含一个NSError对象,为了防止图像的参数为空导致的错误。 
     第四个参数是一个SDImageCacheType枚举值,图像是从本地缓存还是内存缓存或者是从网络上获得
     第五个参数,当使用SDWebImageProgressiveDownload,且图像正在下载的时候,这个值是NO。当呈现部分图像的时候会多次调用这个回调,当图像完全下载完毕以后,这个回调会最后一次调用,返回全部的图像,这个参数最后被设定为YES
     最后一个参数是原始的图像url
 *   返回一个遵循SDWebImageOperation的对象,应该是一个SDWebImageDownloaderOperation对象的实例
 *   这个NSObject类是遵循一个协议,这个协议叫做SDWebImageOperation,这个协议很简单,就是一个cancel掉operation的协议.
 */
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                             progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                            completed:(nullable SDInternalCompletionBlock)completedBlock;

可以看到这个方法是非常长的,但是不要怕,先从翻译的注释看起来,通过方法注释大概可以看出,这个下载方法有四个参数,返回一个遵循SDWebImageOperation协议的id类型的对象。
这种写法id <SDWebImageOperation>在我们写代码的过程中也可以参考来使用。

现在我们一行一行的来看这个方法的实现,下面的代码主要做了一下容错。

// 如果没有设置completedBlock来调用这个方法是没有意义的
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

// 有时候xcode不会警告这个类型错误(将NSSTring当做NSURL),所以这里做一下容错
if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}

// 防止app由于类型错误的奔溃,比如传入NSNull代替NSURL
if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}



下面创建了一个继承自NSObjectSDWebImageCombinedOperation对象,至于__block__weak相信大家都了解,我就不多解释了。

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

查看SDWebImageCombinedOperation的具体代码可以看出,这是一个很简单的对象,拥有三个属性,实现了SDWebImageOperation的协议的对应方法,其实就是这里的取消方法。

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
@end

@implementation SDWebImageCombinedOperation

- (void)setCancelBlock:(nullable SDWebImageNoParamsBlock)cancelBlock {
    // 如果该operation已经取消了,我们只是调用回调block
    if (self.isCancelled) {
        if (cancelBlock) {
            cancelBlock();
        }
        _cancelBlock = nil; 
        // 不要忘了设置cacelBlock为nil,否则可能会奔溃
    } else {
        _cancelBlock = [cancelBlock copy];
    }
}

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



接下来主要是对url的一个操作,如果url的长度为0,或者url在失败的url列表中且下载的策略不为SDWebImageRetryFailed那么就抛出错误,并return
当然如果不存在上面的这些错误,就将对应的SDWebImageCombinedOperation对象加入到SDWebImageManagerrunningOperations数组中。

// 创建一个互斥锁防止现在有别的线程修改failedURLs.
// 判断这个url是否是fail过的.如果url failed过的那么isFailedUrl就是true
BOOL isFailedUrl = NO;
if (url) {
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }
}

// 1.如果url的长度为0时候执行
// 2.当前url在失败的URL列表中,且options 不为 SDWebImageRetryFailed   时候执行
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
    // 这里做的就是抛出错误,文件不存在
    [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
    return operation;
}

// 创建一个互斥锁防止现在有别的线程修改runningOperations.
@synchronized (self.runningOperations) {
    [self.runningOperations addObject:operation];
}



做完上述的操作以后,来获取到url对应的cacheKey,也就是说缓存对应的key(如果有缓存的话)

// 通过url来获取到对应的cacheKey
NSString *key = [self cacheKeyForURL:url];

下面是具体实现的代码,对应的作用已经通过注释写在里面了

// 利用Image的URL生成一个缓存时需要的key.
// 这里有两种情况,第一种是如果检测到cacheKeyFilter不为空时,利用cacheKeyFilter来处理URL生成一个key.
// 如果为空,那么直接返回URL的string内容,当做key.
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
    if (!url) {
        return @"";
    }
    // 如果设置了缓存key的过滤器,过滤一下url
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    } else {
        // 否则直接使用url
        return url.absoluteString;
    }
}



下面的方法是个非常长的方法,有100行的代码,其中绝大多数的代码都是在完成的block中,在这里先不看block中的代码,先概览一下这个方法。

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock 

在上面创建的SDWebImageCombinedOperation对象有cacheOperation属性,这个属性是一个NSOperation类型的,通过SDWebImageManagerSDImageCache实例调用上面的方法来返回所需要的这个NSOperation实例。

通过上面的方法名可以大概猜出来这个是来搜索缓存的代码,现在来看一下这个方法的实现。

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
   // 如果key为nil,说明url不对,因此不执行后面的操作了,直接返回Operaion为nil。
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

    // First check the in-memory cache...
   // 检查内存中key对应的缓存,返回图像
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        NSData *diskData = nil;
        if ([image isGIF]) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        // 现在已经找到内存对应的图像缓存了,直接返回
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        return nil;
    }

    // 如果内存中没有,现在检查磁盘的缓存
    NSOperation *operation = [NSOperation new];
    // 新开一个串行队列,在里面执行下面的代码
    // 在这个文件全局搜索,发现_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL); 
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }

        @autoreleasepool {
            // 搜索磁盘缓存,将磁盘缓存加入内存缓存
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];
            // 如果可以获取到磁盘图像,且缓存图像到内存缓存为yes(默认为yes)
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                // 计算出需要花费的内存代销,将该图像缓存到内存中
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            // 在主线程执行对应的回调,这里的缓存类型是磁盘缓存
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });
    return operation;
}

通过👆上面的这个方法,如果能从内存或者磁盘中得到该图像,那么我们就把对应的image,data,SDImageCacheType传递出去(磁盘缓存对应的CacheTypeSDImageCacheTypeDisk,内存缓存对应的CacheTypeSDImageCacheTypeMemory),并且返回新创将的NSOperation对象。如果没有获得图像,那么就不给对应的doneBlock传参,只返回新创建的NSOperation对象。

现在已经知道上面的方法有什么作用,接下来看看那个在查询完缓存block中100行的代码。

在这里需要强烈注意的一点是:这里除了operation.cacheOperationNSOperation类型的对象以外,别的operation的都是继承自NSObject类型的对象,遵循<SDWebImageOperation>协议。

现在我们只需要知道这个block里的代码是在查询完缓存以后调用,无论有没有图片的缓存相关信息。

首先如果当前的这个operation进行了取消操作,在SDWebImageManager的runningOperations移除operation。

// 如果对当前operation进行了取消标记,在SDWebImageManager的runningOperations移除operation
if (operation.isCancelled) {
  [self safelyRemoveOperationFromRunning:operation];
  return;
}

现在我们的SDWebImageManager有一个数组,专门用来存放这些对图片的操作的operation对象,再多嘴一句,这个operation不是继承自NSOperation

@property (strong, nonatomic, nonnull) NSMutableArray<SDWebImageCombinedOperation *> *runningOperations;

如果当前operation的取消标记为YES,在runningOperations中把当前这个operation移除了。

- (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
    @synchronized (self.runningOperations) {
        if (operation) {
            [self.runningOperations removeObject:operation];
        }
    }
}

下面是一个很长的if else的判断,在这里先从简单的else开始看起

else {
            // Image not in cache and download disallowed by delegate
            // 不在缓存中,不被代理允许下载
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            // completedBlock中image和error均传入nil。
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }

通过上面的代码的传参image:nil data:nil error:nil cacheType:SDImageCacheTypeNone可以知道,这里肯定是没有获取到图片和相关数据,所以都传入的是nil
下面的代码就是在主线程直接调用完成的回调。

- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
                             completion:(nullable SDInternalCompletionBlock)completionBlock
                                  image:(nullable UIImage *)image
                                   data:(nullable NSData *)data
                                  error:(nullable NSError *)error
                              cacheType:(SDImageCacheType)cacheType
                               finished:(BOOL)finished
                                    url:(nullable NSURL *)url {
    dispatch_main_async_safe(^{
        if (operation && !operation.isCancelled && completionBlock) {
            completionBlock(image, data, error, cacheType, finished, url);
        }
    });
}



解释完上面的函数,来看看这个判断,这里其实很简单,如果缓存图片存在,就把图片及对应的数据传给回调的方法,看清楚了,这里传递的参数可不是为nil
但是需要注意的是因为这里是从后往前看代码,所以并不是说有缓存图片就一定会走这个方法,肯定是先走if再走else if

else if (cachedImage) {
    // 如果有缓存,返回缓存
    __strong __typeof(weakOperation) strongOperation = weakOperation;
    // 直接执行completedBlock,其中error置为nil即可。
    [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
    [self safelyRemoveOperationFromRunning:operation];
} 

最后来到这个比较长的if判断

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

可以进入这个判断里的几种组合在这里我就列出来了

  1. cachedImagenilSDWebImageManager的代理对象没有实现imageManager:shouldDownloadImageForURL:
    解释:如果现在下载的图片没有缓存,且没有实现代理方法(这个代理方法是我们自己来实现的)。
  2. cachedImagenilSDWebImageManager的代理对象的代理方法返回YES(当图像没有在内存中找到的时候,控制是否下载图像)。(无缓存,但是实现了代理方法且返回YES)
    解释:如果现在下载的图片没有缓存,我们实现了代理方法,但是代理方法返回的是YES
  3. optionsSDWebImageRefreshCachedSDWebImageManager的代理对象没有实现imageManager:shouldDownloadImageForURL:
    解释:如果下载的方法的options没有被手动设定为SDWebImageRefreshCached,且没有实现代理方法(这个代理方法是我们自己来实现的)。
  4. optionsSDWebImageRefreshCachedSDWebImageManager的代理对象的代理方法返回YES(当图像没有在内存中找到的时候,控制是否下载图像)。(无缓存,但是实现了代理方法且返回YES)
    解释:如果下载的方法的options没有被手动设定为SDWebImageRefreshCached,我们实现了代理方法,但是代理方法返回的是YES

上面的这些情况会进入对应的if代码块

下面的代码就是如果可以找到缓存,且optionsSDWebImageRefreshCached,那么就先把缓存的图像数据传递出去

if (cachedImage && options & SDWebImageRefreshCached) {
    // 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.
    [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}



首先downloaderOptions初始值为0,如果这里的options设定了哪些,就给downloaderOptions加上哪些,其实下面的代码就是为什么会有13种的SDWebImageOptions的原因,可以帮着做一些高定制化的工作。
关于这里的&和|符号的详解,可以看我的上一篇文章。

SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;



这里和上面解释过的是同一种,这里的作用就是如果可以找到缓存,且optionsSDWebImageRefreshCached,那么就不让downloaderOptions包含SDWebImageDownloaderProgressiveDownload(渐进式下载),并且让downloaderOptions里必须包含SDWebImageDownloaderIgnoreCachedResponse(忽略缓存)。

if (cachedImage && options & SDWebImageRefreshCached) {
    // force progressive off if image already cached but forced refreshing
    downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
    // ignore image read from NSURLCache if image if cached but force refreshing
    downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}

接下来又出现了一个新的类SDWebImageDownloadToken,参看它的源文件可以发现,这恐怕是迄今为止见到的最简单的类了吧。

// 一个与每个下载关联的Token,可以用来取消下载
@interface SDWebImageDownloadToken : NSObject
@property (nonatomic, strong, nullable) NSURL *url;
@property (nonatomic, strong, nullable) id downloadOperationCancelToken;
@end

@implementation SDWebImageDownloadToken
@end



在上面的代码中已经讲过找对应的缓存来给UIImageView设置图片了。
但是还没有讲如果不存在缓存的话,下载图像的操作和过程,这一部分的内容留在下一篇讲。
下面的代码主要是图片获取成功以后的操作。主要的讲解写在代码的注释中了。

SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
    // block中的__strong 关键字--->防止对象提前释放
    __strong __typeof(weakOperation) strongOperation = weakOperation;
    // 如果operation(SDWebImageCombinedOperation类型)为空,或者operation取消了
    if (!strongOperation || strongOperation.isCancelled) {
        // Do nothing if the operation was cancelled
        // See #699 for more details
        // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
    } else if (error) {
        // 如果发生了错误,就把错误传入对应的回调来处理error
        [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
        // 检查错误类型,确认不是客户端或者服务器端的网络问题,就认为这个url本身问题了。并把这个url放到failedURLs中
        if (   error.code != NSURLErrorNotConnectedToInternet
            && error.code != NSURLErrorCancelled
            && error.code != NSURLErrorTimedOut
            && error.code != NSURLErrorInternationalRoamingOff
            && error.code != NSURLErrorDataNotAllowed
            && error.code != NSURLErrorCannotFindHost
            && error.code != NSURLErrorCannotConnectToHost
            && error.code != NSURLErrorNetworkConnectionLost) {
            @synchronized (self.failedURLs) {
                [self.failedURLs addObject:url];
            }
        }
    }
    else {
        // 如果使用了SDWebImageRetryFailed选项,那么即使该url是failedURLs,也要从failedURLs移除,并继续执行download
        if ((options & SDWebImageRetryFailed)) {
            @synchronized (self.failedURLs) {
                [self.failedURLs removeObject:url];
            }
        }
        // 如果不设定options里包含SDWebImageCacheMemoryOnly,那么cacheOnDisk为YES,表示会把图片缓存到磁盘
        BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
       // 如果options包含SDWebImageRefreshCached,cachedImage有值,但是下载图像downloadedImage为nil,不调用完成的回调completion block
       // 这里的意思就是虽然现在有缓存图片,但是要强制刷新图片,但是没有下载到图片,那么现在就什么都不做,还是使用原来的缓存图片 
        if (options & SDWebImageRefreshCached && cachedImage && !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:)]) {
            /*
             1.图片下载成功了,不是gif图像,且代理实现了imageManager:transformDownloadedImage:withURL:
             2.图片下载成功了,options中包含SDWebImageTransformAnimatedImage,且代理实现了imageManager:transformDownloadedImage:withURL:
              这里做的主要操作是在一个新开的异步队列中对图片做一个转换的操作,例如需要改变原始图片的灰度值等情况
             */
           dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                // 如果获得了新的transformedImage,不管transform后是否改变了图片.都要存储到缓存中
                UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
                if (transformedImage && finished) {
                    BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                    // pass nil if the image was transformed, so we can recalculate the data from the image
                    // 如果图像被转换,则给imageData传入nil,因此我们可以从图像重新计算数据
                    [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                }
                // 将对应转换后的图片通过block传出去
                [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
            });
        } else {
            // 下载好了图片且完成了,存到内存和磁盘,将对应的图片通过block传出去
            if (downloadedImage && finished) {
                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
            }
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
        }
    }
    // 下载结束后移除对应operation
    if (finished) {
        [self safelyRemoveOperationFromRunning:strongOperation];
    }
}];

最后在来讲一些关于SDWebImageManager的相关知识:

  1. SDWebImageManager是一个单例对象(这不是废话🙃)
  2. 它同时拥有两个子单例对象,一个为SDImageCache,另一个为SDWebImageDownloader,一个负责缓存相关的操作,一个负责下载相关的操作。
  3. SDWebImageManager还拥有failedURLs用来存放下载失败的URL地址的数组。
  4. SDWebImageManager还拥有runningOperations用来存放SDWebImageCombinedOperation : NSObject对象。
  5. SDWebImageCombinedOperation的作用就是关联缓存和下载的对象,每当有新的图片地址需要下载的时候,就会产生一个新的SDWebImageCombinedOperation实例。

这篇文章讲的东西还是比较杂,我还是以一张图来总结图来结尾吧!

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

推荐阅读更多精彩内容