RN图片加载和原生统一

RN图片加载和原生统一

针对RN和原生混合开发的项目,由于图片的加载RN有自己的一套机制,跟原生的是分开的,就存在加载和缓存的差异性;我们可以做一些工作让图片的加载统一成一套,这对于维护和做一些优化都是有益处的

1. RN图片加载框架

在对RN的图片加载做优化之前,我们得先知道RN的图片加载框架流程;

RN的图片的加载、缓存、解码等都是在RCTImageLoader中处理的,这里大概梳理了下其中的结构

RCTImageLoader.jpg

RCTImageLoader可以划分为以下几个模块:

  • 图片加载 RCTImageURLLoader
  • 图片缓存 RCTImageCache
  • 图片解码 RCTImageDataDecoder

为了增强模块的可扩展性,RN将这三个核心模块都提供了外部可定制的能力,通过set方法、或者协议的方式;

1.1 图片的缓存

定义了RCTImageCache协议,同时提供了- (void)setImageCache:(id<RCTImageCache>)cache;来供外部去定义图片缓存模块

/**
 * Provides an interface to use for providing a image caching strategy.
 */
@protocol RCTImageCache <NSObject>

- (UIImage *)imageForUrl:(NSString *)url
                    size:(CGSize)size
                   scale:(CGFloat)scale
              resizeMode:(RCTResizeMode)resizeMode;

- (void)addImageToCache:(UIImage *)image
                    URL:(NSString *)url
                   size:(CGSize)size
                  scale:(CGFloat)scale
             resizeMode:(RCTResizeMode)resizeMode
               response:(NSURLResponse *)response;

@end

如果我们设置了自定义的图片缓存,那么就使用自定义的,否则RN内部会使用默认的RCTImageCache;这个默认的实现只做了内存缓存,没有做磁盘缓存的

// 提供set方法供外部设置图片缓存模块
- (void)setImageCache:(id<RCTImageCache>)cache;

- (void)setImageCache:(id<RCTImageCache>)cache
{
    if (_imageCache) {
        RCTLogWarn(@"RCTImageCache was already set and has now been overriden.");
    }
    _imageCache = cache;
}

// 如果设置了图片缓存模块则用外部设置的,否则使用默认实现
- (id<RCTImageCache>)imageCache
{
    if (!_imageCache) {
        //set up with default cache
        _imageCache = [RCTImageCache new];
    }
    return _imageCache;
}
1.2 图片加载

图片加载的统一入口函数:

- (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest
                                                      size:(CGSize)size
                                                     scale:(CGFloat)scale
                                                   clipped:(BOOL)clipped
                                                resizeMode:(RCTResizeMode)resizeMode
                                             progressBlock:(RCTImageLoaderProgressBlock)progressBlock
                                          partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadBlock
                                           completionBlock:(RCTImageLoaderCompletionBlock)completionBlock;

同时定义了RCTImageURLLoader协议,将图片的加载能力进行抽象;协议中的canLoadImageURL:方法用来定义该Loader支持的URL资源的加载,这样不同的图片资源就可以使用不同的Loader去加载

/**
 * Provides the interface needed to register an image loader. Image data
 * loaders are also bridge modules, so should be registered using
 * RCT_EXPORT_MODULE().
 */
@protocol RCTImageURLLoader <RCTBridgeModule>

// 是否是该loader支持加载的图片URL
- (BOOL)canLoadImageURL:(NSURL *)requestURL;

// 图片加载入口函数
- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
                                              size:(CGSize)size
                                             scale:(CGFloat)scale
                                        resizeMode:(RCTResizeMode)resizeMode
                                   progressHandler:(RCTImageLoaderProgressBlock)progressHandler
                                partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
                                 completionHandler:(RCTImageLoaderCompletionBlock)completionHandler;

@optional

// loader 优先级
- (float)loaderPriority;

// 是否需要将任务放到内部的串行队列去执行,默认是YES在主线程执行
- (BOOL)requiresScheduling;

// 是否缓存图片,默认是YES
- (BOOL)shouldCacheLoadedImages;

@end

RCTImageCache不同的是,图片加载loader不是通过提供set接口去定制,而是通过RCT_EXPORT_MODULE()的方式导出模块给RN,内部去获取实现了RCTImageURLLoader协议的loaders列表_loaders = [_bridge modulesConformingToProtocol:@protocol(RCTImageURLLoader)]

- (id<RCTImageURLLoader>)imageURLLoaderForURL:(NSURL *)URL
{
    if (!_maxConcurrentLoadingTasks) {
        [self setUp];
    }

    if (!_loaders) {
        // Get loaders, sorted in reverse priority order (highest priority first)
        RCTAssert(_bridge, @"Bridge not set");
        _loaders = [[_bridge modulesConformingToProtocol:@protocol(RCTImageURLLoader)] sortedArrayUsingComparator:^NSComparisonResult(id<RCTImageURLLoader> a, id<RCTImageURLLoader> b) {
            float priorityA = [a respondsToSelector:@selector(loaderPriority)] ? [a loaderPriority] : 0;
            float priorityB = [b respondsToSelector:@selector(loaderPriority)] ? [b loaderPriority] : 0;
            if (priorityA > priorityB) {
                return NSOrderedAscending;
            } else if (priorityA < priorityB) {
                return NSOrderedDescending;
            } else {
                return NSOrderedSame;
            }
        }];
    }
    // ...
    // Normal code path
    for (id<RCTImageURLLoader> loader in _loaders) {
        if ([loader canLoadImageURL:URL]) {
            return loader;
        }
    }
    return nil;
}

RN图片加载模块默认实现了2类资源的Loader

  • RCTPhotoLibraryImageLoader 相册资源
  • RCTLocalAssetImageLoader 本地资源

对于网络图片的加载,没有内置的Loader,假如外部没有定义该类型的loader则默认走的RCTNetworking模块去加载的

我们如果想对于网络资源图片的加载走跟原生一样的模块(比如SDWebImage)则可以通过2种方式去实现:

  1. 可以通过定义一个Loader并通过RCT_EXPORT_MODULE()的方式导出模块给RN就能实现
  2. hook内部的默认网络资源加载函数loadImageWithURLRequest:
1.3 图片解码

针对请求返回的是data类型的数据,则需要去将data解码成图片;通过定义了RCTGIFImageDecoder协议来将解码的能力抽象,外部则可以实现对应的解码器来解码不同类型的数据,这里的设计跟上面介绍的Loader的设计是一样的

/**
 * Provides the interface needed to register an image decoder. Image decoders
 * are also bridge modules, so should be registered using RCT_EXPORT_MODULE().
 */
@protocol RCTImageDataDecoder <RCTBridgeModule>

// 判断数据是否是该loader可以解码的
- (BOOL)canDecodeImageData:(NSData *)imageData;

// 解码函数,传入imageData解码得到image
- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
                                              size:(CGSize)size
                                             scale:(CGFloat)scale
                                        resizeMode:(RCTResizeMode)resizeMode
                                 completionHandler:(RCTImageLoaderCompletionBlock)completionHandler;

@optional

// 优先级,内部会根据这个来排序,使用优先级高的decoder
- (float)decoderPriority;

@end

RN模块默认内置了一个RCTGIFImageDecodergif的解码模块,假如我们需要支持WebP,那么就可以定义一个WebP的Decoder来实现解码

2. RN和原生的图片缓存统一

由于RN和原生的图片加载是2个模块去实现的,这就存在一张图可能RN侧加载缓存了、原生侧也加载缓存了,这就造成了资源的重复加载以及无法复用缓存的问题;

同时RN的缓存模块还只是做了内存缓存的,app杀掉下次打开则还是又会发起网络请求去加载,这就造成了不必要的请求浪费

为了解决这些问题,将两端的图片缓存统一就显得有必要,同时统一了之后,后续需要做修改或者优化则不用2端都去修改,增强了可维护性

2.1 自定义ImageCache

RCTImageLoader也提供了缓存协议以及设置缓存的函数,自定制起来也很简单,定义一个Cache实现RCTImageCache协议

@interface HCRNImageCache : NSObject <RCTImageCache>

@end

项目用的是SDWebImage做图片加载的,那么缓存的内部实现就是用SD那一套,只需要将缓存的key跟SD保持一致,那么读取和写入就统一了

@implementation HCRNImageCache

#pragma mark - RCTImageCache

- (void)addImageToCache:(UIImage *)image URL:(NSString *)url size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode response:(NSURLResponse *)response {
    NSString *cacheKey = [HCImageLoaderUtility cacheKeyWithUrlString:url size:size scale:scale];
    if ([self isURLInBlackList:url]) {
        [[SDImageCache sharedImageCache] storeImage:image forKey:cacheKey toDisk:NO completion:nil];
    } else {
        [[SDImageCache sharedImageCache] storeImage:image forKey:cacheKey toDisk:YES completion:nil];
    }
}

- (UIImage *)imageForUrl:(NSString *)url size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode {
    NSString *cacheKey = [HCImageLoaderUtility cacheKeyWithUrlString:url size:size scale:scale];
    if ([self isURLInBlackList:url]) {
        return [[SDImageCache sharedImageCache] imageFromMemoryCacheForKey:cacheKey];
    } else {
        return [[SDImageCache sharedImageCache] imageFromCacheForKey:cacheKey];
    }
}

#pragma mark - Private Methods
// 这里用来判断哪些资源不是网络资源:比如本地资源或者RN调试模式的本地资源
- (BOOL)isURLInBlackList:(NSString *)url {
    return [HCImageLoaderUtility isLocalAssetURL:url];
}

@end

这里我将用到的一些工具方法抽到一个工具类中

// 由于有一些是工具方法,就抽出来一个工具类来
@implementation HCImageLoaderUtility

// 根据URL返回缓存的key,内部实现就是SD那一套
+ (NSString *)cacheKeyWithUrlString:(NSString *)urlString size:(CGSize)size scale:(CGFloat)scale {
    NSString *cacheKey = [self sdCacheKeyWithUrlString:urlString];
    
    return cacheKey;
}

+ (NSString *)sdCacheKeyWithUrlString:(NSString *)urlString {
    NSString *cacheKey = [[SDWebImageManager sharedManager] cacheKeyForURL:[NSURL URLWithString:urlString]];
    
    return cacheKey;
}

// 判断URL是否是本地的资源文件
+ (BOOL)isLocalAssetURL:(NSString *)url {
    // 调试RN模式
    BOOL isDebugMode = [url rangeOfString:@"http"].location != NSNotFound && [url rangeOfString:@"8081/assets"].location != NSNotFound;
    // 加载本地图片文件
    BOOL isLocalAsset = [url rangeOfString:@"file://"].location != NSNotFound;
    
    return isDebugMode || isLocalAsset;
}

+ (BOOL)isURLNotSupportFormat:(NSString *)url {
    // 非阿里云返回原地址; 动图返回原地址
    if (![url containsString:@"oss"] || ![url containsString:@"aliyuncs.com"] || [url containsString:@".gif"]) {
        return YES;
    }
    
    return NO;
}

+ (BOOL)checkNeedSetSizeCompressFormatWithURLString:(NSString *)url size:(CGSize)size scale:(CGFloat)scale {
    CGFloat width = size.width * scale;
    CGFloat height = size.height * scale;
    BOOL willSetSize = YES;
    if (width <= 0 || width > 4096 || height <= 0 || height > 4096) {
        willSetSize = NO;
    }
    
    return willSetSize;
}
@end
2.2 注入自定义ImageCache到RN模块

注入则需要注意注入的时机,以及当bridge reload之后需要重新注入(reload之后会重新加载RN模块,RN模块load完成会有一个通知RCTJavaScriptDidLoadNotification);这里我们使用一个管理类来处理注入的逻辑,监听这个RN模块load完成的通知,然后去设置自定义的ImageCache即可

@interface HCRNImageLoader ()

@property (nonatomic, nullable, strong) HCRNImageCache *imageCache;

@end

@implementation HCRNImageLoader

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

+ (instancetype)sharedInstance {
    static HCRNImageLoader *_instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [self new];
    });
    
    return _instance;
}

- (void)startObserver {
    [self addNotifications];
}

- (void)registerHCImageLoader {
    // 你的项目的RCTbridge实例
    SomeBridge.imageLoader setImageCache:self.imageCache];
}

- (void)unregisterHCImageLoader {
    self.imageCache = nil;
    SomeBridge.imageLoader setImageCache:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

#pragma mark - Private Methods

- (void)addNotifications {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jsBridgeReloaded) name:RCTJavaScriptDidLoadNotification object:nil];
}

- (void)jsBridgeReloaded {
    [self registerHCImageLoader];
}

#pragma mark - Getter && Setter

- (HCRNImageCache *)imageCache {
    if (_imageCache == nil) {
        _imageCache = [HCRNImageCache new];
    }
    
    return _imageCache;
}

@end
3. RN和原生的图片加载统一

图片的加载统一上面也介绍了有2种方式可以实现,自定义RCTImageURLLoader或者hook RN的图片加载入口函数loadImageWithURLRequest:

下面介绍定义loader的方式:

@implementation HCRNURLImageLoader

RCT_EXPORT_MODULE()

- (BOOL)canLoadImageURL:(NSURL *)requestURL {
// 这里的逻辑实现根据项目的图片资源的URL格式来实现即可
    return (!requestURL.isFileURL && [requestURL.absoluteString containsString:@"https://"]);
}

- (BOOL)shouldCacheLoadedImages {
    return YES;
}

- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler {
    // 取缓存
    NSString *cacheKey = [HCRNImageLoaderUtility cacheKeyWithUrlString:imageURL.absoluteString size:size scale:scale];
    UIImage *cachedImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:cacheKey];
    if (cachedImage) {
        if (completionHandler) {
            completionHandler(nil, cachedImage);
        }
    } else {
        // 取网络
        [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:imageURL
                                                              options:0
                                                             progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            if (progressHandler) {
                progressHandler(receivedSize, expectedSize);
            }
        } completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
            if (image) {
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                    [[SDImageCache sharedImageCache] storeImage:image forKey:cacheKey completion:nil];
                });
            }
            if (completionHandler) {
                completionHandler(error, image);
            }
        }];
    }

    return ^{};
}

需要注意的是如果自己实现了Loader,则对应资源的加载直接托管给Loader去实现了,我们需要在loader中处理缓存、解码等这一套流程

RCTImageLoader代码片段截取

if (loadHandler) {
            cancelLoad = [loadHandler loadImageForURL:request.URL
                                                 size:size
                                                scale:scale
                                           resizeMode:resizeMode
                                      progressHandler:progressHandler
                                   partialLoadHandler:partialLoadHandler
                                    completionHandler:^(NSError *error, UIImage *image) {
                                        completionHandler(error, image, nil);
                                    }];
        } else {
            UIImage *image;
            if (cacheResult) {
                image = [[strongSelf imageCache] imageForUrl:request.URL.absoluteString
                                                        size:size
                                                       scale:scale
                                                  resizeMode:resizeMode];
            }

            if (image) {
                completionHandler(nil, image, nil);
            } else {
                // Use networking module to load image
                cancelLoad = [strongSelf _loadURLRequest:request
                                           progressBlock:progressHandler
                                         completionBlock:completionHandler];
            }
        }

你可以选择这种方式定义一个Loader去处理,也可以不做处理让走默认的图片加载流程

4. 图片加载的一些优化

图片加载优化除了缓存之外,还包括按需加载(按视图尺寸加载)、压缩(压缩参数、WebP格式等等)、裁剪(圆角)等等,现在主流的文件托管平台都支持通过配置参数来获取定制化的图片资源

这些通过定制URL的图片格式化参数来加载图片带来的益处是值得去做的

  • 按视图尺寸加载 -- 减少了视图渲染的Color Misaligned Images
  • 压缩参数 -- 降低了图片资源的大小,提升了下载的效率
  • 裁剪等参数 -- 不需要代码去处理特殊的效果,通常一些效果还会触发离屏渲染

项目中图片是放在阿里云OSS上,我们可以根据文档来设置这些参数图片缩放

那么想让RN的图片加载也可以去设置图片处理的参数,当然也可以在自定义的Loader中去处理URL拼接format参数,或者不自定义Loader的话直接hook RN图片加载的入口函数loadImageWithURLRequest:

@implementation RCTImageLoader (HCLoader)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethod:@selector(loadImageWithURLRequest:size:scale:clipped:resizeMode:progressBlock:partialLoadBlock:completionBlock:) with:@selector(hc_loadImageWithURLRequest:size:scale:clipped:resizeMode:progressBlock:partialLoadBlock:completionBlock:)];
    });
}

- (RCTImageLoaderCancellationBlock)hc_loadImageWithURLRequest:(NSURLRequest *)imageURLRequest
                                                           size:(CGSize)size
                                                          scale:(CGFloat)scale
                                                        clipped:(BOOL)clipped
                                                     resizeMode:(RCTResizeMode)resizeMode
                                                  progressBlock:(RCTImageLoaderProgressBlock)progressBlock
                                               partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadBlock
                                                completionBlock:(RCTImageLoaderCompletionBlock)completionBlock {
    NSString *formattedURLString = [self formatURLWithURLString:imageURLRequest.URL.absoluteString size:size scale:scale mode:resizeMode];
    NSMutableURLRequest *tmpURLRequest = imageURLRequest.mutableCopy;
    tmpURLRequest.URL = [NSURL URLWithString:formattedURLString];
    return [self hc_loadImageWithURLRequest:tmpURLRequest
                                         size:size
                                        scale:scale
                                      clipped:clipped
                                   resizeMode:resizeMode
                                progressBlock:progressBlock
                             partialLoadBlock:partialLoadBlock
                              completionBlock:completionBlock];
}

- (NSString *)formatURLWithURLString:(NSString *)urlString size:(CGSize)size scale:(CGFloat)scale mode:(RCTResizeMode)resizeMode {
    BOOL isInBlackList = [self isURLInBlackList:urlString];
    if (isInBlackList) { // 不需要拼接参数的直接返回原URL
        return urlString;
    }
    
    // 根据传入的width、height、scale、resizeMode来拼接url的format参数,格式类似这样 ?x-oss-process=image/resize,m_lfit,w_148,h_148/format,webp
    BOOL needSetSizeFormat = [HCImageLoaderUtility checkNeedSetSizeCompressFormatWithURLString:urlString size:size scale:scale];
    // ?x-oss-process=image/resize,m_lfit,w_148,h_148/format,webp
    NSString *resizeModeString = @"lfit"; // oss默认值
    switch (resizeMode) {
        case RCTResizeModeCover: // UIViewContentModeScaleAspectFill
            resizeModeString = @"fill";
            break;
        case RCTResizeModeContain: // UIViewContentModeScaleAspectFit
            resizeModeString = @"pad";
            break;
        case RCTResizeModeStretch: // UIViewContentModeScaleToFill
            resizeModeString = @"fixed";
            break;
        case RCTResizeModeCenter: // UIViewContentModeCenter
            resizeModeString = @"fill";
            break;
        default:
            resizeModeString = @"lfit";
            break;
    }
    NSMutableString *formattedString = urlString.mutableCopy;
    // 缩放配置
    [formattedString appendFormat:@"?x-oss-process=image/resize,m_%@", resizeModeString];
    if (needSetSizeFormat) {
        // 宽高设置
        [formattedString appendFormat:@",w_%.0f,h_%.0f", ceil(size.width * scale), ceil(size.height * scale)];
    }
    // WebP格式设置
    [formattedString appendFormat:@"/format,webp"];
    return formattedString;
}

// 这里根据需求,将不支持参数的URL、或者不需要拼接参数的URL过滤掉
- (BOOL)isURLInBlackList:(NSString *)url {
    BOOL isLocalAsset = [HCImageLoaderUtility isLocalAssetURL:url];
    BOOL isNotSupportFormat = [HCImageLoaderUtility isURLNotSupportFormat:url];
    
    return isLocalAsset || isNotSupportFormat;
}

@end

我们设置了图片格式WebP,那么默认的RCTImageLoader是么有WebP格式的Decoder,此时就需要实现一个供其使用,定义一个实现协议RCTImageDataDecoder的解码器


@interface HCRNWebPImageDecoder : NSObject <RCTImageDataDecoder>

@end

@implementation HCRNWebPImageDecoder

RCT_EXPORT_MODULE()

- (BOOL)canDecodeImageData:(NSData *)imageData {
    return [[SDWebImageWebPCoder sharedCoder] canDecodeFromData:imageData];
}

- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
                                              size:(CGSize)size
                                             scale:(CGFloat)scale
                                        resizeMode:(RCTResizeMode)resizeMode
                                 completionHandler:(RCTImageLoaderCompletionBlock)completionHandler {
    UIImage *image = [[SDWebImageWebPCoder sharedCoder] decodedImageWithData:imageData];
    if (completionHandler) {
        if (image) {
            completionHandler(nil, image);
        } else {
            completionHandler([NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:@{NSLocalizedFailureReasonErrorKey : @"解码失败"}], nil);
        }
    }
    
    return ^{};
}

@end

至此RN的图片加载就也支持设置format参数获取处理过的图片,同时也支持WebP格式的图片的加载了

5. 总结

在做RN的图片加载模块优化的时候,阅读了RCTImageLoader的源码,也学到了一些设计的理念,模块划分很清晰:Cache、Loader、Decoder;同时也提供了接口或者协议的方式将能力抽象,供外部可定制化。这样在做定制的时候就很清晰,而不用去各种hook方法去实现。

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