Bitmap 分析


Bitmap是位图,每个像素点根据位的深度表示不同种类的颜色,深度越大,代表的颜色值越多,存储空间越大。


BitmapFactory工厂

从文件读取。

public static Bitmap decodeFile(String pathName)
public static Bitmap decodeFile(String pathName, Options opts)

根据路径,创建一个FileInputStream,然后,调用decodeStream方法。
从资源读取。

public static Bitmap decodeResource(Resources res, int id, Options opts) 
public static Bitmap decodeResource(Resources res, int id)
public static Bitmap decodeResourceStream(Resources res, TypedValue value,InputStream is, Rect pad, Options opts)

前两个方法,通过Resources的openRawResource(id, value)方法,根据资源id,调用AssetManager的openNonAsset方法,创建AssetInputStream。然后,他们再去调用decodeResourceStream方法。最终,这三个方法全部触发decodeStream。
从字节数组读取。

public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)
public static Bitmap decodeByteArray(byte[] data, int offset, int length)

调用本地nativeDecodeByteArray方法。
从流读取。

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
public static Bitmap decodeStream(InputStream is)

资源解析id成AssetInputStream流,调用本地nativeDecodeAsset方法。其他流调用decodeStreamInternal方法,调用本地nativeDecodeStream方法。
从文件描述符读取。

public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)
public static Bitmap decodeFileDescriptor(FileDescriptor fd)

调本地nativeDecodeFileDescriptor方法,或创建FileInputStream流解析,与文件读取类似,调用decodeStreamInternal方法。

综上所述

BitmapFactory提供了解析文件、资源、字节数组、文件描述符、或流的方式创建Bitmap,在创建时,Option配置将影响它占用的内存。以上就是几种主要的解析方式,下面我们根据源码看一下InputStream是如何解析。


InputStream流解析

看一下BitmapFactory的decodeStream方法。

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
    Bitmap bm = null;
    try {
        if (is instanceof AssetManager.AssetInputStream) {
            final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
            bm = nativeDecodeAsset(asset, outPadding, opts);//根据InputStream不同。
        } else {
            bm = decodeStreamInternal(is, outPadding, opts);
        }
        ...
        setDensityFromOptions(bm, opts);
    } finally {
    }
    return bm;
}

它的三个参数,InputStream、Rect和Option,Options是工厂的静态内部类,包含一些解析的配置信息,将这些信息传到底层。AssetInputStream流调用nativeDecodeAsset方法,其他的调用nativeDecodeStream方法。

private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
    byte [] tempStorage = null;
    if (opts != null) tempStorage = opts.inTempStorage;
    if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
    return nativeDecodeStream(is, tempStorage, outPadding, opts);
}

字节数组大小16 * 1024。

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
        jobject padding, jobject options) {
    jobject bitmap = NULL;
    SkAutoTDelete<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage))
    if (stream.get()) {
        SkAutoTDelete<SkStreamRewindable> bufferedStream(
                SkFrontBufferedStream::Create(stream.detach(), BYTES_TO_BUFFER));
        bitmap = doDecode(env, bufferedStream, padding, options);
    }
    return bitmap;
}

该JNI#方法的第一个参数是InputStream数据输入流,第二个参数是storage字节数组,padding是null,第四个参数是Bitmap配置Options。然后,将上层传入的InputStream转换底层SkStreamRewindable,JNI#方法doDecode解析bufferedStream,生成一个jobject对象,它就是Java层的Bitmap。看一下JNI#方法doDecode。该方法比较长,分成四个部分。

第一部分代码
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    int sampleSize = 1;
    SkImageDecoder::Mode decodeMode = SkImageDecoder::kDecodePixels_Mode;
    SkColorType prefColorType = kN32_SkColorType;

    bool doDither = true;
    bool isMutable = false;
    float scale = 1.0f;
    bool preferQualityOverSpeed = false;
    bool requireUnpremultiplied = false;

    if (options != NULL) {
        //解析Option的inSampleSize,图片读取到内存中的像素压缩的比例。
        sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
        //解析Option的inJustDecodeBounds,只获取图片信息,不读像素模式。
        if (optionsJustBounds(env, options)) {
            decodeMode = SkImageDecoder::kDecodeBounds_Mode;
        }
        //初始化Option的outWidth与outHeight为-1。outMimeType为0。
        env->SetIntField(options, gOptions_widthFieldID, -1);
        env->SetIntField(options, gOptions_heightFieldID, -1);
        env->SetObjectField(options, gOptions_mimeFieldID, 0);
        //Bitmap.Config配置inPreferredConfig。
        jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
        //解析出SkColorType。
        prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig);
        //Java层值读取
        isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
        doDither = env->GetBooleanField(options, gOptions_ditherFieldID);
        preferQualityOverSpeed = env->GetBooleanField(options,gOptions_preferQualityOverSpeedFieldID);
        requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
        //Java层inBitmap对象
        javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
        //Option的inScaled,默认true
        if (env->GetBooleanField(options, gOptions_scaledFieldID)) { 
            //Option的inDensity
            const int density = env->GetIntField(options, gOptions_densityFieldID);
            //Option的inTargetDensity
            const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
            //Option的inScreenDensity
            const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
            if (density != 0 && targetDensity != 0 && density != screenDensity) {
                scale = (float) targetDensity / density;
            }
        }
    }
    const bool willScale = scale != 1.0f;
    ...
    //后续代码
}

当Java层Option参数不空时候,读取参数信息,主要Option信息包括。
inSampleSize,图片读取到内存中的像素采样率,即图片的压缩比,inSampleSize是2代表图像的长宽都变为原始的1/2,该值根据图片的真实像素宽高和ImageView的宽高共同计算得出。
inJustDecodeBounds,该参数表示解析时只读取图片信息,如图片尺寸,不分配像素内存空间,SkImageDecoder模式是kDecodeBounds_Mode。
isMutable,可变的,生成Bitmap可变位图,可修改像素,默认是false。
inPreferredConfig:Bitmap.Config图片像素类型,基本类型有ALPHA_8、RGB_565、ARGB_4444、ARGB_8888。默认是ARGB_8888,它的图片质量较高,每像素4个字节,内存占用最大。可配置成RGB_565类型,2个字节。
outHeight/outWidth,图像宽高,初始值设置-1。
inBitmap,重用bitmap,如果在Java层设置了这个参数,新Bitmap会使用该参数的内存空间,Option#isMutable必须是可变的。
prefColorType,根据上层Config的nativeInt变量,解析SkColorType类型,与Config像素类型对应,kRGB_565_SkColorType、kIndex_8_SkColorType、kN32_SkColorType等。
inScaled,inDensity,inTargetDensity,inScreenDensity,由它们决定图片是否缩放,首先是inScaled决定是否缩放,默认缩放,然后,如果配置了inTargetDensity和inDensity,并且inScreenDensity和inDensity不相等,按照比例inTargetDensity/inDensity缩放,图片去适应屏幕。最后的结果scale不是1,将设置willScale参数。
inDensity,图片像素密度。
inTargetDensity,目标像素密度。根据该值与Bitmap的像素密度的比值,确定缩放比例。
inScreenDensity,屏幕像素密度。
当我们从资源加载图片时,设置inTargetDensity值为densityDpi,因此,不同屏幕像素密度手机从资源文件加载图片时缩放比不同,载入内存也是不同的。densityDpi值以160像素为基准,240,320,480,640。

第二部分代码
SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
//decoder解析器是空返回null
//SkImageDecoder设置sampleSize、doDither、
//preferQualityOverSpeed、requireUnpremultiplied等参数。
decoder->setSampleSize(sampleSize);
...
android::Bitmap* reuseBitmap = nullptr;
unsigned int existingBufferSize = 0;
if (javaBitmap != NULL) {
    reuseBitmap = GraphicsJNI::getBitmap(env, javaBitmap);
    //这个bitmap不可变
    if (reuseBitmap->peekAtPixelRef()->isImmutable()) {//不可变的bitmap,无法重用
        javaBitmap = NULL;
        reuseBitmap = nullptr;
    } else {
        existingBufferSize = GraphicsJNI::getBitmapAllocationByteCount(env, javaBitmap);
    }
}
...
JavaPixelAllocator javaAllocator(env);
RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);
ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
SkBitmap::Allocator* outputAllocator = (javaBitmap != NULL) ?
            (SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator;
if (decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
    if (!willScale) {
        decoder->setSkipWritingZeroes(outputAllocator == &javaAllocator);
        decoder->setAllocator(outputAllocator);
    } else if (javaBitmap != NULL) {
        decoder->setAllocator(&scaleCheckingAllocator);
    }
}

创建解析器SkImageDecoder,设置从Option读取到部分配置。获取上层重用javaBitmap的底层Bitmap对象,如果不可变,不能重用,置空。如果可重用,解析javaBitmap内存字节大小existingBufferSize。
在非kDecodeBounds_Mode时,读取像素到内存,SKImageDecoder的setAllocator方法,设置Allocator内存分配器。包括三种,JavaPixelAllocator,RecyclingPixelAllocator和ScaleCheckingAllocator。
当不需要缩放时,重用Bitmap,使用RecyclingPixelAllocator,不重用Bitmap,使用JavaPixelAllocator。
当需要缩放,重用Bitmap,使用ScaleCheckingAllocator。
只有RecyclingPixelAllocator和ScaleCheckingAllocator传入existingBufferSize大小,与重用Bitmap相关。

第三部分代码
SkAutoTDelete<SkImageDecoder> add(decoder);
AutoDecoderCancel adc(options, decoder);

SkBitmap decodingBitmap;
//stream解析到skbitmap
if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
            != SkImageDecoder::kSuccess) {
    return nullObjectReturn("decoder->decode returned false");
}
//拿到宽高
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
//Scale处理后的宽高
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
    scaledWidth = int(scaledWidth * scale + 0.5f);
    scaledHeight = int(scaledHeight * scale + 0.5f);
}
//得到了宽和高和mimeType,设置option
if (options != NULL) {
    jstring mimeType = getMimeTypeString(env, decoder->getFormat());
    if (env->ExceptionCheck()) {
        return nullObjectReturn("OOM in getMimeTypeString()");
    }
    env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
    env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
    env->SetObjectField(options, gOptions_mimeFieldID, mimeType);
}
//inJustDecodeBounds为true,直接返回NULL
if (decodeMode == SkImageDecoder::kDecodeBounds_Mode) {
    return NULL;
}
//NinePatchPeeker相关处理
...

SkImageDecode的decode方法,解码stream后,得到SkBitmap类型的对象。
获取SkBitmap的宽高,如果需要缩放,并且非kDecodeBounds_Mode模式,将重新计算的scaledWidth、scaledHeight和mimeType信息设置到Java层Option对象。
如果是kDecodeBounds模式,表示Java层Option设置inJustDecodeBounds参数,不需要分配内存,返回空,图片信息已保存在Option对象。

第四部分代码
SkBitmap outputBitmap;
if (willScale) {
    //计算Scale的比例。
    const float sx = scaledWidth / float(decodingBitmap.width());
    const float sy = scaledHeight / float(decodingBitmap.height());

    SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType());  
    outputBitmap.setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
            colorType, decodingBitmap.alphaType()));
    if (!outputBitmap.tryAllocPixels(outputAllocator, NULL)) {
        return nullObjectReturn("allocation failed for scaled bitmap");
    }
   
    if (outputAllocator != &javaAllocator) {
        outputBitmap.eraseColor(0);
    }

    SkPaint paint;
    paint.setFilterQuality(kLow_SkFilterQuality);

    SkCanvas canvas(outputBitmap);
    canvas.scale(sx, sy);
    canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
} else {
    outputBitmap.swap(decodingBitmap);
}

if (outputBitmap.pixelRef() == NULL) {
    return nullObjectReturn("Got null SkPixelRef");
}

if (!isMutable && javaBitmap == NULL) {
    outputBitmap.setImmutable();
}

if (javaBitmap != NULL) {
    bool isPremultiplied = !requireUnpremultiplied;
    GraphicsJNI::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied);
    outputBitmap.notifyPixelsChanged();
    return javaBitmap;
}

int bitmapCreateFlags = 0x0;
if (isMutable) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Mutable;
if (!requireUnpremultiplied) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Premultiplied;

// now create the java bitmap
return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
                bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);

SkImageInfo是一个结构体,包含宽高、颜色类型、透明类型等信息,Make方法,创建该结构体,SKBitmap的setInfo方法设置内部SkImageInfo对象。
如果不缩放,将前面解析的decodingBitmap交给输出outputBitmap。
如果重用javaBitmap不空,利用输出的outputBitmap重新初始化该对象,通知像素改变,将该Java层Bitmap对象返回。
如果不重用,GraphicsJNI的createBitmap方法,创建Java层的Bitmap实例。

jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap,
        int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
        int density) {
    ...
    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(),
            bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
            ninePatchChunk, ninePatchInsets);
    return obj;
}

env的NewObject方法,创建Bitmap实例,入参是native层bitmap指针,Java层字节数组的强引用,bitmap宽/高,density,isMutable,isPremultiplied,ninePatchChunk,ninePatchInsets。
Java层字节数组强引用是mPixelStorage中结构体java的jbyteArray变量。在使用JavaPixelAllocator分配器分配内存时,会介绍如何对结构体赋值。

// called from JNI
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density, boolean isMutable, 
                boolean requestPremultiplied, byte[] ninePatchChunk, 
                NinePatch.InsetStruct ninePatchInsets)

最终,Java层Bitmap实例创建成功,构造方法是由底层JNI方法调用。


了解SKImageDecode解码

我们前面说过,通过解码器SkImageDecoder的decode方法,解析stream,得到SkBitmap对象。

SkImageDecoder::Result SkImageDecoder::decode(SkStream* stream, SkBitmap* bm,
                    SkColorType pref,Mode mode) {
    fShouldCancelDecode = false;
    fDefaultPref = pref;
    SkBitmap tmp;
    const Result result = this->onDecode(stream, &tmp, mode);
    if (kFailure != result) {
        bm->swap(tmp);
    }
    return result;
}

调用的onDecode方法在它的子类里实现,子类包括各种图片格式png,jpeg及gif的解码类。比如,SkPNGImageDecoder继承SkImageDecoder,看一下它的onDecode方法。

SkImageDecoder::Result SkPNGImageDecoder::onDecode(SkStream* sk_stream, 
                    SkBitmap* decodedBitmap,Mode mode) {
    //初始化
    if (!onDecodeInit(sk_stream, &png_ptr, &info_ptr)) {
        return kFailure;
    }
    ....
    //该模式直接返回成功
    if (SkImageDecoder::kDecodeBounds_Mode == mode) {
        return kSuccess;
    }
    ....
    //内存分配
    //最终触发Allocator的allocPixelRef方法,入参decodedBitmap
    if (!this->allocPixelRef(decodedBitmap,
                kIndex_8_SkColorType == colorType ? colorTable : NULL)) {
        return kFailure;
    }
    png_read_update_info(png_ptr, info_ptr);
    ....
    //像素读取
    ....
    png_read_end(png_ptr, info_ptr);
    ....
    return kSuccess;
}

调用它自己的allocPixelRef方法。

bool SkImageDecoder::allocPixelRef(SkBitmap* bitmap,
                    SkColorTable* ctable) const {
    return bitmap->tryAllocPixels(fAllocator, ctable);
}

该方法调用入参SkBitmap的tryAllocPixels方法,将SkImageDecoder内部的分配器传入。在SkImageDecoder创建后会setAllocator初始化Allocator,前面代码都有写。

bool SkBitmap::tryAllocPixels(Allocator* allocator, SkColorTable* ctable) {
    HeapAllocator stdalloc;
    if (NULL == allocator) {
        allocator = &stdalloc;
    }
    return allocator->allocPixelRef(this, ctable);
}

最终,在SkBitmap类中,调用Allocator的allocPixelRef方法,将SKBitmap作为参数传入。以JavaPixelAllocator分配器为例,在Java堆上分配内存。

class JavaPixelAllocator : public SkBitmap::Allocator {
public:
    JavaPixelAllocator(JNIEnv* env);
    ~JavaPixelAllocator();

    virtual bool allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) override;

    android::Bitmap* getStorageObjAndReset() {
        android::Bitmap* result = mStorage;
        mStorage = NULL;
        return result;
    };

private:
    JavaVM* mJavaVM;
    android::Bitmap* mStorage = nullptr;
};

JavaPixelAllocator的allocPixelRef方法

bool JavaPixelAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
    JNIEnv* env = vm2env(mJavaVM);
    mStorage = GraphicsJNI::allocateJavaPixelRef(env, bitmap, ctable);
    return mStorage != nullptr;
}

GraphicsJNI的allocateJavaPixelRef方法。创建底层Bitmap对象,指针赋值mStorage,在JavaPixelAllocator中保存。

android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, 
                    SkBitmap* bitmap,SkColorTable* ctable) {
    const SkImageInfo& info = bitmap->info();

    size_t size;
    if (!computeAllocationSize(*bitmap, &size)) {
        return NULL;
    }
    const size_t rowBytes = bitmap->rowBytes();
    jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(
                gVMRuntime, gVMRuntime_newNonMovableArray,
                gByte_class, size);//Java堆分配内存
    jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, 
                    gVMRuntime_addressOf, arrayObj);//地址
    android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
            info, rowBytes, ctable);//创建native层Bitmap
    wrapper->getSkBitmap(bitmap);//用native层Bitmap初始化SkBitmap
    bitmap->lockPixels();

    return wrapper;
}

GraphicsJNI的computeAllocationSize方法,根据SkBitmap的height和width计算分配空间大小,存储在size。
VMRuntime的newNonMovableArray方法,在Java堆分配内存,返回Java层字节数组arrayObj。
VMRuntime的addressOf方法,返回jbyteArray字节数组arrayObj的地址。
VMRuntime源码/libcore/libart/src/main/java/dalvik/system/VMRuntime.java目录。
创建底层的Bitmap。

Bitmap::Bitmap(JNIEnv* env, jbyteArray storageObj, void* address,
        const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable)
        : mPixelStorageType(PixelStorageType::Java) {
    env->GetJavaVM(&mPixelStorage.java.jvm);
    mPixelStorage.java.jweakRef = env->NewWeakGlobalRef(storageObj);
    mPixelStorage.java.jstrongRef = nullptr;//数组强引用暂时置空。
    mPixelRef.reset(new WrappedPixelRef(this, address, info, rowBytes, ctable));
 
    mPixelRef->unref();
}

底层Bitmap的两个变量mPixelStorage和mPixelRef。
mPixelStorage,联合体,内部的java结构体内容。jweakRef指向NewWeakGlobalRef方法创建的Java层字节数组(Java堆对象)的弱全局引用。mPixelRef,WrappedPixelRef,继承SkPixelRef,封装Bitmap本身,地址addr,像素大小rowBytes,SkImageInfo等。

struct {
    JavaVM* jvm;
    jweak jweakRef;
    jbyteArray jstrongRef;
} java;

底层Bitmap创建后,调用getSkBitmap方法,初始化SkBitmap输出。

void Bitmap::getSkBitmap(SkBitmap* outBitmap) {
    outBitmap->setInfo(mPixelRef->info(), mPixelRef->rowBytes());
    outBitmap->setPixelRef(refPixelRefLocked())->unref();//获取mPixelRef引用
    outBitmap->setHasHardwareMipMap(hasHardwareMipMap());
}

底层Bitmap的refPixelRefLocked方法,获取mPixelRef引用。SkBitmap内部包含mPixelRef引用。

JavaPixelAllocator分配内存.jpg

综上所述

Android6.0的Bitmap源码,位图像素存储在Java堆字节数组中,Java层的Bitmap对象引用底层Bitmap,字节数组同时被Java层Bitmap和底层Bitmap#mPixelStorage引用


任重而道远

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

推荐阅读更多精彩内容