底层研究 - AutoreleasePool

1、前言

AutoreleasePool是Objective-C 中自动释放池,在iOS项目的mian.m文件中,可以发现如下代码

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

这意味着将所有的事件、消息全部交给了 UIApplication 来处理,而这个应用则是包含在一个自动释放池的block中。

在源码中,有一段AutoreleasePool的解释

Autorelease pool implementation

- A thread's autorelease pool is a stack of pointers. 
线程的自动释放池是指针的栈

- Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
每个指针都是要释放的对象,或者是POOL_BOUNDARY,它是自动释放池的边界。

- A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
池令牌是指向该池的POOL_BOUNDARY的指针。弹出池后,将释放比哨点更热的每个对象。

- The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary. 
堆栈分为两个双向链接的页面列表。根据需要添加和删除页面。

- Thread-local storage points to the hot page, where newly autoreleased objects are stored. 
线程本地存储(TLS)指向热页面,该页面存储新自动释放的对象。

从这段说明中,可以提取几个关键词:栈,POOL_BOUNDARY,双向链表,热页面,TLS,接下来将围绕着几个点来具体了解下

2、AutoreleasePoolPage

既然自动释放池是一个,那么就会有出栈和入栈操作,从源码中可以发现

//出栈
void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}
//入栈
void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

从中可以发现出栈入栈都是依靠AutoreleasePoolPage的pop和push方法来实现的,那么AutoreleasePoolPage又是啥呢?

在源码中发现:

//NSObject-internal.h
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
    //校验 AutoreleasePoolPage 的结构是否完整
    magic_t const magic;
    //指向最新添加的 autoreleased 对象的下一个位置,初始化时指向begin()
    __unsafe_unretained id *next;
    //当前线程
    pthread_t const thread;
    //指向前一个page的指针, 即双向链表上一个节点
    AutoreleasePoolPage * const parent;
    //指向后一个page的指针,即双向链表下一个节点
    AutoreleasePoolPage *child;
    //双向链表的深度,即该链表中结点的个数。
    uint32_t const depth;
    //high water mark,数据容纳的一个上限。
    uint32_t hiwat;

    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};

可以得知:
AutoreleasePoolPageData结构体的内存大小为56字节

  • 属性magic 的类型是magic_t结构体,所占内存大小为m[4];所占内存(即 4 x 4 = 16 字节)
  • 属性next(指针)、thread(对象)、parent(对象)、child(对象)均占8字节(即 4 x 8 = 32 字节)
  • 属性depth、hiwat类型为 uint32_t,实际类型是unsigned int类型,均占4字节(即 2 x 4 = 8 字节)
//AutoreleasePool.mm
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    friend struct thread_data_t;

public:
    static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MIN_SIZE;  // size and alignment, power of 2
#endif
    
private:
    static pthread_key_t const key = 38;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const COUNT = SIZE / sizeof(id);

    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
    // pushed and it has never contained any objects. This saves memory
    // when the top level (i.e. libdispatch) pushes and pops pools but
    // never uses them.
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

#   define POOL_BOUNDARY nil
...
}
#define BYTE_SIZE               8               /* byte size in bits */

#define I386_PGBYTES            4096            /* bytes per 80386 page */
#define I386_PGSHIFT            12              /* bitshift for pages */


#if !defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || (__MAC_OS_X_VERSION_MIN_REQUIRED < 101600)
#define PAGE_SHIFT              I386_PGSHIFT
#define PAGE_SIZE               I386_PGBYTES
#define PAGE_MASK               (PAGE_SIZE-1)
#else /* !defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || (__MAC_OS_X_VERSION_MIN_REQUIRED < 101600) */
#define PAGE_SHIFT              vm_page_shift
#define PAGE_SIZE               vm_page_size
#define PAGE_MASK               vm_page_mask
#endif /* !defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || (__MAC_OS_X_VERSION_MIN_REQUIRED < 101600) */

#define PAGE_MAX_SHIFT          14
#define PAGE_MAX_SIZE           (1 << PAGE_MAX_SHIFT)
#define PAGE_MAX_MASK           (PAGE_MAX_SIZE-1)

#define PAGE_MIN_SHIFT          12
#define PAGE_MIN_SIZE           (1 << PAGE_MIN_SHIFT)
#define PAGE_MIN_MASK           (PAGE_MIN_SIZE-1)

通过上面的代码,可以发现
(1)AutoreleasePoolPage的大小一般都是4096字节
(2)AutoreleasePool是以AutoreleasePoolPage为节点构成双链表形式连接起来的
(3)POOL_SENTINEL(哨兵对象)其实是一个nil的别名
(4)结合上面的结论,可以发现page能够存储的对象为(4096 - 56)/ 8 = 505 个对象,第一页因为有哨兵,所以第一页只有504个对象

    //构造函数
    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),
                                objc_thread_self(),
                                newParent,
                                newParent ? 1+newParent->depth : 0,
                                newParent ? newParent->hiwat : 0)
    {
        if (parent) {
            parent->check();
            ASSERT(!parent->child);
            parent->unprotect();
            parent->child = this;
            parent->protect();
        }
        protect();
    }
    //析构函数
    ~AutoreleasePoolPage()
    {
        check();
        unprotect();
        ASSERT(empty());

        // Not recursive: we don't want to blow out the stack
        // if a thread accumulates a stupendous amount of garbage
        ASSERT(!child);
    }

    template<typename Fn>
    void
    busted(Fn log) const
    {
        magic_t right;
        log("autorelease pool page %p corrupted\n"
             "  magic     0x%08x 0x%08x 0x%08x 0x%08x\n"
             "  should be 0x%08x 0x%08x 0x%08x 0x%08x\n"
             "  pthread   %p\n"
             "  should be %p\n",
             this,
             magic.m[0], magic.m[1], magic.m[2], magic.m[3],
             right.m[0], right.m[1], right.m[2], right.m[3],
             this->thread, objc_thread_self());
    }

    __attribute__((noinline, cold, noreturn))
    void
    busted_die() const
    {
        busted(_objc_fatal);
        __builtin_unreachable();
    }

    inline void
    check(bool die = true) const
    {
        if (!magic.check() || thread != objc_thread_self()) {
            if (die) {
                busted_die();
            } else {
                busted(_objc_inform);
            }
        }
    }

    inline void
    fastcheck() const
    {
#if CHECK_AUTORELEASEPOOL
        check();
#else
        if (! magic.fastcheck()) {
            busted_die();
        }
#endif
    }


    id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

    id * end() {
        return (id *) ((uint8_t *)this+SIZE);
    }

    bool empty() {
        return next == begin();
    }

    bool full() {
        return next == end();
    }

    bool lessThanHalfFull() {
        return (next - begin() < (end() - begin()) / 2);
    }

    id *add(id obj)
    {
        printf("%p----\n", this);
        ASSERT(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

  • fastcheck() 快速检查page的完整性和是否在当前线程
  • begin() 表示了一个AutoreleasePoolPage节点开始存autorelease对象的位置。
  • end() 一个AutoreleasePoolPage节点最大的位置
  • empty() 如果next指向beigin()说明为空
  • full() 如果next指向end()说明满了
  • lessThanHalfFull() 已用空间小于一半总空间
  • id *add(id obj) 添加一个autorelease对象,next指向下一个存对象的地址。

那么自动释放池中的栈具体是怎样的呢

2.1 objc_autoreleasePoolPush

    static inline void *push()
    {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // Each autorelease pool starts on a new pool page.
            //创建新页,存储哨兵
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            //存储哨兵
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        printf("%p\n", dest);

        return dest;
    }

DebugPoolAllocation判断是否需要生成一个新page,这里涉及了两个方法autoreleaseNewPage和autoreleaseFast

OPTION( DebugPoolAllocation, OBJC_DEBUG_POOL_ALLOCATION, "halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools")
源码中有一个DebugPoolAllocation的相关定义,翻译过来就是当自动释放池被打乱顺序时暂停,并允许堆调试器跟踪自动释放池

//创建新页
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
    //获取当前操作页
    AutoreleasePoolPage *page = hotPage();
    //如果存在,则压栈对象
    if (page) return autoreleaseFullPage(obj, page);
    //如果不存在,则创建页
    else return autoreleaseNoPage(obj);
}

static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
   if (page && !page->full()) {
        //当前hotPage存在且未满,直接添加到当前hotPage
        return page->add(obj);
   } else if (page) {
        //当前hotPage存在且满了,则创建一个新的page并设置为hotPage,然后添加对象到hotPage
        return autoreleaseFullPage(obj, page);
    } else {
        //hotPage不存在创建一个hotPage
        return autoreleaseNoPage(obj);
    }
 }

//******** hotPage方法 ********
//获取当前操作页
static inline AutoreleasePoolPage *hotPage() 
{
    //获取当前页
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key);
    //如果是一个空池,则返回nil,否则,返回当前线程的自动释放池
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
    if (result) result->fastcheck();
    return result;
}

static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    // The hot page is full. 
    // Step to the next non-full page, adding a new page if necessary.
    // Then add the object to that page.
    ASSERT(page == hotPage());
    ASSERT(page->full()  ||  DebugPoolAllocation);

   //通过循环查找最后一页未满页,如果最后一页已满则新建一页
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

//******** autoreleaseNoPage方法 ********
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    // "No page" could mean no pool has been pushed
    // or an empty placeholder pool has been pushed and has no contents yet
    ASSERT(!hotPage());

    bool pushExtraBoundary = false;
    //判断是否是空占位符,如果是,则压栈哨兵标识符置为YES
    if (haveEmptyPoolPlaceholder()) {
        // We are pushing a second pool over the empty placeholder pool
        // or pushing the first object into the empty placeholder pool.
        // Before doing that, push a pool boundary on behalf of the pool 
        // that is currently represented by the empty placeholder.
        pushExtraBoundary = true;
    }
    //如果对象不是哨兵对象,且没有Pool,则报错
    else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        // We are pushing an object with no pool in place, 
        // and no-pool debugging was requested by environment.
        _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug", 
                     objc_thread_self(), (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }
    //如果对象是哨兵对象,且没有申请自动释放池内存,则设置一个空占位符存储在tls中,其目的是为了节省内存
    else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {//如果传入参数为哨兵
        // We are pushing a pool with no pool in place,
        // and alloc-per-pool debugging was not requested.
        // Install and return the empty pool placeholder.
        return setEmptyPoolPlaceholder();//设置空的占位符
    }

    // We are pushing an object or a non-placeholder'd pool.

    // Install the first page.
    //初始化第一页
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    //设置page为当前聚焦页
    setHotPage(page);
    
    // Push a boundary on behalf of the previously-placeholder'd pool.
    //压栈哨兵的标识符为YES,则压栈哨兵对象
    if (pushExtraBoundary) {
        //压栈哨兵
        page->add(POOL_BOUNDARY);
    }
    
    // Push the requested object or pool.
    //压栈对象
    return page->add(obj);
}

总结一下push的逻辑:

  1. 首先判断是否需要生成一个新的page,需要则执行autoreleaseNewPage方法,否则执行autoreleaseFast方法
  2. 当进入autoreleaseNewPage时:
    (1)当前存在page执行 autoreleaseFullPage 方法
    (2)当前不存在执行 pageautoreleaseNoPage 方法
  3. 当进入autoreleaseFast方法时:
    (1)存在page且未满,通过 add 方法进行添加
    (2)当前page已满执行 autoreleaseFullPage 方法
    (3)当前不存在page执行 autoreleaseNoPage 方法。

2.2 autorelease

static inline id autorelease(id obj)
{
    ASSERT(obj);
//        ASSERT(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    printf("%p\n", dest);
    return obj;
}

autorelease跟 push 操作的实现非常相似。只不过 push 操作插入的是一个 POOL_SENTINEL ,而 autorelease 操作插入的是一个具体的 autoreleased 对象。

2.3 objc_autoreleasePoolPop

static inline void
    pop(void *token)
    {
        //token为自动释放池中存储的要被释放的对象,当创建出autoreleasePool后如果没有存放数据token则为EMPTY_POOL_PLACEHOLDER的地址
        AutoreleasePoolPage *page;
        id *stop;
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            //如果token存在,但是自动释放池中指向的是一个空池,(有可能自动释放池创建了出来,但是什么对象都没有存)
            // Popping the top-level placeholder pool.
            //则先取到当前hotpage
            page = hotPage();
            if (!page) {
                //如果hotpage为空,则把当前线程所关联的page置为nil,然后返回(也就相当于pop了所有对象)
                // Pool was never used. Clear the placeholder.
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            //在hotpage有值的情况(pool被使用)下,查找hotpage的父节点(由于其父节点有可能为nil,所以递归查找其直属父节点),
            page = coldPage();
            //这里将指针指向该page的开始
            token = page->begin();
        } else {
            //如果token不指向EMPTY_POOL_PLACEHOLDER,返回该page的首地址
            page = pageForPointer(token);
        }

        stop = (id *)token;
        // 判断最后一个位置,是否是POOL_BOUNDARY
        if (*stop != POOL_BOUNDARY) {
            //判断当前page是否指向开始,以及其父parent节点为空,上边的方法中做了这些处理,相当于清空当前page数据,否则执行badPop
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }
        //执行popPage,出栈
        return popPage<false>(token, page, stop);
    }

    static inline AutoreleasePoolPage *coldPage()
    {
        //有数据循环到首页,无数据则为nil
        AutoreleasePoolPage *result = hotPage();
        if (result) {
            while (result->parent) {
                result = result->parent;
                result->fastcheck();
            }
        }
        return result;
    }

    static AutoreleasePoolPage *pageForPointer(uintptr_t p)
    { 
        AutoreleasePoolPage *result;
        //该地址取余page的大小,得出该地址的偏移量相对于该page的偏移量
        uintptr_t offset = p % SIZE;

        ASSERT(offset >= sizeof(AutoreleasePoolPage));
        //获取该页首地址
        result = (AutoreleasePoolPage *)(p - offset);
        result->fastcheck();

        return result;
    }

    template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        if (allowDebug && PrintPoolHiwat) printHiwat();
        //循环遍历删除page上存储的数据,并把autorelease对象置为SCRIBBLE
        page->releaseUntil(stop);

        // memory: delete empty children
        if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top)
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }

    //从栈顶一直释放对象,直到stop(哨兵对象的地址)
    void releaseUntil(id *stop)
    {
        // Not recursive: we don't want to blow out the stack
        // if a thread accumulates a stupendous amount of garbage
        
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release
            // autoreleased more objects
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            ASSERT(page->empty());
        }
#endif
    }

    void kill()
    {
        // Not recursive: we don't want to blow out the stack
        // if a thread accumulates a stupendous amount of garbage
        AutoreleasePoolPage *page = this;
        //循环获取最后一页
        while (page->child) page = page->child;

        AutoreleasePoolPage *deathptr;
        do {
            deathptr = page;
            page = page->parent;
            if (page) {
                page->unprotect();
                page->child = nil;
                page->protect();
            }
            delete deathptr;
        } while (deathptr != this);
    }

总结一下pop:

  1. pop函数会将POOL_BOUNDARY的内存地址传进去
  2. autorelease对象从end的结束地址开始进行发送release消息,一直找到POOL_BOUNDARY为止
  3. 一旦发现当前页已经空了,就会去上一个页面进行pop,并释放当前页面

2.4 TLS

线程局部存储(Thread Local Storage,TLS)主要用于在多线程中,存储和维护一些线程相关的数据,存储的数据会被关联到当前线程中去,并不需要锁来维护,因此也没有多线程间资源竞争问题,在源码中也是多次使用到,通过固定的key来进行存取hotPage

 static pthread_key_t const key = 38;

//取值
static inline void *tls_get_direct(tls_key_t k)
{
//    ASSERT(is_valid_direct_key(k));

    if (_pthread_has_direct_tsd()) {
        return _pthread_getspecific_direct(k);
    } else {
        return pthread_getspecific(k);
    }
}
//存值
static inline void tls_set_direct(tls_key_t k, void *value)
{
//    ASSERT(is_valid_direct_key(k));

    if (_pthread_has_direct_tsd()) {
        _pthread_setspecific_direct(k, value);
    } else {
        pthread_setspecific(k, value);
    }
}

 static inline void setHotPage(AutoreleasePoolPage *page)
{
    if (page) page->fastcheck();
    tls_set_direct(key, (void *)page);
}

对照 setHotPage 发现tls_set_direct传入的为key-value,即pthread_key_t线程key值与当前AutoreleasePoolPage;那么在取的时候,取的也就是当前AutoreleasePoolPage

那为什么要用TLS呢?

  • hotPage可以看做一个全局变量,在多线程访问下,不用TLS直接更改参数就会造成数据污染

那为什么要用inline内联?

  • 如果直接获取TLS变量,在有大量使用TLS变量的地方,寻址所消耗的时间将会增加一倍

3、 总结

  1. autoreleasepool本身并没有内部结构,而是一种通过AutoreleasePoolPage为节点的双向链表结构
  2. 初始化poolpage对象属性之外,还会插入一个POOL_SENTINEL哨兵,用来区分不同autoreleasepool之间包裹的对象
  3. 当对象调用 autorelease 方法时,会将实际对象插入 AutoreleasePoolPage 的栈中,通过next指针移动
  4. page能够存储的对象为(4096 - 56)/ 8 = 505 个对象,第一页因为有哨兵,所以第一页只有504个对象
  5. push操作是先判断hotPage,hotPage没满则直接添加;满了则创建新页,再添加;没有hotPage则创建一个hotPage,压入哨兵POOL_BOUNDARY
  6. pop操作是传入一个POOL_BOUNDARY的内存地址,从最后一个入栈的autorelease对象开始,将自动释放池中的autorelease对象全部释放(实际上是给它们发送一条release消息),直到遇到这个POOL_BOUNDARY ,next指针移到目标哨兵。
  7. 使用TLS来存储hotPage
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容