iOS 内存管理 部分四

主要讲解Autoreleasepool的原理;

本文中的测试代码主要为 MRC 环境;
文中使用的 runtime源码是objc4-781版本;

iOS 内存管理 部分一
iOS 内存管理 部分二
iOS 内存管理 部分三
iOS 内存管理 部分四


1. 什么是 Autoreleasepool?

从字面意思即可得知是自动释放池, 其作用就是在@autoreleasepool结束的时候会将其中的对象进行回收,释放内存空间;
它有如下特点;

    1. Autoreleasepool的底层也是一个结构体, 其存储结构类似栈,先入的autorelease对象后被释放;编译器会将@autoreleasepool{autorelease对象}变为以下代码
      struct{
      /*
       push 函数入参POOL_BOUNDARY返回一个地址, 这个地址是起始位置, 
       从这个开始, 下一个位置开始存放 autorelease 对象, 
      */
      void *ctx = objc_autoreleasePoolPush();
         push进autorelease对象
         push进autorelease对象
         push进autorelease 对象
      objc_autoreleasePoolPop(ctx);
      }
      
    1. @autoreleasepool是由一个一个的autoreleasepoolpage组成, 所有的autoreleasepoolpage通过双向链表连接在一次, 每个autoreleasepoolpage占用4096个字节;除了自身成员变量所占用空间, 其余的都是用来存放autorelease对象(MRC环境);
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
    magic_t const magic;
    __unsafe_unretained id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
};
magic用来校验AutoreleasePoolPage结构是否完整;
next指向当前栈顶第一个可用的地址;
thread指向当前的线程;
parent双向链表中指向父指针;
child双向链表中指向子指针;

通过双向链表将多个autoreleasepoolpage连接起来;

3. Autoreleasepool的流程;

我们知道Autoreleasepool底层是一个或者多个AutoreleasepoolPage通过childparent连接起来的双向链表结构, 这就类似栈的结构, 后面加入的总能最先被使用或者释放, 因此如果有多个AutoreleasepoolPage时, 肯定是前面的AutoreleasepoolPage空间被用完了, 而链表尾部AutoreleasepoolPage空间未满仍可以使用, 从源码中也可以看到hotPage就是链表尾部的AutoreleasepoolPage, 而 coldPage则是栈底的AutoreleasepoolPage;因此我们可以整理Autoreleasepool使用时的执行流程如下

  • 3.1 首先Autoreleasepool底层转化为结构体, 实际上一个一个AutoreleasePage 双向链表结构连接起来;
  • 3.2 执行Autoreleasepool代码时, 也就是执行AutoreleasepoolPagepush操作时, 首先 判断pool中是否有Autorelease对象, 如果没有则创建一个占位 pool并返回结束;
  • 3.3 如果有Autorelease对象, 判断是否有 hotPage;

    a. 如果有hotPage则判断是否够用, 不够用则创建新的 page并通过childparent指针连接, 同时设置hotPage;
    b. 如果没有hotPage则创建新的 page并设置为hotPage;

  • 3.4 在往AutoreleasePage(hotPage) 添加Autorelease对象时,实际执行两个步骤;

    a. 如果是第一次添加Autorelease对象, 首先往hotPage中添加POOL_BOUNDARY指针并返回它地址, 同时next指针向后移动; 如果不是第一次添加则字节执行下面步骤;
    b. 往hotPage中添加Autorelease对象指针, 同时next指针向后移动;

  • 3.5 在Autoreleasepool代码结束时, 也就是后面大括号时, 开始执行AutoreleasepoolPagepop操作时, 入参3.4.a 返回的POOL_BOUNDARY地址, 作为标志(也就是大家说的哨兵对象地址)
    然后开始从hotPage通过while循环开始释放Autorelease对象, 如果hotPage释放完毕仍然未到达标志, 则通过parent指针向上查找, 继续释放; 释放的同时next--跟着移动和重新设置hotPage;
    释放到达POOL_BOUNDARY 标志位时结束, 同时释放childPage的空间;

备 :抽时间整理流程图;

补充 Autoreleasepool 的底层结构和源码大致逻辑

1.Autoreleasepool的底层结构

通过clang语句将下面代码转转后

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        Model *model = [[Model alloc] init];
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

可以得到Autoreleasepool其底层结构如下,

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
        Model *model = ((Model *(*)(id, SEL))(void *)objc_msgSend)((id)((Model *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Model"), sel_registerName("alloc")), sel_registerName("init"));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);

===>
#autoreleasepool 转化为了__AtAutoreleasePool, 直接在.cpp 中搜索可以得知
struct __AtAutoreleasePool {
      ///构造函数, 在结构体创建的时候调用
  __AtAutoreleasePool() {
        atautoreleasepoolobj = objc_autoreleasePoolPush();
     }
    ///析构函数, 在结构体销毁的时候调用;
  ~__AtAutoreleasePool() {
        objc_autoreleasePoolPop(atautoreleasepoolobj);
      }
  void * atautoreleasepoolobj;
};

===>
#去 objc4-781版本源码中查询objc_autoreleasePoolPush的结构如下, 
# 注意它是返回一个指针地址, 这个地址是POOL_BOUNDARY 的地址;
# 当在执行 pop 操作时入参这个地址, 表示释放到这个位置为止
void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

===>
#AutoreleasePoolPage的结构如下(继承自AutoreleasePoolPageData)
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{        
         ///验证AutoreleasePoolPageData 的完整性
    magic_t const magic;
        ///指向当前AutoreleasePoolPageData栈顶的可用空间的指针
    __unsafe_unretained id *next;
        ///当前线程
    pthread_t const thread;
         ///双向链表结构, 指向前面AutoreleasePoolPageData
    AutoreleasePoolPage * const parent;
        ///双向链表结构, 指向后面AutoreleasePoolPageData
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
        ///构造函数, 注意 child 默认传入的是空, parent指向前面的 page;
    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)
    {
    }
};
2.Autoreleasepoolpage的的 push 逻辑

我们接着上面的源码继续看Autoreleasepoolpagepush过程
首先看下一个重要的参数 POOL_BOUNDARY他的定义如下

 #   define POOL_BOUNDARY nil

我们知道Autoreleasepoolpage中的next指向当前栈顶的可用地址; 下面以如下代码来讲解下POOL_BOUNDARYnext的协调作用

    @autoreleasepool {
        for (int i = 0; i < 5;  i++) {
            Model *model = [[Model alloc] init];
        }
    }

假设刚开始的时候POOL_BOUNDARY = nilnext指向当前栈顶的可用位置地址, 当我们向
Autoreleasepoolpage添加变量时, 这时POOL_BOUNDARY变为原先 next位置, 同时next向栈顶移动;

#下面看下Autoreleasepoolpage的 push函数
static inline void *push()   {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // Each autorelease pool starts on a new pool page.
            /*
          特殊情况: 调试模式下每一个 autoreleapool 都会创建新的 page; 
          判断条件为DebugPoolAllocation, 请点击查看其成立条件;
          正常使用autoreleapool不会走此流程, 正常情况下即使是多个 autoreleapool嵌套, 
          他们也公用 page, 通过各自 的 POOL_BOUNDARY 来作为边界;
            */
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            ///正常情况下autoreleapool 的push逻辑
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

===>

# 看下autoreleaseFast(POOL_BOUNDARY)的执行逻辑;
    static inline id *autoreleaseFast(id obj)   {
         /*
        注意这个名字 hotPage, hot说明是正在使用的 page, 例如, 如果有多个AutoreleasePoolPage
        那么一定是前面的AutoreleasePoolPage已经存储满了, 所以使用最后面的AutoreleasePoolPage;  
        这个空间尚未使用满的AutoreleasePoolPage称为hotPage; 注意它的值可能是 nil, 
        因为如果AutoreleasePool 中并没有autoreleas 对象时, 
        并不会创建AutoreleasePoolPage, 这时 hotPage = nil;
        */
        AutoreleasePoolPage *page = hotPage();
        ///如果当前page 存在且没有使用完则执行add操作
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            ///如果当前 page 已经使用完毕. 则执行autoreleaseFullPage操作
            return autoreleaseFullPage(obj, page);
        } else {
            ///当前 page 不存在, 执行autoreleaseNoPage; 特殊情况不深入探究
            return autoreleaseNoPage(obj);
        }
    }

===>
# 下面看下 没有AutoreleasePoolPage时的添加逻辑:
 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());
         ///注意这个标志, 是否需要 PUSH 进去额外的 POOL_BOUNDARY 对象
        bool pushExtraBoundary = false;
        /*
        第一次执行 AutoreleasePoolPage::push 操作时, 此时haveEmptyPoolPlaceholder() == false, 所以条件不满足;
        从第二次开始, 这个条件就满足了, 会将pushExtraBoundary = true;
        */
        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 中添加元素, 直接返回 nil
        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;
        }
        /*
            注意这个判断条件:  当第一次AutoreleasePoolPage::push时入参POOL_BOUNDARY满足, 
            调用setEmptyPoolPlaceholder(), 然后haveEmptyPoolPlaceholder() == true;
            如果嵌套AutoreleasePool时, 再次调用AutoreleasePoolPage::push时, 如果没有实际
            添加autorelease 对象, 则此条件仍然满足, 因为入参仍然是 POOL_BOUNDARY;
            只有实际添加autorelease对象入栈时此条件才不满足;
        */
        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.
            ///返回的一个占位 pool, 实际上此时还没有开辟AutoreleasePoolPage;
            return setEmptyPoolPlaceholder();
        }

        // We are pushing an object or a non-placeholder'd pool.
        // Install the first page.
        ///实际上第一次添加autorelease对象到AutoreleasePool 时正式创建AutoreleasePoolPage;
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        ///因为pushExtraBoundary == true, 所以实际上第一次添加autorelease对象 时, 是先添加POOL_BOUNDARY指针;
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        // Push the requested object or pool.
        ///正式添加autorelease对象
        return page->add(obj);
    }

===>
#实际的 add 操作
    id *add(id obj)    {
        ASSERT(!full());
        unprotect();
        ///首先将 next 指针记录下来
        id *ret = next;  // faster than `return next-1` because of aliasing
        /*
        这行语句分解开等价于  *next = obj;  *next ++;
        因为根据运算符的结合性,  从右向左结合, 先执行=运算符;
        执行后把POOL_BOUNDAY 或者 autorelease 对象放入 next 原本位置;
        然后 next 的指向向后移动;
        */
        *next++ = obj;
        protect();
        return ret;
    }

===>
/*
如果 hotPage 满了的时候 需要向后移动查找尚未;
调用autoreleaseFullPage(obj, page); 注意入参, 除了 obj(POOL_BOUNDARY)
 还有一个参数是 page, 作用是需要把 page 连接起来
*/
    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);
        ///如果 child 指针指向的 page 存在则设置为 hotPage, 否则创建新的设置为 hotPage
        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());
        ///设置当前HotPage
        setHotPage(page);
        ///当 page 创建成功后, 再执行新的添加操作;
        return page->add(obj);
    }

不考虑占位 autoreleasepool 时的添加过程

3.Autoreleasepoolpage的的 pop 逻辑
 static inline void
    pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
        /*
        根据AutoreleasePoolPage的 push 我们知道它返回的是结构体 的地址;
        首先判断是否最后一个是不是空的占位 pool, 如果是占位 pool 则执行;
        如果多个AutoreleasePool嵌套(或者一个 pool), 最内层没有实际Autorelease的对象, 
         那么它一定是一个占位 pool, 实际上没有开辟AutoreleasePoolPage;
        */
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            page = hotPage();
            ///如果没有 hotPage , 说明没有AutoreleasePoolPage, 只有占位 pool;只需要将占位pool 置 nil 即可;
            if (!page) {
                // Pool was never used. Clear the placeholder.
                //看这句话翻译, pool 没有使用(就是没有添加Autorelease对象), 清理占位 pool;
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            /*
            注意官方注释: pool 被使用了, 那就正常的 pop;将 page 指针指向 coldpage;
            关于 coldpage 的定义, 请自行去看下就行, 实际上就是指向栈底的那个 AutoreleasePoolPage;
            然后将 token 指针指实际的 page(去掉占位 pool ) 的 begin();
            */
            page = coldPage();
            token = page->begin();
        } else {
            /*
          走到这里说明所有的 page 都是被使用状态, 没有占位 pool;
           调用pageForPointer()函数获取入参 page 数据信息;
            */
            page = pageForPointer(token);
        }
        /*
        设置 stop 为 token ; 
        如果链表尾部是实际的 page:
            这个 token 的值是链表尾部 page 的POOL_BOUNDAY 的地址;
        如果链表尾部是占位的 pool:
            这个 token 的值是去掉占位 pool 后链表尾部 page 的POOL_BOUNDAY 的地址;
         */
        stop = (id *)token;
        ///这里判断是祛除特殊情况
        if (*stop != POOL_BOUNDARY) {
            ///正常情况, 不做任何处理
            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.
                 /*
                 经过上面判断, stop 应该指向入参 page 的 begin()地址;
                page 指向入参的 page, 所以它的 parent 指向应该是空;
                出现错误,执行 badPop(), 请自行查看, 文中不再贴出具体逻辑;
                */
                return badPop(token);
            }
        }
        ///特殊情况, 调试模式(每个 pool 都会创建新的 page)不再深入探究, 请自行查看;
        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }
        ///执行popPage 操作, 注意入参 false
        return popPage<false>(token, page, stop);
    }

 ===>
#执行 pagePop 操作, allowDebug的值是 false
  template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        if (allowDebug && PrintPoolHiwat) printHiwat();
        ///实际的释放操作, autorelease 对象在这里释放的
        page->releaseUntil(stop);

        // memory: delete empty children
        ///删除多余的 chiildPage;
        ///特殊情况/调试模式,  不深究
        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);
        /*
      正常情况, 执行 childPage的 kill 操作
      注意由于 child 默认是空, 所以, 执行到里面是一定是多个 page 连接的情况
        */
        } else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            ///看上面的官方注释: 如果当前 page 使用率超过一半, 则保留一个空 page 备用;
            if (page->lessThanHalfFull()) {
                ///当前 page 的使用率不足一半, 则只保留当前 page;
                page->child->kill();
            }
            else if (page->child->child) {
              ///当前 page 的使用率超过一半, 则只保留当前 page+下一个 chiildPage;
                page->child->child->kill();
            }
        }
    }

===>
#真正释放 autorelease 对象的逻辑
 void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        /*
          通过static inline void    pop(void *token)函数的逻辑我们可以得知, 正常情况下stop应该
          指向的是链表尾部 page 的 begin()位置, 也就是其 POOL_BOUNDARY 的地址;
          所以就从链表尾部开始遍历, 直到 next 指向 POOL_BOUNDARY 位置结束;
        */
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            /*
            注意这句官方注释: 每次从 hotPage 开始释放, 防止重复释放autoreleased对象;
            因为是栈结构, 后加入的先释放, 先加入的后释放, 从链表尾部开始释放;
            */
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            while (page->empty()) {
                ///如果当前 page 已经释放完毕, 通过 parent 指针找到前面的 page,并设置为 hotPage;
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            /*
            id obj = *--page->next;根据运算符向左结合性, 这行代码等价于
            先将 next指针执行--, 然后将这个位置的autorelease对象给 obj;
            因为 next 指针始终指向的是栈顶可以用的位置; 所以它的前面
            存放的是POOL_BOUNDARY或者autorelease对象;
            */
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();
            ///如果 obj 不是POOL_BOUNDARY指针, 那就是 autoreleased 对象, 执行objc_release操作;
            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }
        ///已经遍历到入参 page, 设置入参 page 为 hotPage;
        setHotPage(this);
#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            ASSERT(page->empty());
        }
#endif
    }
4._objc_autoreleasePoolPrint()函数打印

我们通过extern void _objc_autoreleasePoolPrint(void);方式可以调用私有函数来打印autoreleasepool的信息, 如下代码

#import "ViewController1.h"
#import "Model.h"

extern void _objc_autoreleasePoolPrint(void);
 
@interface ViewController1 ()
@end

@implementation ViewController1
///在工程设置为此文件为MRC
- (void)viewDidLoad {
    [super viewDidLoad];
    @autoreleasepool {
        Model *model = [[[Model alloc] init] autorelease];
        Model *model2 = [[[Model alloc] init] autorelease];
        @autoreleasepool {
            Model *model2 = [[[Model alloc] init] autorelease];
            _objc_autoreleasePoolPrint();
        }
     }
}
@end

可以获得如下打印结果



参考文章和下载链接
文中测试代码
objc4源码下载地址

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

推荐阅读更多精彩内容