图片ImageI/O解码探究

最近在做ImageI/O的相关调研,在使用CGImageSourceCreateImageAtIndex方法创建UIImage对象,和使用CGImageSourceCreateThumbnailAtIndex创建UIImage对象缩略图时,引发了一系列的问题和思考探究,主要是关于ImageI/O的使用以及解码过程。

正常情况下,当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。我们可以通过下面的方法模拟图片被解码并渲染的过程:


- (void)drawImage:(UIImage*)image {

    size_t width = CGImageGetWidth(image.CGImage);

    size_t height = CGImageGetHeight(image.CGImage);

    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();

    CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpaceRef, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);

    if(!context)return;

    CGColorSpaceRelease(colorSpaceRef);

    CGContextDrawImage(context,CGRectMake(0,0, width, height), image.CGImage);// decode

    CGImageRef newImageRef = CGBitmapContextCreateImage(context);

    CFRelease(context);

    CGImageRelease(newImageRef);

}

用TimeProfiler一步一步来看过程中内部调用的函数可以帮助我们解决问题,由于TimeProfiler统计函数栈为间隔一段时间统计一次,导致没有记录下所有函数的调用而且每次函数栈还可能不一致,所以没法精确判断函数栈是如何调用的,但是可以大概推测出每步做了什么。

那么我们看下正常情况下图片解码时候,系统都是如何做的。首先是PNG格式的图片:

PNG的解码过程

CGContextDrawImageWithOptions方法中,调用了PNGPlugin库中的一系列方法,没有明显看到带有decode关键字的方法,猜测png_read_IDAT_dataApple就是执行的解码过程。

接着看下JPEG格式的图片:

JPEG的解码过程

CGContextDrawImageWithOptions方法中,调用了AppleJPEGPlugin库中的一系列方法,可以看到带有decode关键字的方法FigPhotoJPEGDecodeJPEGIntoRGBSurface,这个应该就是执行解码的过程。

好了,以上的实验知道了PNG和JPEG格式的图片执行解码的关键方法,接下来正式进入本文章的探究主题。

下面的方法是使用ImageI/O,通过获取缩略图的方法,将图片进行裁剪操作,生成所需要的UIImage对象。


- (UIImage*)resizeWithData:(NSData*)data scaleSize:(CGSize)size {

    if(!data) {

        returnnil;

    }

    // Create the image source

    CGImageSourceRef imageSourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);

    if(!imageSourceRef) {

        returnnil;

    }

    CGFloatmaxPixelSize =MAX(size.width, size.height);

    // Create thumbnail options

    CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]

                                                           };

    // Generate the thumbnail

    CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(imageSourceRef, 0, options);

    UIImage*thumbnailImage = [UIImageimageWithCGImage:imageRef];

    CFRelease(imageSourceRef);

    CGImageRelease(imageRef);

    returnthumbnailImage;

}

这里有几个参数需要解释一下,依次如下:

kCGImageSourceShouldCacheImmediately,查看文档解释:


/* Specifies whether image decoding and caching should happen at image creation time.

 * The value of this key must be a CFBooleanRef. The default value is kCFBooleanFalse (image decoding will

 * happen at rendering time).

 */

翻译过来就是说:

是否应该在图像创建过程中,进行图像解码和缓存。 此键的值必须是CFBooleanRef。 默认值为kCFBooleanFalse(图像解码将在渲染时发生)。

kCGImageSourceShouldCache,查看下官方文档解释:


/** Keys for the options dictionary of "CGImageSourceCopyPropertiesAtIndex"

 ** and "CGImageSourceCreateImageAtIndex". **/

/* Specifies whether the image should be cached in a decoded form. The

 * value of this key must be a CFBooleanRef.

 * kCFBooleanFalse indicates no caching, kCFBooleanTrue indicates caching.

 * For 64-bit architectures, the default is kCFBooleanTrue, for 32-bit the default is kCFBooleanFalse.

 */

翻译过来就是:

在方法CGImageSourceCopyPropertiesAtIndex和CGImageSourceCreateImageAtIndex中使用

指定是否应以解码形式缓存图像。 此键的值必须是CFBooleanRef。 kCFBooleanFalse表示没有缓存,kCFBooleanTrue表示缓存。 对于64位体系结构,默认值为kCFBooleanTrue,对于32位,默认值为kCFBooleanFalse。

注意:此key指定的是解码后的数据是否需要缓存。此处我们设置为kCFBooleanFalse,不进行缓存。

kCGImageSourceCreateThumbnailFromImageAlways,文档解释:


/* Specifies whether a thumbnail should be created from the full image even

 * if a thumbnail is present in the image source file. The thumbnail will

 * be created from the full image, subject to the limit specified by

 * kCGImageSourceThumbnailMaxPixelSize---if a maximum pixel size isn't

 * specified, then the thumbnail will be the size of the full image, which

 * probably isn't what you want. The value of this key must be a

 * CFBooleanRef; the default value of this key is kCFBooleanFalse. */

翻译过来就是:

指定是否应从完整图像创建缩略图,即使图像源文件中存在缩略图也是如此。 缩略图将根据完整图像创建,受kCGImageSourceThumbnailMaxPixelSize指定的限制---如果未指定最大像素大小,则缩略图将是完整图像的大小,这可能不是您想要的。 该键的值必须是CFBooleanRef; 此键的默认值为kCFBooleanFalse。

这里我们设置为kCFBooleanTrue。

kCGImageSourceThumbnailMaxPixelSize,官方解释:


/* Specifies the maximum width and height in pixels of a thumbnail.  If

 * this this key is not specified, the width and height of a thumbnail is

 * not limited and thumbnails may be as big as the image itself.  If

 * present, this value of this key must be a CFNumberRef. */

翻译如下:

指定缩略图的最大宽度和高度(以像素为单位)。 如果未指定此键,则缩略图的宽度和高度不受限制,缩略图可能与图像本身一样大。 如果存在,则此键的此值必须为CFNumberRef。

好的,接下来,我们看看CGImageSourceCreateThumbnailAtIndex系统具体做了什么。

首先我们看下PNG格式的图片 512x384.png

同时为了测试图片的解码过程,我们将代码中kCGImageSourceShouldCacheImmediately对应的值修改为kCFBooleanTrue,也就是创建图片过程中进行解码。

根据Time Profiler我们查看下系统都在这个函数里面做了什么,调用结果如下:

PNG的resizeWithData

可以看到,- (UIImage)resizeWithData:(NSData)data scaleSize:(CGSize)size方法执行了48.00ms,其中CGImageSourceCreateThumbnailAtIndex执行了大约45.00ms,大部分耗时都在这里。我们看下里面究竟做了什么。过程中系统调用了CGContextDrawImageWithOptions。因为我们前面设置了kCGImageSourceShouldCacheImmediately对应的值修改为kCFBooleanTrue,也就是需要解码,所以这里系统调用了CGContextDrawImageWithOptions方法,会将图片渲染到画布,这个过程是会解码的。那么接着往下看,具体解码的步骤在哪里。可以看到接下来最耗时的操作分别在img_interpolate_extent和img_interpolate_read两个函数。然后分别看看这两个函数做了什么。

PNG的img_interpolate_extent

这里系统调用了CGImageProviderCopyImageBlockSet,里面调用了PNGPlugin库的_cg_png_read_row和_cg_png_read_info方法,_cg_png_read_row方法调用了png_read_IDAT_dataApple,这个方法上面已经提到了,是进行的解码操作。

PNG的img_interpolate_read

img_interpolate_read里面调用了img_decide_read,猜测应该是读取解码完成的数据。

好了,PNG格式的图片如何解码我们大致推理出来了,那么再看看JPEG格式的图片,512x384.jpg

JPEG的resizeWithData

JPEG图片的- (UIImage)resizeWithData:(NSData)data scaleSize:(CGSize)size方法执行了55.00ms,其中CGImageSourceCreateThumbnailAtIndex执行了大约53.00ms,这里系统同样调用了CGContextDrawImageWithOptions方法,与PNG不同的是,JPEG里面调用了img_decode_stage和img_interpolate_read方法。

JPEG的img_decode_stage

可以看到img_decode_stage方法里面同样调用了CGImageProviderCopyImageBlockSet方法,然后调用了AppleJPEGPlugin库的FigPhotoJPEGDecodeJPEGIntoRGBSurface方法,这里进行了解码。

JPEG的img_interpolate_read

img_interpolate_read里面调用了img_decode_read,跟PNG图片的一模一样,应该也是对解码完成的数据进行读取。

以上就是解码过程的剖析,那么作为对比试验,我们接下来看下不经过解码时的调用过程。

将kCGImageSourceShouldCacheImmediately对应的值修改为kCFBooleanFalse,也就是创建图片过程中不进行解码。

首先还是看下PNG格式的图片,512x384.png

PNG

然后就震惊了!!!有没有!!!居然跟之前强制解码的一模一样!!!展开图中标出的两个方法。

JPEG
JPEG

真的是也会解码!!!这究竟是为什么呢?

难道是


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]

                                                           };

这个options的问题?试着尝试使用不同的options,有了下面的结果:

情况一:


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

//                                                          (__bridge id) kCGImageSourceThumbnailMaxPixelSize : [NSNumber numberWithFloat:maxPixelSize]

                                                           };

实验结果如下:

情况一PNG
情况一JPEG

情况一结论:如果不设置kCGImageSourceThumbnailMaxPixelSize,同时kCGImageSourceShouldCacheImmediately设置为kCFBooleanFalse,那么不管是PNG还是JPEG格式的图片,都没有进行解码操作。

情况二:


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

//                                                          (__bridge id) kCGImageSourceThumbnailMaxPixelSize : [NSNumber numberWithFloat:maxPixelSize]

                                                           };

实验结果如下:

情况二PNG
情况二JPEG

情况二结论:如果不设置kCGImageSourceThumbnailMaxPixelSize,同时kCGImageSourceShouldCacheImmediately设置为kCFBooleanTure,那么不管是PNG还是JPEG格式的图片,都进行了解码操作。

情况三:


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]

                                                           };

实验结果如下:

情况三PNG
情况三JPEG

情况三结论:如果设置了kCGImageSourceThumbnailMaxPixelSize,同时kCGImageSourceShouldCacheImmediately设置为kCFBooleanFalse,那么不管是PNG还是JPEG格式的图片,都进行了解码操作。

情况四:


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]

                                                           };

实验结果如下:

情况四PNG
情况四JPEG

情况四结论:如果设置了kCGImageSourceThumbnailMaxPixelSize,同时kCGImageSourceShouldCacheImmediately设置为kCFBooleanTure,那么不管是PNG还是JPEG格式的图片,也都进行了解码操作。

总结:

1、在使用CGImageSourceCreateThumbnailAtIndex方法时,如果设置了kCGImageSourceThumbnailMaxPixelSize,那么肯定会进行解码操作,生成对应的新图CGImageRef。

2、如果不设置kCGImageSourceThumbnailMaxPixelSize,那么是否进行解码操作,取决于kCGImageSourceShouldCacheImmediately对应的值是kCFBooleanTure还是kCFBooleanFalse。

ImageI/O中使用CGImageSourceCreateThumbnailAtIndex创建缩略图方法的结论就是如上所述。那么这里又有另一个思考,如果是CGImageSourceCreateImageAtIndex方法,那么上述的kCGImageSourceShouldCacheImmediately键值对会造成什么影响呢?


- (UIImage*)resizeWithData:(NSData*)data scaleSize:(CGSize)size {

    if(!data) {

        returnnil;

    }

    // Create the image source

    CGImageSourceRef imageSourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);

    if(!imageSourceRef) {

        returnnil;

    }

    // Create thumbnail options

    CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse

                                                           };

    // Generate the thumbnail

    NSLog(@"%@", options);

    CGImageRefimageRef =

    CGImageSourceCreateImageAtIndex(imageSourceRef,0, options);

    UIImage*thumbnailImage = [UIImageimageWithCGImage:imageRef];

    CFRelease(imageSourceRef);

    CGImageRelease(imageRef);

    returnthumbnailImage;

}

情况一:


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse

                                                           };

实验如下:

CGImageSourceCreateImageAtIndex不解码_PNG
CGImageSourceCreateImageAtIndex不解码_JPEG

情况一结论:设置kCGImageSourceShouldCacheImmediately为kCFBooleanFalse时,PNG和JPEG都没有进行解码。

情况二:


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse

                                                           };

实验如下:

CGImageSourceCreateImageAtIndex解码_PNG
CGImageSourceCreateImageAtIndex解码_JPEG

情况二结论:设置kCGImageSourceShouldCacheImmediately为kCFBooleanTrue时,PNG和JPEG都进行了解码。

总结:在使用CGImageSourceCreateImageAtIndex方法创建CGImageRef时,kCGImageSourceShouldCacheImmediately值会影响是否开启解码操作。ImageI/O默认的kCGImageSourceShouldCacheImmediately为kCFBooleanFalse,也就是说创建图片时候不解码,会等到图片被渲染的时候才进行解码。

以上部分就明确了CGImageSourceCreateImageAtIndex和CGImageSourceCreateThumbnailAtIndex时系统底层具体的实现。

当然,在使用这个- (UIImage*)resizeWithData:(NSData*)data scaleSize:(CGSize)size方法时候也采坑了。因为一般大部分情况下(参考图片缩放使用UIKIt、Core Graphics、Core Foundation等情况下的方法),是为UIImage添加一个分类,使用分类方法进行缩放。那么既然如此,为什么不同样使用分类呢?嗯,不错的想法,笔者刚开始是这样做的:


- (UIImage*)resizeWithImage:(UIImage*)image scaleSize:(CGSize)size {

    CFDataRef bitmapData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));

    // Create the image source

    CGImageSourceRef imageSourceRef = CGImageSourceCreateWithData(bitmapData, NULL);

    if(!imageSourceRef) {

        returnnil;

    }

    CGFloatmaxPixelSize =MAX(size.width, size.height);

    // Create thumbnail options

    CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                                                                                      (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]

                                                           };

    // Generate the thumbnail

    CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(imageSourceRef, 0, options);

    UIImage*thumbnailImage = [UIImageimageWithCGImage:imageRef];

    CFRelease(imageSourceRef);

    CGImageRelease(imageRef);

    returnthumbnailImage;

}

干净漂亮,直接跑起来,美滋滋。

但是!!!结果返回的图片为nil。

为什么?百思不得其解,详细了解了下CGDataProvider的一系列API,CGimage的dataProvider,指的是CGImageCreate时候,传入的承载了Bitmap Buffer数组的一个提供者,可以是一个内存中的buffer,也可以是一个callback来实现惰性解码。也就是说,这个传入的Bitmap Buffer数组,必须是未经过解压缩的数据。如果是经过了解压缩的图片数据,那么传给ImageI/0是没有意义的。

问题真的出在这里吗?这两个方法参数不同之处是,一个是使用UIImage *image = [UIImage imageWithContentsOfFile:path],传入UIImage对象,而另一个是通过NSData *data = [NSData dataWithContentsOfFile:path],传入的NSData对象。

那么这两个方法本质的区别到底是什么呢?为什么造成不同的结局呢?系统在这两个方法里面具体都干了什么呢?

首先,拿JPEG格式的做实验,看看[UIImage imageWithContentsOfFile:]都做了什么。

JPEG的imageWithContentsOfFile

可以看到,在该方法中系统调用了CGImageSourceCreateImageAtIndex方法,在该方法中,系统使用了AppleJPEGPlugin库的一些方法,但是并没有发现decode相关的函数,所以这里应该没有进行解码,而只是将图片进行了解压缩(decompress)。这也就解释了为什么使用CGImageGetDataProvider获取的CGDataProvider对象是无效的了。

那么同时可以看下PNG格式的图片,在使用[UIImage imageWithContentsOfFile:]时系统都做了什么。

PNG的imageWithContentsOfFile

PNG格式的图片,同样是调用CGImageSourceCreateImageAtIndex等方法,同时可以看到使用的是PNGPlugin库相关的方法,PNGReadPlugin读取文件数据进行解压缩。

有兴趣的同学可以看下[UIImage imageWithNamed:]方法创建的UIImage对象,至于[UIImage imageWithNamed:]和[UIImage imageWithContentsOfFile:]的具体区别,会另起一篇文章进行分析。

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

推荐阅读更多精彩内容