SDWebImage源码阅读-图片处理(图片解压缩)

解码

SDWebImageDownloaderOperationdidCompleteWithError中图片下载完成,开始解析图片:

      ......
      dispatch_async(self.coderQueue, ^{
        @autoreleasepool {
            UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
            CGSize imageSize = image.size;
            if (imageSize.width == 0 || imageSize.height == 0) {
                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
            } else {
                [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
            }
            [self done];
        }
    });
      ......

coderQueue是个串行队列:

_coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);

接下来调用图片解码的方法SDImageLoaderDecodeImageData

先不考虑动图的解码,这里首先获取图片的scale。scale可以在请求图片的context中通过SDWebImageContextImageScaleFactor设置。如果没有特别指定,则通过SDImageScaleFactorForKey(cacheKey)方法获取。cacheKey默认就是图片的地址,SDImageScaleFactorForKey根据图片地址中是否含有@2x、@3x、%402x、%403x来决定图片的scale,默认是1。

之后调用SDImageCodersManager来解析图片,将NSData转为UIImage:

image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:coderOptions];

SDImageCodersManager中有一个数组来保存Decoder:_imageCoders。_imageCoders的初始化:

_imageCoders = [NSMutableArray arrayWithArray:@[[SDImageIOCoder sharedCoder], [SDImageGIFCoder sharedCoder], [SDImageAPNGCoder sharedCoder]]];

默认定义了三个ImageCoder。SDImageCodersManager在解析图片时会先询问ImageCoder是否能解码该格式的图片:

if ([coder canDecodeFromData:data]) {
  ......

SDImageIOCoder除了WebP之外基本都能解析。

看看SDImageIOCoder解析图片的方法:

- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
    if (!data) {
        return nil;
    }
    CGFloat scale = 1;
    NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
    if (scaleFactor != nil) {
        scale = MAX([scaleFactor doubleValue], 1) ;
    }
    
    UIImage *image = [[UIImage alloc] initWithData:data scale:scale];
    image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
    return image;
}

基本上就是调用了系统利用NSData创建UIImage的方法。

解压缩

到这里图片解码已经完成,得到了UIImage,但这时UIImage还不是位图,如果要显示到UIImageView上面,还要经过一次解压缩。如果我们直接把这个UIImage传给UIImageView,那UIImageView会帮我们做这个解压缩的操作,但有可能会卡主线程。如果我们解压缩完成之后再传给UIImageView,那图片显示的效率会高很多。所以接下来SDWebImage开始对图片进行解压缩。我们也可以设置context中的SDWebImageAvoidDecodeImage来禁止自动解压缩。另外如果是动图也不会进行解压缩:

        BOOL shouldDecode = (options & SDWebImageAvoidDecodeImage) == 0;
        if ([image conformsToProtocol:@protocol(SDAnimatedImage)]) {
            // `SDAnimatedImage` do not decode
            shouldDecode = NO;
        } else if (image.sd_isAnimated) {
            // animated image do not decode
            shouldDecode = NO;
        }
              ......

解压缩在SDImageCoderHelper中的decodedImageWithImage方法进行。首先判断是否需要进行解压缩。解压缩过的或者动图都不需要解压。SDWebImage用sd_isDecoded来标记解压缩过的图片。

接下来来到下面这个方法,进行解压缩的操作:

+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
  ......

这个方法不是很长,核心的函数就是CGBitmapContextCreate,这个函数用于创建一个位图上下文,用来绘制一张宽 width 像素,高 height 像素的位图:

CGContextRef CGBitmapContextCreate(void *data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef space, uint32_t bitmapInfo);

我们在这个context上绘制的UIImage会被渲染成位图。

位图

位图就是像素数组,每个像素有固定的格式,称为像素格式,它由以下三个参数决定:

  • 颜色空间
  • 一个像素中每个独立的颜色分量使用的 bit 数(Bits per component)
  • 透明值(CGBitmapInfo)

颜色空间
颜色空间是对色彩的一种描述方式,主要有6种:RGB、CMY/CMYK、HSV/HSB、HSI/HSL、Lab、YUV/YCbCr。

比如RGB是通过红绿蓝三原色来描述颜色的颜色空间,R=Red、G=Green、B=Blue。RGB颜色空间下,一个像素由R、G、B三个颜色分量表示,每个分量使用的bit 数就是bpc。若每个分量用8位,那么一个像素共用24位表示,24就是像素的深度

最常用的就是RGB和CMYK。同一个色值在不同的颜色空间下表现出来是不同的颜色。

比如我们拿一个RGB格式的图片去打印,会发现打印出来的颜色和我们在电脑上面看到的有色差,这就是因为颜色空间不同导致的,因为打印机的颜色空间是CMYK。

PBC
然后这个的PBC就是一个像素中每个独立的颜色分量使用的 bit 数。

颜色分量是什么?比如RGB是通过红绿蓝三原色来描述颜色的颜色空间,R=Red、G=Green、B=Blue,也就是红绿蓝。RGB颜色空间下,一个像素就由R、G、B三个颜色分量表示,这个就是颜色分量。每个分量使用的bit 数就是bpc。

如果每个分量用8位,那么一个像素共用24位表示,24就是像素的深度。再加上如果有透明度信息,那就是8888,一共有32位也就是4个字节,就是我们前面说的iOS中每个像素所占的字节数。

BitmapInfo
然后还有BitmapInfo。BitmapInfo就是用来说明每个像素中的bits包含了哪些信息。有以下三个方面:

  • 是否包含Alpha通道,如果包含 alpha ,那么 alpha 信息所处的位置,在像素的最低有效位,比如 RGBA ,还是最高有效位,比如 ARGB ;
  • 如果包含 alpha ,那么每个颜色分量是否已经乘以 alpha 的值,这种做法可以加速图片的渲染时间,因为它避免了渲染时的额外乘法运算。比如,对于 RGB 颜色空间,用已经乘以 alpha 的数据来渲染图片,每个像素都可以避免 3 次乘法运算,红色乘以 alpha ,绿色乘以 alpha 和蓝色乘以 alpha
  • 颜色分量是否为浮点数

iOS中,alpha通道的布局信息是一个枚举值 CGImageAlphaInfo ,有以下几种情况:

typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {
    kCGImageAlphaNone,               /* For example, RGB. */
    kCGImageAlphaPremultipliedLast,  /* For example, premultiplied RGBA */
    kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */
    kCGImageAlphaLast,               /* For example, non-premultiplied RGBA */
    kCGImageAlphaFirst,              /* For example, non-premultiplied ARGB */
    kCGImageAlphaNoneSkipLast,       /* For example, RBGX. */
    kCGImageAlphaNoneSkipFirst,      /* For example, XRGB. */
    kCGImageAlphaOnly                /* No color data, alpha data only */
};
  • kCGImageAlphaNone : 无alpha通道
  • kCGImageAlphaOnly:无颜色数据,只有alpha通道
  • kCGImageAlphaNoneSkipLastkCGImageAlphaNoneSkipFirst :有alpha通道,但是忽略了alpha值,即透明度不起作用。两者的区别是alpha通道所在的位置
  • kCGImageAlphaLastkCGImageAlphaFirst:有alpha通道,且alpha通道起作用,两者的区别是alpha通道所在的位置不同
  • kCGImageAlphaPremultipliedLastkCGImageAlphaPremultipliedFirst :有alpha通道,且alpha通道起作用。这两个值的区别是alpha通道坐在的位置不同。和kCGImageAlphaLast、kCGImageAlphaFirst的区别是:带有Premultiplied,在解压缩的时候就将透明度乘到每个颜色分量上,这样渲染的时候就不用再处理alpha通道,提高了渲染的效率。

对于位图来说,像素格式并不是随意组合的,目前只支持以下有限的 17 种特定组合:

iOS支持的只有8种,除去无颜色空间的和灰色的之外,只剩下RGB的5种,所有iOS 并不支持 CMYK 的颜色空间。

根据苹果官方文档的介绍,如果图片无alpha通道,则应该使用kCGImageAlphaNoneSkipFirst,如果图片含alpha通道,则应该使用kCGImageAlphaPremultipliedFirst

如果我们拿不在列表里面的像素格式去创建位图上下文会创建失败,比如下面这中,bpc为8,但bpp为16:

CGBitmapInfo bitmapInfo = kCGBitmapByteOrder16Host;
    bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
    CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);

这是就会得到以下提示:

CGBitmapContextCreate: unsupported parameter combination: set CGBITMAP_CONTEXT_LOG_ERRORS environmental variable to see the details

看看不同的像素格式下,一个像素是被如何表示的:

image

CGBitmapContextCreate

现在回到系统的CGBitmapContextCreate函数,看看它的参数分别有什么含义:

CGContextRef CGBitmapContextCreate(void *data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef space, uint32_t bitmapInfo);

data:一个指针,它应该指向一块大小至少为 bytesPerRow * height 字节的内存。如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可。

**bytesPerRow **:位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。
这里为什么需要指定每行所占的字节数呢?因为大家可能觉得直接就是宽度乘以每个像素所占的直接数就行了。但是这里涉及到一个CPU缓存行对齐的问题。

缓存行对齐。每次内存和CPU缓存之间交换数据都是固定大小,cache line就表示这个固定的长度,一般为64个字节。如果我们的数据是它的倍速,那数据的读取效率就会快很多。

当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行Cache Line Alignment 的优化。

比如我们看一个解压缩完成的图片:

这里的row bytes不是540 * 4 = 2160,而是2176,而且2176刚好能被64整除。

space就是颜色空间,前面提到过了,这里就是RGB,因为iOS只支持RGB。

然后就是bitmapInfo。这个参数除了要指定alpha的信息外,就是前面提到的ARGB还是RGBA,另外还需要指定字节顺序

字节顺序分为两种:小端模式和大端模式。它是由枚举值 CGImageByteOrderInfo 来表示的:

typedef CF_ENUM(uint32_t, CGImageByteOrderInfo) {
    kCGImageByteOrderMask     = 0x7000,
    kCGImageByteOrderDefault  = (0 << 12),
    kCGImageByteOrder16Little = (1 << 12),
    kCGImageByteOrder32Little = (2 << 12),
    kCGImageByteOrder16Big    = (3 << 12),
    kCGImageByteOrder32Big    = (4 << 12)
} CG_AVAILABLE_STARTING(10.0, 2.0);

在iOS中使用的是小端模式,在macOS中使用的是大端模式,为了兼容,使用kCGBitmapByteOrder32Host,32位字节顺序,该宏在不同的平台上面会自动组装换成不同的模式。32是指数据以32bit为单位(字节顺序)。字节顺序也以32bit为单位排序。

#ifdef __BIG_ENDIAN__
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else    /* Little endian. */
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif

下面是SD解压缩图片的源码,拿到位图的上下文CGContextRef之后,调用CGContextDrawImage进行绘制,然后就可以通过CGBitmapContextCreateImage拿到位图。

CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
if (!context) {
    return NULL;
}

// Apply transform
CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
CGContextConcatCTM(context, transform);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);

到这里图片的解压缩就结束了。

再回到SDWebImage,这里图片解压缩结束。

如果再在context中设置了SDWebImageScaleDownLargeImages,那在解压缩的时候就要做进一步缩放处理。一般来说SDWebImage会保持图片的原始尺寸,但如果图片过大且设置了SDWebImageScaleDownLargeImages,则会对图片进行缩小。这时候会边解压缩边缩小。具体的实现在下面这个方法:

​```objectivec

  • (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes {
    ......

`limitBytes`可以限制图片的大小,如果传入0则使用默认值,就是解压缩后不超过60MB。这里传入的就是0。

先看看下面这段代码:

```objectivec
  ......
      CGFloat destTotalPixels;
    CGFloat tileTotalPixels;
    if (bytes > 0) {
      destTotalPixels = bytes / kBytesPerPixel;
        tileTotalPixels = destTotalPixels / 3;
  } else {
        destTotalPixels = kDestTotalPixels;
      tileTotalPixels = kTileTotalPixels;
    }
  ......

这里图片最大限制为60MB,每个像素占4个字节,1MB就有1024 * 1024个字节,那1MB有1024 * 1024 / 4个像素,所以kDestTotalPixels为1024 * 1024 / 4 * 60,即输出图片的像素。

接着根据目标总像素和原图像素计算目标图片的尺寸:

    CGSize sourceResolution = CGSizeZero;
    sourceResolution.width = CGImageGetWidth(sourceImageRef);
    sourceResolution.height = CGImageGetHeight(sourceImageRef);
    CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
  // Determine the scale ratio to apply to the input image
    // that results in an output image of the defined size.
  // see kDestImageSizeMB, and how it relates to destTotalPixels.
    CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
  CGSize destResolution = CGSizeZero;
    destResolution.width = (int)(sourceResolution.width * imageScale);
  destResolution.height = (int)(sourceResolution.height * imageScale);

然后调用前面提到的CGBitmapContextCreate创建位图上下文。

CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);设置图像插值的质量为高。

接下来开始图片缩小,算法的基本流程如下:

基本的思想就是每次压缩一小部分,然后绘制到输出的上下文中。

每次读取的大小定义在kSourceImageTileSizeMB中:

/*
 * 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.f;

知道每次读取的大小,就可以计算每次读取的像素,接着就可以得到读取的矩形区域:

        // Now define the size of the rectangle to be used for the
        // incremental blits from the input image to the output image.
        // we use a source tile width equal to the width of the source
        // image due to the way that iOS retrieves image data from disk.
        // iOS must decode an image from disk in full width 'bands', even
        // if current graphics context is clipped to a subrect within that
        // band. Therefore we fully utilize all of the pixel data that results
        // from a decoding opertion by achnoring our tile size to the full
        // width of the input image.
        CGRect sourceTile = CGRectZero;
        sourceTile.size.width = sourceResolution.width;
        // The source tile height is dynamic. Since we specified the size
        // of the source tile in MB, see how many rows of pixels high it
        // can be given the input image width.
        sourceTile.size.height = (int)(tileTotalPixels / sourceTile.size.width );
      sourceTile.origin.x = 0.0f;
        // The output tile is the same proportions as the input tile, but
      // scaled to image scale.
        CGRect destTile;
        destTile.size.width = destResolution.width;
        destTile.size.height = sourceTile.size.height * imageScale;
        destTile.origin.x = 0.0f;

这里为了防止有空隙,每次会重叠两个像素:

static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to overlap the seems where tiles meet.

......
// The source seem overlap is proportionate to the destination seem overlap.
// this is the amount of pixels to overlap each tile as we assemble the ouput image.
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
......
        
// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;

接下来进入缩小循环:

        for( int y = 0; y < iterations; ++y ) {
            @autoreleasepool {
                sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
                destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
                sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
                if( y == iterations - 1 && remainder ) {
                    float dify = destTile.size.height;
                    destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
                  dify -= destTile.size.height;
                    destTile.origin.y += dify;
              }
                CGContextDrawImage( destContext, destTile, sourceTileImageRef );
              CGImageRelease( sourceTileImageRef );
            }
      }

主要是两个关键函数:CGImageCreateWithImageInRect( sourceImageRef, sourceTile )CGContextDrawImage( destContext, destTile, sourceTileImageRef )。这里因为坐标系的原因,destTile和sourceTile起始点是相反的。

之后就是调用CGBitmapContextCreateImage(destContext)来得到位图,再创建UIImage即可。

到这里SDWebImageDownloaderOperation中的图片解压缩和缩小就结束了。这时结果被返回到SDWebImageManager

SDWebImageManager在写入缓存之前,会对图片做进一步变换处理。我们可以通过context的SDWebImageContextImageTransformer来指定图片的变换,包括修改图片大小、圆角剪裁、模糊处理等等。SDWebImage提供了一些默认的变换:

  • SDImageRoundCornerTransformer

  • SDImageResizingTransformer

  • SDImageCroppingTransformer

  • SDImageFlippingTransformer

  • SDImageRotationTransformer

  • SDImageTintTransformer

  • SDImageBlurTransformer

  • SDImageFilterTransformer`

还可以用`SDImagePipelineTransformer`组合多个变换。

变换完成后,SDWebImage要把转换后的UIImage转为NSData并写入缓存,此时需要SDImageCodersManager对图片进行编码。先看看SDImageIOCoder如何对图片进行编码。

在SDImageCoder的encodedDataWithImage方法中:

首先调用以下方法的得到imageDestination:

CGImageDestinationRef CGImageDestinationCreateWithData(CFMutableDataRef data, CFStringRef type, size_t count, CFDictionaryRef options);

参数注释:

  • data:The data object to write to. For more information on data objects, see CFData and Data Objects.

  • type:The uniform type identifier (UTI) of the resulting image file. See Uniform Type Identifiers Overview for a list of system-declared and third-party UTIs.

  • count:The number of images (not including thumbnail images) that the image file will contain.

  • options:Reserved for future use. Pass NULL.

接着设置图片的方向和压缩质量,

    NSMutableDictionary *properties = [NSMutableDictionary dictionary];
#if SD_UIKIT || SD_WATCH
    CGImagePropertyOrientation exifOrientation = [SDImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation];
#else
    CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp;
#endif
    properties[(__bridge NSString *)kCGImagePropertyOrientation] = @(exifOrientation);
  double compressionQuality = 1;
    if (options[SDImageCoderEncodeCompressionQuality]) {
      compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue];
    }
    properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality);

最后调用CGImageDestinationAddImage压缩图片:

CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);

到这里图片转为NSData就结束了。

接下来看看动图的解析

这次是SDImageGIFCoder


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