Adroid读代码 Parcel - (2)

关键字:Android, Parcel

20180828 tjy

转载请注明出处

前面的内容列表:

Adroid读代码 Parcel - (1) https://www.jianshu.com/p/42d2dc89649e

今天继续看Parcel的几个简单方法,假设在写如Parcel的时候,Parcel是刚创建的对象,里面是空的。

public final void writeInt(int val)
public final void writeString(String val)
public final void writeBoolean(boolean val)
public final void writeStringArray(String[] val)

public final void writeInt(int val)

//from http://androidxref.com/8.1.0_r33/xref/frameworks/base/core/java/android/os/Parcel.java#writeInt

/**
* Write an integer value into the parcel at the current dataPosition(),
* growing dataCapacity() if needed.
*/
public final void writeInt(int val) {
    nativeWriteInt(mNativePtr, val);
}

//from http://androidxref.com/8.1.0_r33/xref/frameworks/base/core/jni/android_os_Parcel.cpp#785

/*
    接下来进入JNI代码
    nativeWriteInt
    {"nativeWriteInt", "(JI)V", (void*)android_os_Parcel_writeInt},
    调用android_os_Parcel_writeInt方法。
*/
 static void android_os_Parcel_writeInt(JNIEnv* env, 
        jclass clazz, 
        jlong nativePtr, 
        jint val) {
    /*把mNativePtr转换成C++对象指针*/
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    if (parcel != NULL) {
        const status_t err = parcel->writeInt32(val);//继续向下调用
        if (err != NO_ERROR) {
            signalExceptionForError(env, clazz, err);
        }
    }
}
/*
    先看下C++中class Parcel的属性的定义
    from http://androidxref.com/8.1.0_r33/xref/frameworks/native/include/binder/Parcel.h
    
    class Parcel
    {
        //mData表示存储数据的内存空间的首地址,
        //初始值为0
        uint8_t* mData;
        //mDataSize表示当前存储的有效数据的大小,
        //初始值为0
        size_t mDataSize;
        //mDataCapacity表示当前Parcel可用空间的容量,
        //初始值为0
        size_t mDataCapacity;
        //mDataPos表示当前写/读的位置,
        //从 mData+mDataPos开始读/写数据,
        //初始值为0
        mutable size_t  mDataPos;
    };
*/
//from http://androidxref.com/8.1.0_r33/xref/frameworks/native/libs/binder/Parcel.cpp#933
/*
    Parcel中用int32_t存储int
*/
status_t Parcel::writeInt32(int32_t val)
{
    return writeAligned(val);
}

template<class T> status_t Parcel::writeAligned(T val) {
    COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));

    if ((mDataPos+sizeof(val)) <= mDataCapacity) {//0+4<=0 不成立
restart_write:
        /*
            goto跳转到这里,空间申请过了,可以写入了。
        */
        *reinterpret_cast<T*>(mData+mDataPos) = val;//写入int32_t
        //调用finishWrite
        return finishWrite(sizeof(val));
    }

    /*
        运行到这里表示没有可用空间存放数据,需要申请空间
        growData会计算需要申请的空间,并申请空间。
        我们在这里是第一次写入数据到Parcel,比较简单。
        growData会返回NO_ERROR,
    */
    status_t err = growData(sizeof(val));
    //申请完空间后,用goto语句返回restart_write继续写入。
    if (err == NO_ERROR) goto restart_write;
    return err;
}

//len = 4
status_t Parcel::growData(size_t len)
{
    if (len > INT32_MAX) {
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return BAD_VALUE;
    }

    /*
        这里申请空间并不是需要多少申请多少,
        而是大于需要的空间,不是严格的1.5倍。
    */
    //(0+4)*3/2=6
    size_t newSize = ((mDataSize+len)*3)/2;
    /*
        6<0 不成立,调用 continueWrite(newSize)
        continuerite正常会返回 NO_ERROR,
        所以growData会返回NO_ERROR
    */
    return (newSize <= mDataSize)
            ? (status_t) NO_MEMORY
            : continueWrite(newSize);
}

//desired = 6
status_t Parcel::continueWrite(size_t desired)
{
    if (desired > INT32_MAX) {//false
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return BAD_VALUE;
    }

    // If shrinking, first adjust for any objects that appear
    // after the new data size.
    size_t objectsSize = mObjectsSize;
    if (desired < mDataSize) {//6<0, false
        if (desired == 0) {
            objectsSize = 0;
        } else {
            while (objectsSize > 0) {
                if (mObjects[objectsSize-1] < desired)
                    break;
                objectsSize--;
            }
        }
    }

    if (mOwner) {//false
        // If the size is going to zero, just release the owner's data.
        if (desired == 0) {//false
            freeData();
            return NO_ERROR;
        }

        // If there is a different owner, we need to take
        // posession.
        uint8_t* data = (uint8_t*)malloc(desired);
        if (!data) {
            mError = NO_MEMORY;
            return NO_MEMORY;
        }
        binder_size_t* objects = NULL;

        if (objectsSize) {
            objects = (binder_size_t*)calloc(objectsSize, sizeof(binder_size_t));
            if (!objects) {
                free(data);

                mError = NO_MEMORY;
                return NO_MEMORY;
            }

            // Little hack to only acquire references on objects
            // we will be keeping.
            size_t oldObjectsSize = mObjectsSize;
            mObjectsSize = objectsSize;
            acquireObjects();
            mObjectsSize = oldObjectsSize;
        }

        if (mData) {
            memcpy(data, mData, mDataSize < desired ? mDataSize : desired);
        }
        if (objects && mObjects) {
            memcpy(objects, mObjects, objectsSize*sizeof(binder_size_t));
        }
        //ALOGI("Freeing data ref of %p (pid=%d)", this, getpid());
        mOwner(this, mData, mDataSize, mObjects, mObjectsSize, mOwnerCookie);
        mOwner = NULL;

        LOG_ALLOC("Parcel %p: taking ownership of %zu capacity", this, desired);
        pthread_mutex_lock(&gParcelGlobalAllocSizeLock);
        gParcelGlobalAllocSize += desired;
        gParcelGlobalAllocCount++;
        pthread_mutex_unlock(&gParcelGlobalAllocSizeLock);

        mData = data;
        mObjects = objects;
        mDataSize = (mDataSize < desired) ? mDataSize : desired;
        ALOGV("continueWrite Setting data size of %p to %zu", this, mDataSize);
        mDataCapacity = desired;
        mObjectsSize = mObjectsCapacity = objectsSize;
        mNextObjectHint = 0;

    } else if (mData) {//false
        if (objectsSize < mObjectsSize) {
            // Need to release refs on any objects we are dropping.
            const sp<ProcessState> proc(ProcessState::self());
            for (size_t i=objectsSize; i<mObjectsSize; i++) {
                const flat_binder_object* flat
                    = reinterpret_cast<flat_binder_object*>(mData+mObjects[i]);
                if (flat->type == BINDER_TYPE_FD) {
                    // will need to rescan because we may have lopped off the only FDs
                    mFdsKnown = false;
                }
                release_object(proc, *flat, this, &mOpenAshmemSize);
            }
            binder_size_t* objects =
                (binder_size_t*)realloc(mObjects, objectsSize*sizeof(binder_size_t));
            if (objects) {
                mObjects = objects;
            }
            mObjectsSize = objectsSize;
            mNextObjectHint = 0;
        }

        // We own the data, so we can just do a realloc().
        if (desired > mDataCapacity) {
            uint8_t* data = (uint8_t*)realloc(mData, desired);
            if (data) {
                LOG_ALLOC("Parcel %p: continue from %zu to %zu capacity", this, mDataCapacity,
                        desired);
                pthread_mutex_lock(&gParcelGlobalAllocSizeLock);
                gParcelGlobalAllocSize += desired;
                gParcelGlobalAllocSize -= mDataCapacity;
                pthread_mutex_unlock(&gParcelGlobalAllocSizeLock);
                mData = data;
                mDataCapacity = desired;
            } else if (desired > mDataCapacity) {
                mError = NO_MEMORY;
                return NO_MEMORY;
            }
        } else {
            if (mDataSize > desired) {
                mDataSize = desired;
                ALOGV("continueWrite Setting data size of %p to %zu", this, mDataSize);
            }
            if (mDataPos > desired) {
                mDataPos = desired;
                ALOGV("continueWrite Setting data pos of %p to %zu", this, mDataPos);
            }
        }

    } else {//true
        //第一次向Parcel中写入数据
        // This is the first data.  Easy!
        //第一次申请空间,这里用的是 malloc
        uint8_t* data = (uint8_t*)malloc(desired);
        if (!data) {
            mError = NO_MEMORY;
            return NO_MEMORY;
        }

        if(!(mDataCapacity == 0 && mObjects == NULL
             && mObjectsCapacity == 0)) {//false
            ALOGE("continueWrite: %zu/%p/%zu/%zu", mDataCapacity, mObjects, mObjectsCapacity, desired);
        }

        LOG_ALLOC("Parcel %p: allocating with %zu capacity", this, desired);
        pthread_mutex_lock(&gParcelGlobalAllocSizeLock);
        //static size_t gParcelGlobalAllocSize = 0;
        //增加分配的内存空间计数
        gParcelGlobalAllocSize += desired;
        gParcelGlobalAllocCount++;
        pthread_mutex_unlock(&gParcelGlobalAllocSizeLock);
        //把申请的空间赋值给mData
        mData = data;
        mDataSize = mDataPos = 0;
        ALOGV("continueWrite Setting data size of %p to %zu", this, mDataSize);
        ALOGV("continueWrite Setting data pos of %p to %zu", this, mDataPos);
        mDataCapacity = desired;//=6,Parcel空间容量
    }
    //从这个函数返回NO_ERROR
    return NO_ERROR;
}

//len=4
status_t Parcel::finishWrite(size_t len)
{
    if (len > INT32_MAX) {
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return BAD_VALUE;
    }

    //printf("Finish write of %d\n", len);
    /*
        更新mDataPos
    */
    mDataPos += len;
    ALOGV("finishWrite Setting data pos of %p to %zu", this, mDataPos);
    if (mDataPos > mDataSize) {//4>0成立
        mDataSize = mDataPos;//更新mDataSize
        ALOGV("finishWrite Setting data size of %p to %zu", this, mDataSize);
    }
    //printf("New pos=%d, size=%d\n", mDataPos, mDataSize);
    return NO_ERROR;
}

上面是第一次写入一个int,接下来写入一个string。

public final void writeString(String val)

//from http://androidxref.com/8.1.0_r33/xref/frameworks/base/core/java/android/os/Parcel.java

/**
* Write a string value into the parcel at the current dataPosition(),
* growing dataCapacity() if needed.
*/
public final void writeString(String val) {
    mReadWriteHelper.writeString(this, val);
}

//from http://androidxref.com/8.1.0_r33/xref/frameworks/base/core/java/android/os/Parcel.java#352

/**
* Called when writing a string to a parcel. Subclasses wanting to write a string
* must use {@link #writeStringNoHelper(String)} to avoid
* infinity recursive calls.
*/
public void writeString(Parcel p, String s) {
    nativeWriteString(p.mNativePtr, s);
}

//from androidxref.com/8.1.0_r33/xref/frameworks/base/core/jni/android_os_Parcel.cpp#792
 {"nativeWriteString",         "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeString},
 
 //from androidxref.com/8.1.0_r33/xref/frameworks/base/core/jni/android_os_Parcel.cpp#276
 
static void android_os_Parcel_writeString(JNIEnv* env, jclass clazz, jlong nativePtr, jstring val)
{
    //转换成对象指针
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    if (parcel != NULL) {
        status_t err = NO_MEMORY;
        if (val) {
            //转换成 jchar*
            const jchar* str = env->GetStringCritical(val, 0);
            if (str) {
                //调用 writeString16,第二个参数是字符串长度,即jchar的个数
                //第一个参数把jchar*转成了char16_t
                err = parcel->writeString16(
                    reinterpret_cast<const char16_t*>(str),
                    env->GetStringLength(val));
                //需要release str
                env->ReleaseStringCritical(val, str);
            }
        } else {
            err = parcel->writeString16(NULL, 0);
        }
        if (err != NO_ERROR) {
            signalExceptionForError(env, clazz, err);
        }
    }
}

//from androidxref.com/8.1.0_r33/xref/frameworks/native/libs/binder/Parcel.cpp#1063
status_t Parcel::writeString16(const char16_t* str, size_t len)
{
    //字符串为空则写入-1
    if (str == NULL) return writeInt32(-1);
    //先写入int表示字符串长度
    status_t err = writeInt32(len);
    if (err == NO_ERROR) {
        //由于原来的len用的是 char8_t,这里要写入char16_t,
        //所以写入的len要乘以sizeof(char16_t),
        //其实这样写比较容易理解:len *= sizeof(char16_t)/sizeof(char8_t)
        len *= sizeof(char16_t);
        //writeInplace是先占下len+sizeof(char16_t)空间,这里并不写入数据
        //返回的是开始写入的起始地址。
        //等等,为什么是 len+sizeof(char16_t)?不是len?
        //下面有答案
        uint8_t* data = (uint8_t*)writeInplace(len+sizeof(char16_t));
        if (data) {
            //用memcpy函数去写char16_t*串,占用的字节数是len。
            memcpy(data, str, len);
            //前面多占了一个char16_t,这里写入0表示结束
            *reinterpret_cast<char16_t*>(data+len) = 0;
            return NO_ERROR;
        }
        err = mError;
    }
    return err;
}

//from androidxref.com/8.1.0_r33/xref/frameworks/native/libs/binder/Parcel.cpp#719
void* Parcel::writeInplace(size_t len)
{
    if (len > INT32_MAX) {
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return NULL;
    }
    //这里是4个字节对齐,不够补齐
    const size_t padded = pad_size(len);

    // sanity check for integer overflow
    if (mDataPos+padded < mDataPos) {
        return NULL;
    }

    //先检查空间是否足够,
    //上次写入int的时候申请了6个字节的空间,写入int消耗了4个,剩下2个
    //刚刚在写入字符串长度的时候需要写入int,需要4个字节,空间不够
    //会申请(4+4)*3/2=12个字节,
    //写入int后剩下8个字节
    //假如这次我们要写入"hello",需要的空间是 sizeof("hello")*sizeof(char16_t) + sizeof(char16_t) = 12,12个字节已经对齐了。
    //空间不够,看代码。
    if ((mDataPos+padded) <= mDataCapacity) {//false
restart_write:
        //在下面扩充完空间通过goto语句返回到这里,此时空间足够了。
        //printf("Writing %ld bytes, padded to %ld\n", len, padded);
        uint8_t* const data = mData+mDataPos;//data指向开始写入的位置。

        // Need to pad at end?
        if (padded != len) {//false
#if BYTE_ORDER == BIG_ENDIAN
            static const uint32_t mask[4] = {
                0x00000000, 0xffffff00, 0xffff0000, 0xff000000
            };
#endif
#if BYTE_ORDER == LITTLE_ENDIAN
            static const uint32_t mask[4] = {
                0x00000000, 0x00ffffff, 0x0000ffff, 0x000000ff
            };
#endif
            //printf("Applying pad mask: %p to %p\n", (void*)mask[padded-len],
            //    *reinterpret_cast<void**>(data+padded-4));
            *reinterpret_cast<uint32_t*>(data+padded-4) &= mask[padded-len];
        }
        /*
            接下来调用 finishWrite更新mDataPos和mDataSize,就不把这个函数抄在这里了。
        */
        finishWrite(padded);
        /*
            返回要写入的起始位置。回到上面调用这个函数的位置。
        */
        return data;
    }
    //growData重新扩充空间,返回NO_ERROR
    //goto 到 restart_write
    status_t err = growData(padded);
    if (err == NO_ERROR) goto restart_write;
    return NULL;
}

//from androidxref.com/8.1.0_r33/xref/frameworks/native/libs/binder/Parcel.cpp#2471
status_t Parcel::growData(size_t len)
{
    if (len > INT32_MAX) {
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return BAD_VALUE;
    }
    /*
        mDataSize此此时等于8(两个int)
        len等于12
        所以,newSize = (8+12)*3/2=30
    */
    size_t newSize = ((mDataSize+len)*3)/2;
    //接下来调用 continueWrite,会返回NO_ERROR,
    //这个函数也会返回 NO_ERROR
    //会到上面调用这个函数的地方
    return (newSize <= mDataSize)
            ? (status_t) NO_MEMORY
            : continueWrite(newSize);
}

//from androidxref.com/8.1.0_r33/xref/frameworks/native/libs/binder/Parcel.cpp#2534
//desired = 30
status_t Parcel::continueWrite(size_t desired)
{
    if (desired > INT32_MAX) {
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return BAD_VALUE;
    }

    // If shrinking, first adjust for any objects that appear
    // after the new data size.
    size_t objectsSize = mObjectsSize;
    if (desired < mDa taSize) {//false
        if (desired == 0) {
            objectsSize = 0;
        } else {
            while (objectsSize > 0) {
                if (mObjects[objectsSize-1] < desired)
                    break;
                objectsSize--;
            }
        }
    }

    if (mOwner) {//false
        // If the size is going to zero, just release the owner's data.
        if (desired == 0) {
            freeData();
            return NO_ERROR;
        }

        // If there is a different owner, we need to take
        // posession.
        uint8_t* data = (uint8_t*)malloc(desired);
        if (!data) {
            mError = NO_MEMORY;
            return NO_MEMORY;
        }
        binder_size_t* objects = NULL;

        if (objectsSize) {
            objects = (binder_size_t*)calloc(objectsSize, sizeof(binder_size_t));
            if (!objects) {
                free(data);

                mError = NO_MEMORY;
                return NO_MEMORY;
            }

            // Little hack to only acquire references on objects
            // we will be keeping.
            size_t oldObjectsSize = mObjectsSize;
            mObjectsSize = objectsSize;
            acquireObjects();
            mObjectsSize = oldObjectsSize;
        }

        if (mData) {
            memcpy(data, mData, mDataSize < desired ? mDataSize : desired);
        }
        if (objects && mObjects) {
            memcpy(objects, mObjects, objectsSize*sizeof(binder_size_t));
        }
        //ALOGI("Freeing data ref of %p (pid=%d)", this, getpid());
        mOwner(this, mData, mDataSize, mObjects, mObjectsSize, mOwnerCookie);
        mOwner = NULL;

        LOG_ALLOC("Parcel %p: taking ownership of %zu capacity", this, desired);
        pthread_mutex_lock(&gParcelGlobalAllocSizeLock);
        gParcelGlobalAllocSize += desired;
        gParcelGlobalAllocCount++;
        pthread_mutex_unlock(&gParcelGlobalAllocSizeLock);

        mData = data;
        mObjects = objects;
        mDataSize = (mDataSize < desired) ? mDataSize : desired;
        ALOGV("continueWrite Setting data size of %p to %zu", this, mDataSize);
        mDataCapacity = desired;
        mObjectsSize = mObjectsCapacity = objectsSize;
        mNextObjectHint = 0;

    } else if (mData) {//true
        if (objectsSize < mObjectsSize) {//false
            // Need to release refs on any objects we are dropping.
            const sp<ProcessState> proc(ProcessState::self());
            for (size_t i=objectsSize; i<mObjectsSize; i++) {
                const flat_binder_object* flat
                    = reinterpret_cast<flat_binder_object*>(mData+mObjects[i]);
                if (flat->type == BINDER_TYPE_FD) {
                    // will need to rescan because we may have lopped off the only FDs
                    mFdsKnown = false;
                }
                release_object(proc, *flat, this, &mOpenAshmemSize);
            }
            binder_size_t* objects =
                (binder_size_t*)realloc(mObjects, objectsSize*sizeof(binder_size_t));
            if (objects) {
                mObjects = objects;
            }
            mObjectsSize = objectsSize;
            mNextObjectHint = 0;
        }

        // We own the data, so we can just do a realloc().
        if (desired > mDataCapacity) {//true, 30 > 12
            //调用realloc扩充内存空间,原来已经写入的数据不会变
            uint8_t* data = (uint8_t*)realloc(mData, desired);
            if (data) {//扩充空间成功
                LOG_ALLOC("Parcel %p: continue from %zu to %zu capacity", this, mDataCapacity,
                        desired);
                pthread_mutex_lock(&gParcelGlobalAllocSizeLock);
                gParcelGlobalAllocSize += desired;//增加空间计数
                gParcelGlobalAllocSize -= mDataCapacity;
                pthread_mutex_unlock(&gParcelGlobalAllocSizeLock);
                mData = data;//mData重新指向空间起始位置
                mDataCapacity = desired;//更新mDataCapacity = 30
            } else if (desired > mDataCapacity) {
                mError = NO_MEMORY;
                return NO_MEMORY;
            }
        } else {
            if (mDataSize > desired) {
                mDataSize = desired;
                ALOGV("continueWrite Setting data size of %p to %zu", this, mDataSize);
            }
            if (mDataPos > desired) {
                mDataPos = desired;
                ALOGV("continueWrite Setting data pos of %p to %zu", this, mDataPos);
            }
        }

    } else {
        // This is the first data.  Easy!
        uint8_t* data = (uint8_t*)malloc(desired);
        if (!data) {
            mError = NO_MEMORY;
            return NO_MEMORY;
        }

        if(!(mDataCapacity == 0 && mObjects == NULL
             && mObjectsCapacity == 0)) {
            ALOGE("continueWrite: %zu/%p/%zu/%zu", mDataCapacity, mObjects, mObjectsCapacity, desired);
        }

        LOG_ALLOC("Parcel %p: allocating with %zu capacity", this, desired);
        pthread_mutex_lock(&gParcelGlobalAllocSizeLock);
        gParcelGlobalAllocSize += desired;
        gParcelGlobalAllocCount++;
        pthread_mutex_unlock(&gParcelGlobalAllocSizeLock);

        mData = data;
        mDataSize = mDataPos = 0;
        ALOGV("continueWrite Setting data size of %p to %zu", this, mDataSize);
        ALOGV("continueWrite Setting data pos of %p to %zu", this, mDataPos);
        mDataCapacity = desired;
    }
    //最后返回 NO_ERROR, 回到上面调用这个函数的地方。
    return NO_ERROR;
}

通过上面的代码,在写入一个java int的时候,会调用C++的实现去做。
先判断Parcel的空间是否足够,不够的话就去申请足够的空间(比需要的空间多),第一次申请空间用的是malloc函数,后面因为空间不够重新申请空间就会使用realloc,realloc可以使一块内存扩大或缩小,这里是扩大内存,原始的内存里面的数据还会存在。
申请空间完成后,向新的空间中继续写入。

写入java string的时候,先写入string长度,在把string转换成char16_t*写入Parcel,外加一个char16_t 0.

writeBoolean函数会把boolean转换成int, true=1, false=0.
writeStringArray函数会先写入一个表示字符串数组长度的int,然后再把string挨个写入。

这几个是典型的基本数据类型写入Parcel,从Parcel中读取基本数据类型就是上面的逆过程。

注释里面说了,Parcel主要是为IPC的目的做的,所以重点和难点在于写入IBinder对象。由于涉及到IBinder,建议先看过IBinder后再来看写入对象。事实上,看IBinder的过程中,会有向Parcel读写IBinder对象的过程,那个时候再回到这里来看。

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

推荐阅读更多精彩内容