九、SDWebImage源码解析之编解码(一)

SDWebImage里自己写了一个编解码管理器,用于实现编码,解码,压缩,缩小图片像素功能。涉及到的文件有SDWebImageCodersManager,SDWebImageCoder,SDWebImageImageIOCoder等等

一、SDWebImageCodersManager
我们先来看SDWebImageCodersManager,SDWebImageCodersManager是一个编码解码管理器,处理多个图片编码解码任务,编码器数组是一个优先级队列,这意味着后面添加的编码器将具有最高优先级。

SDWebImageCodersManager.h

@interface SDWebImageCodersManager : NSObject<SDWebImageCoder>

/**
 Shared reusable instance
 单例
 */
+ (nonnull instancetype)sharedInstance;

/**
 All coders in coders manager. The coders array is a priority queue, which means the later added coder will have the highest priority
 编码器管理器中的所有编码器。编码器数组是一个优先级队列,这意味着后面添加的编码器将具有最高优先级。
 */
@property (nonatomic, strong, readwrite, nullable) NSArray<SDWebImageCoder>* coders;

/**
 Add a new coder to the end of coders array. Which has the highest priority.
增加一个新的coder ,新加入的最先编解码
 @param coder coder
 */
- (void)addCoder:(nonnull id<SDWebImageCoder>)coder;

/**
 Remove a coder in the coders array.
删除一个coder
 @param coder coder
 */
- (void)removeCoder:(nonnull id<SDWebImageCoder>)coder;

初始化方法

+ (nonnull instancetype)sharedInstance {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

- (instancetype)init {
    if (self = [super init]) {
        // initialize with default coders
        _mutableCoders = [@[[SDWebImageImageIOCoder sharedCoder]] mutableCopy]; //初始化SDWebImageImageIOCoder支持PNG,JPEG,TIFF,包括支持渐进解码的解码器
#ifdef SD_WEBP
        [_mutableCoders addObject:[SDWebImageWebPCoder sharedCoder]]; //添加WebP
#endif
        _mutableCodersAccessQueue = dispatch_queue_create("com.hackemist.SDWebImageCodersManager", DISPATCH_QUEUE_CONCURRENT); //创建队列
    }
    return self;
}

添加编码器和删除编码器

//添加编码器
- (void)addCoder:(nonnull id<SDWebImageCoder>)coder { //增加编码器,遵守SDWebImageCoder协议
    if ([coder conformsToProtocol:@protocol(SDWebImageCoder)]) {//该编码器是否实现了SDWebImageCoder协议的方法
        dispatch_barrier_sync(self.mutableCodersAccessQueue, ^{ //同步添加编码器,将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们
            [self.mutableCoders addObject:coder];
        });
    }
}

//删除编码器
- (void)removeCoder:(nonnull id<SDWebImageCoder>)coder {//遵守SDWebImageCoder协议
    dispatch_barrier_sync(self.mutableCodersAccessQueue, ^{//同步删除编码器
        [self.mutableCoders removeObject:coder];
    });
}

coders的getter和setter方法

//coders getter 方法
- (NSArray<SDWebImageCoder> *)coders {
    __block NSArray<SDWebImageCoder> *sortedCoders = nil;
    dispatch_sync(self.mutableCodersAccessQueue, ^{
        sortedCoders = (NSArray<SDWebImageCoder> *)[[[self.mutableCoders copy] reverseObjectEnumerator] allObjects];
    });
    return sortedCoders;
}

//coders setter 方法
- (void)setCoders:(NSArray<SDWebImageCoder> *)coders {
    dispatch_barrier_sync(self.mutableCodersAccessQueue, ^{
        self.mutableCoders = [coders mutableCopy];
    });
}

reverseObjectEnumerator 将数组倒序
e.g.
//1.原始数组
NSMutableArray array = [NSMutableArray arrayWithObjects:@"1",@"2",@"3",nil];
//2.倒序的数组
NSArray
reversedArray = [[array reverseObjectEnumerator] allObjects];

SDWebImageCoder delegate 方法

1.该图片是否可以编码。 遍历self.coders中遵守SDWebImageCoder 的 coder, 然后通过canEncodeToFormat 这个方法来判断是否可以编码

- (BOOL)canEncodeToFormat:(SDImageFormat)format {
    for (id<SDWebImageCoder> coder in self.coders) {
        if ([coder canEncodeToFormat:format]) {
            return YES;
        }
    }
    return NO;
}

2.解码。 遍历self.coders中遵守SDWebImageCoder 的 coder, 然后通过canDecodeFromData 这个方法来判断是否可以解码,如果可以,将图像解码

- (UIImage *)decodedImageWithData:(NSData *)data {
    if (!data) {
        return nil;
    }
    for (id<SDWebImageCoder> coder in self.coders) {
        if ([coder canDecodeFromData:data]) {
            return [coder decodedImageWithData:data];
        }
    }
    return nil;
}

3.压缩图像. 遍历self.coders中遵守SDWebImageCoder 的 coder, 然后通过canDecodeFromData 这个方法来判断是否可以解码,如果可以,将图片压缩显示

- (UIImage *)decompressedImageWithImage:(UIImage *)image
                                   data:(NSData *__autoreleasing  _Nullable *)data
                                options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict {
    if (!image) {
        return nil;
    }
    for (id<SDWebImageCoder> coder in self.coders) {
        if ([coder canDecodeFromData:*data]) {
            return [coder decompressedImageWithImage:image data:data options:optionsDict];
        }
    }
    return nil;
}

4.根据image和format(类型)编码图像

- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format {
    if (!image) {
        return nil;
    }
    for (id<SDWebImageCoder> coder in self.coders) {
        if ([coder canEncodeToFormat:format]) {
            return [coder encodedDataWithImage:image format:format];
        }
    }
    return nil;
}

二、SDWebImageCoder
SDWebImageCoder是单个coder,是管理器的操作对象,SDWebImage里写了两个协议:SDWebImageCoder和SDWebImageProgressiveCoder。SDWebImageCodersManager遵守SDWebImageCoder协议,SDWebImageImageIOCoder等遵守SDWebImageProgressiveCoder,主要用于设置一些协议和公用标识

SDWebImageCoder.h

公共定义

/**
 A Boolean value indicating whether to scale down large images during decompressing. (NSNumber)
 标识是否在压缩图片的时候缩小图片的大小
 */
FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageCoderScaleDownLargeImagesKey;

/**
 Return the shared device-dependent RGB color space created with CGColorSpaceCreateDeviceRGB.

 @return The device-dependent RGB color space
 色彩空间:(Color Space)这是一个色彩范围的容器,类型必须是CGColorSpaceRef.对于这个参数,我们可以传入CGColorSpaceCreateDeviceRGB函数的返回值,它将给我们一个RGB色彩空间。
 */
CG_EXTERN CGColorSpaceRef _Nonnull SDCGColorSpaceGetDeviceRGB(void);

/**
 Check whether CGImageRef contains alpha channel.

 @param imageRef The CGImageRef
 @return Return YES if CGImageRef contains alpha channel, otherwise return NO
 是否有透明度
 */
CG_EXTERN BOOL SDCGImageRefContainsAlpha(_Nullable CGImageRef imageRef);

SDWebImageCoder协议

/**
 Returns YES if this coder can decode some data. Otherwise, the data should be passed to another coder.
 
 @param data The image data so we can look at it
 @return YES if this coder can decode the data, NO otherwise
 */
- (BOOL)canDecodeFromData:(nullable NSData *)data;

/**
 Decode the image data to image.

 @param data The image data to be decoded
 @return The decoded image from data
 */
- (nullable UIImage *)decodedImageWithData:(nullable NSData *)data;

/**
 Decompress the image with original image and image data.

 @param image The original image to be decompressed
 @param data The pointer to original image data. The pointer itself is nonnull but image data can be null. This data will set to cache if needed. If you do not need to modify data at the sametime, ignore this param.
 @param optionsDict A dictionary containing any decompressing options. Pass {SDWebImageCoderScaleDownLargeImagesKey: @(YES)} to scale down large images
 @return The decompressed image
 */
- (nullable UIImage *)decompressedImageWithImage:(nullable UIImage *)image
                                            data:(NSData * _Nullable * _Nonnull)data
                                         options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict;

#pragma mark - Encoding

/**
 Returns YES if this coder can encode some image. Otherwise, it should be passed to another coder.
 
 @param format The image format
 @return YES if this coder can encode the image, NO otherwise
 */
- (BOOL)canEncodeToFormat:(SDImageFormat)format;

/**
 Encode the image to image data.

 @param image The image to be encoded
 @param format The image format to encode, you should note `SDImageFormatUndefined` format is also  possible
 @return The encoded image data
 */
- (nullable NSData *)encodedDataWithImage:(nullable UIImage *)image format:(SDImageFormat)format;

SDWebImageProgressiveCoder协议


@required
/**
 Returns YES if this coder can incremental decode some data. Otherwise, it should be passed to another coder.
 
 @param data The image data so we can look at it
 @return YES if this coder can decode the data, NO otherwise
 */
- (BOOL)canIncrementallyDecodeFromData:(nullable NSData *)data;

/**
 Incremental decode the image data to image.
 
 @param data The image data has been downloaded so far
 @param finished Whether the download has finished
 @warning because incremental decoding need to keep the decoded context, we will alloc a new instance with the same class for each download operation to avoid conflicts
 @return The decoded image from data
 */
- (nullable UIImage *)incrementallyDecodedImageWithData:(nullable NSData *)data finished:(BOOL)finished;

SDWebImageCoder.m比较简单
获取颜色空间和判断图片是否包含透明度

//获取颜色空间
CGColorSpaceRef SDCGColorSpaceGetDeviceRGB(void) {
    static CGColorSpaceRef colorSpace;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        colorSpace = CGColorSpaceCreateDeviceRGB();
    });
    return colorSpace;
}

//判断图片是否包含透明度
BOOL SDCGImageRefContainsAlpha(CGImageRef imageRef) {
    if (!imageRef) {
        return NO;
    }
    CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef);
    BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                      alphaInfo == kCGImageAlphaNoneSkipFirst ||
                      alphaInfo == kCGImageAlphaNoneSkipLast);
    return hasAlpha;
}

三、SDWebImageImageIOCoder,SDWebImageGIFCoder,SDWebImageWebPCoder
这三个类主要实现SDWebImageCoder的SDWebImageProgressiveCoder协议
(1)SDWebImageImageIOCoder
内置的编码器,支持PNG,JPEG,TIFF,包括支持渐进解码。
GIF 只处理第一帧
HEIC 取决于 设备性能
(2)SDWebImageGIFCoder 处理GIF图像的编解码
(3)SDWebImageWebPCoder 处理webP的编解码

四、SDWebImageImageIOCoder
我们首先熟悉下一些定义

static const size_t kBytesPerPixel = 4; //kBytesPerPixel用来说明每个像素占用内存多少个字节,在这里是占用4个字节。(图像在iOS设备上是以像素为单位显示的)。
static const size_t kBitsPerComponent = 8;//kBitsPerComponent表示每一个组件占多少位。这个不太好理解,我们先举个例子,比方说RGBA,其中R(红色)G(绿色)B(蓝色)A(透明度)是4个组件,每个像素由这4个组件组成,那么我们就用8位来表示着每一个组件,所以这个RGBA就是8*4 = 32位。
//知道了kBitsPerComponent和每个像素有多少组件组成就能计算kBytesPerPixel了。计算公式是:(bitsPerComponent * number of components + 7)/8.


/*
 * Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set
 * Suggested value for iPad1 and iPhone 3GS: 60.
 * Suggested value for iPad2 and iPhone 4: 120.
 * Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
 */
static const CGFloat kDestImageSizeMB = 60.0f; //最大支持压缩图像源的大小默认的单位是MB,这里设置了60MB。当我们要压缩一张图像的时候,首先就是要定义最大支持的源文件的大小,不能没有任何限制。

/*
 * Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set
 * Suggested value for iPad1 and iPhone 3GS: 20.
 * Suggested value for iPad2 and iPhone 4: 40.
 * Suggested value for iPhone 3G and iPod 2 and earlier devices: 10.
 */
static const CGFloat kSourceImageTileSizeMB = 20.0f;//原图方块的大小,这个方块将会被用来分割原图,默认设置为20M。

static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;//1M有多少字节
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;//1M有多少像素
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;//目标总像素
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;//原图放款总像素

static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to overlap the seems where tiles meet.重叠像素大小

是否可以解码,根据data判断图片类型,WebP不支持,根据本机类型来判断是否支持SDImageFormatHEIC

- (BOOL)canDecodeFromData:(nullable NSData *)data {
    switch ([NSData sd_imageFormatForImageData:data]) {
        case SDImageFormatWebP:
            // Do not support WebP decoding
            return NO;
        case SDImageFormatHEIC:
            // Check HEIC decoding compatibility
            return [[self class] canDecodeFromHEICFormat];
        default:
            return YES;
    }
}

是否可以编码,并且实现逐渐显示效果,根据data判断图片类型,WebP不支持,根据本机类型来判断是否支持SDImageFormatHEIC

- (BOOL)canIncrementallyDecodeFromData:(NSData *)data {
    switch ([NSData sd_imageFormatForImageData:data]) {
        case SDImageFormatWebP:
            // Do not support WebP progressive decoding
            return NO;
        case SDImageFormatHEIC:
            // Check HEIC decoding compatibility
            return [[self class] canDecodeFromHEICFormat];
        default:
            return YES;
    }
}

解码图像
先通过initWithData获取到一个image,然后获取data的类型
如果是GIF,用 animatedImageWithImages:duration:方法获取完整图像
如果是其他的类型,则通过sd_imageOrientationFromImageData获取orientation,然后合成新的图片
注意: imageWithCGImage: scale: orientation: 这个方法可以准确还原原image的方向,尺寸

- (UIImage *)decodedImageWithData:(NSData *)data {
    if (!data) {
        return nil;
    }
    
    UIImage *image = [[UIImage alloc] initWithData:data];
    
#if SD_MAC
    return image;
#else
    if (!image) {
        return nil;
    }
    
    SDImageFormat format = [NSData sd_imageFormatForImageData:data];
    if (format == SDImageFormatGIF) {
        // static single GIF need to be created animated for `FLAnimatedImage` logic
        // GIF does not support EXIF image orientation
        image = [UIImage animatedImageWithImages:@[image] duration:image.duration];
        return image;
    }
    UIImageOrientation orientation = [[self class] sd_imageOrientationFromImageData:data];
    if (orientation != UIImageOrientationUp) {
        image = [UIImage imageWithCGImage:image.CGImage
                                    scale:image.scale
                              orientation:orientation];
    }
    
    return image;
#endif
}

解码图像 逐渐显示

绘制背景渐变

CGCradientCreateWithColorComponents函数需要四个参数:

色彩空间:(Color Space)这是一个色彩范围的容器,类型必须是CGColorSpaceRef.对于这个参数,我们可以传入CGColorSpaceCreateDeviceRGB函数的返回值,它将给我们一个RGB色彩空间。

颜色分量的数组:这个数组必须包含CGFloat类型的红、绿、蓝和alpha值。数组中元素的数量和接下来两个参数密切。从本质来讲,你必须让这个数组包含足够的值,用来指定第四个参数中位置的数量。所以如果你需要两个位置位置(起点和终点),那么你必须为数组提供两种颜色

位置数组,颜色数组中各个颜色的位置:此参数控制该渐变从一种颜色过渡到另一种颜色的速度有多快。

位置的数量:这个参数指明了我们需要多少颜色和位置。

CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();

CGFloat colors[] =

{

51.0 / 255.0, 160.0 / 255.0, 0.0 / 255.0, 1.00,

68.0 / 255.0, 198.0 / 255.0, 0.0 / 255.0, 1.00,

//        0.0 / 255.0,  50.0 / 255.0, 126.0 / 255.0, 1.00,

};

CGGradientRef myGradient = CGGradientCreateWithColorComponents(rgb, colors, NULL, sizeof(colors)/(sizeof(colors[0])*4));
// Allocate bitmap context

CGContextRef bitmapContext = CGBitmapContextCreate(NULL, 320, TITLE_CONTROL_HEIGHT, 8, 4 * 320, CGColorSpaceCreateDeviceRGB(), kCGImageAlphaNoneSkipFirst);

创建好线性渐变后,我们将使用CGContextDrawLinearGradient过程在图形上下文中绘制,此过程需要五个参数:

Graphics context 指定用于绘制线性渐变的图形上下文。

Axial gradient 我们使用CGGradientCreateWithColorComponents函数创建的线性渐变对象的句柄

start point 图形上下文中的一个CGPoint类型的点,表示渐变的起点。

End Point表示渐变的终点。

Gradient drawing options 当你的起点或者终点不在图形上下文的边缘内时,指定该如何处理。你可以使用你的开始或结束颜色来填充渐变以外的空间。此参数为以下值之一:KCGGradientDrawsAfterEndLocation扩展整个渐变到渐变的终点之后的所有点 KCGGradientDrawsBeforeStartLocation扩展整个渐变到渐变的起点之前的所有点。0不扩展该渐变。

- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished {
    if (!_imageSource) {
        _imageSource = CGImageSourceCreateIncremental(NULL);
    }
    UIImage *image;
    
    // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
    // Thanks to the author @Nyx0uf
    
    // Update the data source, we must pass ALL the data, not just the new bytes
    CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
    
    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); //将图片高度绑定在_height上
            val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); //获取图片宽度信息
            if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);//将图片宽度绑定在_width上
            val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); //获取图像方向信息
            if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);//将图片的方向信息绑定在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 didCompleteWithError.) So save it here and pass it on later.
#if SD_UIKIT || SD_WATCH
            _orientation = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:orientationValue]; //转换方向信息
#endif
        }
    }
    
    if (_width + _height > 0) {
        // Create the image
        CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL); //获取图像
        
#if SD_UIKIT || SD_WATCH
        // Workaround for iOS anamorphic image
        if (partialImageRef) {
            const size_t partialHeight = CGImageGetHeight(partialImageRef);
            CGColorSpaceRef colorSpace = SDCGColorSpaceGetDeviceRGB();//色彩空间
            CGContextRef bmContext = CGBitmapContextCreate(NULL, _width, _height, 8, 0, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); //创建上下文
            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) { //拿到CGImageRef 转换成image并且返回
#if SD_UIKIT || SD_WATCH
            image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:_orientation];
#elif SD_MAC
            image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
#endif
            CGImageRelease(partialImageRef);
        }
    }
    
    if (finished) { //下载完成,销毁对象
        if (_imageSource) {
            CFRelease(_imageSource);
            _imageSource = NULL;
        }
    }
    
    return image;
}

压缩图片

- (UIImage *)decompressedImageWithImage:(UIImage *)image
                                   data:(NSData *__autoreleasing  _Nullable *)data
                                options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict {
#if SD_MAC
    return image;
#endif
#if SD_UIKIT || SD_WATCH
    BOOL shouldScaleDown = NO;
    if (optionsDict != nil) { //判断是否有在压缩过程中缩小图片尺寸的标识
        NSNumber *scaleDownLargeImagesOption = nil;
        if ([optionsDict[SDWebImageCoderScaleDownLargeImagesKey] isKindOfClass:[NSNumber class]]) {
            scaleDownLargeImagesOption = (NSNumber *)optionsDict[SDWebImageCoderScaleDownLargeImagesKey];
        }
        if (scaleDownLargeImagesOption != nil) {
            shouldScaleDown = [scaleDownLargeImagesOption boolValue];
        }
    }
    if (!shouldScaleDown) {//如果没有压缩过程中缩小图片尺寸的标识
        return [self sd_decompressedImageWithImage:image];
    } else {//如果有压缩过程中缩小图片尺寸的标识
        UIImage *scaledDownImage = [self sd_decompressedAndScaledDownImageWithImage:image];
        if (scaledDownImage && !CGSizeEqualToSize(scaledDownImage.size, image.size)) {
            // if the image is scaled down, need to modify the data pointer as well
            SDImageFormat format = [NSData sd_imageFormatForImageData:*data];
            NSData *imageData = [self encodedDataWithImage:scaledDownImage format:format]; //解码
            if (imageData) {
                *data = imageData;
            }
        }
        return scaledDownImage;
    }
#endif
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容