OC对象的引用计数存放在哪里?weak和autorelease是怎么实现的?

我们都知道OC是通过引用计数来管理对象的生命周期的.一个新创建的OC对象的默认引用计数是1,调用retain会让对象的引用计数+1,调用release会让对象的引用计数-1.当引用对象为0时,OC对象就会销毁并释放其占用的内存空间.那么这个引用计数是存放在哪里的呢?
arm64 (5S) 架构开始,引用计数就直接存储在对象的isa指针中:

isa中存储引用计数

SideTable的结构如下:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;//引用计数
    weak_table_t weak_table;
}

其中有个散列表RefcountMap就存储着引用计数.RefcountMap中以当前对象的地址作为key,引用计数作为value.
下面我们就从runtime源码中验证一下:
步骤:NSObject.mm中搜索- (NSUInteger)retainCount:

- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}

进入rootRetainCount:

objc_object::rootRetainCount()
{
    //如果是 TaggedPoint 类型就直接返回
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    
    //判断指针是否优化过: 0:普通指针 ; 1:优化过指针
    if (bits.nonpointer) {
        
        //取出 extra_rc 的值然后 加1, extra_rc存储的值是引用计数减1
        uintptr_t rc = 1 + bits.extra_rc;
        
        //判断引用计数是否过大,无法存储在 isa 中
        // has_sidetable_rc 如果为1,那么引用计数就存储在 SideTable 中.
        if (bits.has_sidetable_rc) {
            
            //取出 SideTable 中的引用计数
            rc += sidetable_getExtraRC_nolock();
            
        }
        sidetable_unlock();
        return rc;
    }

如果has_sidetable_rc为1就说明引用引用计数存储在SideTables中,我们进入sidetable_getExtraRC_nolock看看是如何从SideTables中获取引用计数的:

objc_object::sidetable_getExtraRC_nolock()
{
    assert(isa.nonpointer);
    
    //把自身 this 当做 key 取出 table
    SideTable& table = SideTables()[this];
    
    // Map 说明这是一个 散列表
    //refcnts 是 RefcountMap 类型,也是一个 散列表
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;
    
    //取出 second 然后 位运算 得到 引用计数
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}

weak实现的原理

运行下面的代码,可以看到局部变量person再离开其作用域后就销毁了:


如果我们用一个__strong修饰的强指针指向这个person会发现,过了person的作用域后还不会销毁:
__strong

再用weak修饰的person2指向person看看结果:

weak

发现weak没有对person产生强引用.
再用__unsafe_unretained修饰的person3指向person:
__unsafe_unretained

会发现崩溃了,但是person对象依然在出了自身作用域后就销毁了,说明__unsafe_unretained同样没有对person产生强引用.
既然__unsafe_unretainedweak都没有对person产生强引用,那他们有什么区别呢?
区别就是:weak不会对对象产生强引用,并且当weak指向的对象释放后,weak会把指针置为nil.防止野指针错误.而__unsafe_unretained指向的对象销毁后,__unsafe_unretained并不会把指针置为nil.所以__unsafe_unretained是不安全的,容易出现野指针访问

那么weak内部是如何实现的把指针置为nil的呢?我们还是从runtime源码中探寻答案:
步骤:在NSObject.mm中搜索- (void)dealloc:

- (void)dealloc {
    _objc_rootDealloc(self);
}

进入_objc_rootDealloc:

_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}

进入rootDealloc:

objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  //如果是优化过指针
                 !isa.weakly_referenced  &&  //是否被弱引用指向过
                 !isa.has_assoc  &&  //是否设置过关联对象
                 !isa.has_cxx_dtor  &&  //是否有c++析构函数
                 !isa.has_sidetable_rc))//引用计数是否存储在SideTable中
    {
        assert(!sidetable_present());
        //直接释放对象,速度最快
        free(this);
    } 
    else {
        //会走另外的流程释放,速度会慢一些
        object_dispose((id)this);
    }
}

进入object_dispose:

object_dispose(id obj)
{
    if (!obj) return nil;

    //释放前:做一些事情
    objc_destructInstance(obj);
    //释放
    free(obj);

    return nil;
}

进入objc_destructInstance:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();//如果有c++析构函数
        bool assoc = obj->hasAssociatedObjects();//如果有关联对象

        // This order is important.
        if (cxx) object_cxxDestruct(obj);//清除成员变量
        if (assoc) _object_remove_assocations(obj);//移除关联对象
        obj->clearDeallocating();//将指向当前对象的弱指针置为 nil
    }

    return obj;
}

进入clearDeallocating看看它内部是如何把对象置为nil的:

objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        //普通isa指针
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        //优化过isa指针
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

进入优化过的isa指针处理方法clearDeallocating_slow:

objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));
    
    //传入 this ,取出 SideTable 类型的 table
    SideTable& table = SideTables()[this];
    /**
       struct SideTable {
       spinlock_t slock;
       RefcountMap refcnts;//散列表,以当前对象地址作为key,retainCount作为value
       weak_table_t weak_table;//散列表,以当前对象地址作为key,weak修饰的指向此对象的指针作为value
     }
     */
    table.lock();
    if (isa.weakly_referenced) {
        //清除 weak 指针
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    /**
     清除完weak指针后,也会把引用计数表清除
     因为当前对象要销毁了
     */
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

进入weak_clear_no_lock看看是如何清除的:

weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;

    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
}

weak_entry_for_referent(weak_table, referent)把弱引用表 和 当前对象地址值传进去:

static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    assert(referent);

    weak_entry_t *weak_entries = weak_table->weak_entries;

    if (!weak_entries) return nil;

    //利用当前对象的地址值 & 一个值 得到一个索引 begin
    size_t begin = hash_pointer(referent) & weak_table->mask;
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_table->weak_entries);
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    //根据索引值得到对应的weak指针
    return &weak_table->weak_entries[index];
}

最后清除:

    //拿到需要清除的weak指针,清除
    weak_entry_remove(weak_table, entry);

static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
    // remove entry
   //释放
    if (entry->out_of_line()) free(entry->referrers);
   //置为nil
    bzero(entry, sizeof(*entry));

    weak_table->num_entries--;

    weak_compact_maybe(weak_table);
}

ARC帮我们做了什么?

  • ARC是 LLVM 和 Runtime 的结合
  • 利用LLVM自动生成 release 和 retain autorelease 代码
  • 像弱引用的实现就需要 Runtime 运行时处理

autorelease

调用了autorelease的对象不需要我们手动调用release,系统会在适当的时候自动调用release去释放对象.那么系统是怎么做到这点的呢?这就牵扯到了自动释放池@autoreleasepool.我们以一下代码为实例,并且转换为c++代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *person = [[[Person alloc]init]autorelease];
        
    }
    return 0;
}

c++代码:

{ __AtAutoreleasePool __autoreleasepool; 
     //person的初始化代码
    Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, 
SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)
((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")), 
sel_registerName("autorelease"));
    }

我们再把person的初始化代码去掉:

{ __AtAutoreleasePool __autoreleasepool; 
   Person *person = [[[Person alloc]init]autorelease];
    }

会发现autoreleasepool代码就被转成了上面这种样式.__AtAutoreleasePool其实就是结构体:

         struct __AtAutoreleasePool {
         //构造函数,生成结构体变量的时候调用
           __AtAutoreleasePool() {
                  atautoreleasepoolobj = objc_autoreleasePoolPush();
         
         }
         //析构函数,销毁结构体变量的时候调用
           ~__AtAutoreleasePool() {
                  objc_autoreleasePoolPop(atautoreleasepoolobj);
         }
           void * atautoreleasepoolobj;
         };

c++中的结构体和OC中的类很像,可以把c++的结构体看做是OC中的类,所以可以在结构体中声明函数.
也就是说执行__AtAutoreleasePool __autoreleasepool;代码的时候就会去调用__AtAutoreleasePool的构造函数:

//构造函数,生成结构体变量的时候调用
           __AtAutoreleasePool() {
                  atautoreleasepoolobj = objc_autoreleasePoolPush();
         
         }

一旦走完autoreleasepool的大括号就会调用__AtAutoreleasePool的析构函数:

//析构函数,销毁结构体变量的时候调用
           ~__AtAutoreleasePool() {
                  objc_autoreleasePoolPop(atautoreleasepoolobj);
         }
           void * atautoreleasepoolobj;
         };

所以最后实际上的效果就是这样:


         调用构造函数 返回 atautoreleasepoolobj
         atautoreleasepoolobj = objc_autoreleasePoolPush();
         
         Person *person = [[[Person alloc]init]autorelease];
         
         调用析构函数,传入 atautoreleasepoolobj
         objc_autoreleasePoolPop(atautoreleasepoolobj);

也就是autoreleasepool大括号的开始会调用objc_autoreleasePoolPush,大括号的结束会调用objc_autoreleasePoolPop.所以以后只要看到autoreleasepool就代表被objc_autoreleasePoolPushobjc_autoreleasePoolPop包围:

@autoreleasepool {
        atautoreleasepoolobj = objc_autoreleasePoolPush();

        Person *person = [[[Person alloc]init]autorelease];
        @autoreleasepool {
            atautoreleasepoolobj = objc_autoreleasePoolPush();

            objc_autoreleasePoolPop(atautoreleasepoolobj);
        }
        
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
       

所以我们只要搞清楚objc_autoreleasePoolPush()objc_autoreleasePoolPop ()这两个函数内部做了什么,就能搞清楚autoreleasepool的本质了.
runtime源码中搜索objc_autoreleasePoolPushobjc_autoreleasePoolPop函数会发现他们长这样:

objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

发现他们都用到了AutoreleasePoolPage这个类.调用了 autorelease 的对象,最终都是通过 AutoreleasePoolPage这个类来管理的所以我们重点研究一下这个类.
AutoreleasePoolPage类的主要成员如下:

class AutoreleasePoolPage 
{
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
{
  • 一旦调用objc_autoreleasePoolPush ()函数,就会创建一个AutoreleasePoolPage对象.这个对象占用4096个字节的内存,除了用来存放它内部的成员变量以外,剩下的空间用来存放autorelease对象的地址.比如说person对象调用了autorelease,那么person对象的地址就存在了AutoreleasePoolPage对象中.
  • 所有的AutoreleasePoolPage对象通过双向链表的形式链接在一起.

比如说现在有一个AutoreleasePoolPage对象,内存地址在0x1000,那么它的结束内存地址就是0x2000.因为十六进制0x1000的十进制就是4096.person又调用了autorelease方法,那么person对象的地址就会被存放在AutoreleasePoolPage对象内部的0x1038这个位置,因为AutoreleasePoolPage对象内部有7个成员变量,每个成员变量占用8个字节,一共占用56个字节,用十六进制表示就是0x38,画图表示如下:


AutoreleasePoolPage对象内部有两个函数:

//返回开始存放 autorelease 对象的地址
    id * begin() {
        
        算法:自身的地址 + 自身所占空间大小:  0x1000 + 0x38
        return (id *) ((uint8_t *)this+sizeof(*this));
    }
//返回存放 autorelease 对象的结束地址
    id * end() {
        SIZE = 4096 , 自身地址 + 4096 个字节 : 0x1000 + 0x1000
        return (id *) ((uint8_t *)this+SIZE);
    }

如图所示:

bengin & end

如果一个AutoreleasePoolPage对象不够存储autorelease对象,那么就会再创建AutoreleasePoolPage对象.每个AutoreleasePoolPage对象的parent指向它的上一个AutoreleasePoolPage对象,如果是第一个AutoreleasePoolPage对象,它的parent就为nil;每个AutoreleasePoolPage对象的child指向它的下一个AutoreleasePoolPage对象,如果是最后一个AutoreleasePoolPage对象,child也为nil.关系如下:
关系图

现在我们知道了objc_autoreleasePoolPush()会将autorelease对象的地址存放到AutoreleasePoolPage对象中;objc_autoreleasePoolPop会将AutoreleasePoolPage中存放的autorelease对象释放.那么他们底层是怎么存储和释放的呢?

其实一旦调用objc_autoreleasePoolPush()函数,它的内部就会将POOL_BOUNDARY入栈,并且返回其内存地址,也就是0x1038.这里的入栈并不是内存中的堆区和栈区,而是数据结构的那种栈,先进后出;POOL_BOUNDARY其实就是nil,它的底层就是个宏:(define POOL_BOUNDARY nil);也就是说AutoreleasePoolPage存放的第一个对象并不是autorelease对象person,而是POOL_BOUNDARY.其次才是一个个autorelease对象,如图:

image.png

objc_autoreleasePoolPop (0x1038)传入的地址就是objc_autoreleasePoolPush ()返回的地址,也就是0x1038.objc_autoreleasePoolPop拿到这个地址后会从最后一个加入到AutoreleasePoolPageautorelease对象开始一个一个调用它们的release方法,直到POOL_BOUNDARY为止.BOUNDARY就是边界的意思,可见push函数和pop函数结合的非常巧妙.

  • AutoreleasePoolPagenext成员变量永远指向下一个能存放autorelease对象的地址.
  • 使用_objc_autoreleasePoolPrint私有函数查看autoreleasepool自动释放池的情况:
//使用 extern 关键字声明这个函数,即使这个函数在 Foundation 框架内部
//编译器会自动去查找这个方法并调用,这是c语言语法
extern void _objc_autoreleasePoolPrint(void);

我们使用一下:


person autorelease之前
person autorelease之后
多个autoreleasepool
超出一个 page 容量

先面我们将从源码上验证上诉结论:

push源码分析:

// 第一步:
    static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
           //如果没有 page 就创建一个 page,把 POOL_BOUNDARY 传进去
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            //有page
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

//第二步
    id *autoreleaseNewPage(id obj)
    {
        //调用hotPage,创建一个page
        AutoreleasePoolPage *page = hotPage();
        //判断 page 是不是满了
        if (page) return autoreleaseFullPage(obj, page);
        else return autoreleaseNoPage(obj);
    }

//第三步
    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);
        //往 page 中添加POOL_BOUNDARY
        return page->add(obj);
    }

autorelease源码分析:

//第一步:
objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    //将调用了 autorelease 的对象传进去
    return AutoreleasePoolPage::autorelease((id)this);
}

//第二步:
    static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        //进入 autoreleaseFast
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }

//第三步:
    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            //把调用了 autorelease 的对象 加入到 page
            return page->add(obj);
        } else if (page) {
            //如果有page,并且 page 满了
            return autoreleaseFullPage(obj, page);
        } else {
            //如果没有 page,就创建 page
            return autoreleaseNoPage(obj);
        }
    }

pop源码分析:

//第一步:
static inline void pop(void *token) {
        //token 就是 POOL_BOUNDARY
        page = pageForPointer(token);
        stop = (id *)token;
        //释放对象,直到遇到 POOL_BOUNDARY
        page->releaseUntil(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) {

            //如果 不是 POOL_BOUNDARY 就一直释放对象
            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

autorelease 对象什么时候释放?

我们知道调用了autorelease的对象会在适当的时候由系统去调用release释放,但是这个适当的时候是什么时候呢?思考一下以下代码的person对象什么时候会被释放:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[[Person alloc]init]autorelease];
    NSLog(@"viewDidLoad");
}

- (void)viewWillAppear:(BOOL)animated{
    NSLog(@"viewWillAppear");
}

- (void)viewDidAppear:(BOOL)animated{
    NSLog(@"viewDidAppear");
}

有人可能会觉得是走完viewDidLoad的大括号释放的,我们运行一下代码看看结果:

AutoRelease对象和runloop[18097:3714897] viewDidLoad
AutoRelease对象和runloop[18097:3714897] viewWillAppear
AutoRelease对象和runloop[18097:3714897] -[Person dealloc]
AutoRelease对象和runloop[18097:3714897] viewDidAppear

事实上是走完viewDidLoadviewWillAppear才释放的,为什么会这样呢?其实这和runloop有关系.
iOS在主线程中注册了两个Observer用来处理自动释放池相关的工作.Observer是用来监听runloop的状态的,比如进入runloop,runloop即将处理timer,runloop即将休眠等等.
我们打印一下NSLog(@"%@",[NSRunLoop currentRunLoop]);看看:

 // 监听状态为 1
 "<CFRunLoopObserver 0x6000021b0960 [0x7fff80615350]>{valid = Yes, activities =
 0x1, repeats = Yes, order = -2147483647, callout = 
_wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c), context = <CFArray
 0x600001ec6610 [0x7fff80615350]>{type = mutable-small, count = 1, values = (\n\t0 :
 <0x7fdbda801038>\n)}}",
 
 //监听状态为160  相当于 kCFRunLoopBeforeWaiting | kCFRunLoopExit
 "<CFRunLoopObserver 0x6000021b0a00 [0x7fff80615350]>{valid = Yes, activities =
 0xa0, repeats = Yes, order = 2147483647, callout =
 _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c), context = <CFArray
 0x600001ec6610 [0x7fff80615350]>{type = mutable-small, count = 1, values = (\n\t0 :
 <0x7fdbda801038>\n)}}"

会发现有两个Observer会调用_wrapRunLoopWithAutoreleasePoolHandler函数,并且这两个Observer分别监听了两个状态:

  1. activities = 0x1: 0x1 = 1
  2. activities = 0xa0 =xa0 = 160
    我们再来看看runloop中有哪些状态:
 typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
     kCFRunLoopEntry = (1UL << 0),  1
     kCFRunLoopBeforeTimers = (1UL << 1),  4
     kCFRunLoopBeforeSources = (1UL << 2),  8
     kCFRunLoopBeforeWaiting = (1UL << 5),  32
     kCFRunLoopAfterWaiting = (1UL << 6),  64
     kCFRunLoopExit = (1UL << 7),  128
     kCFRunLoopAllActivities = 0x0FFFFFFFU
 };

对照runloop的状态我们知道:

  1. activities = 0x1: 就是监听 kCFRunLoopEntry进入runloop.
  2. activities = 0xa0 就是监听 kCFRunLoopBeforeWaiting | kCFRunLoopExit即将休眠和退出.

那么这两个Observer做了什么事情呢?

  1. 第一个Observer监听kCFRunLoopEntry会调用objc_autoreleasePoolPush ().
  2. 第二个Observer:
    监听kCFRunLoopBeforeWaiting时会调用objc_autoreleasePoolPop (),然后再调用objc_autoreleasePoolPush ();
    监听kCFRunLoopExit事件会调用objc_autoreleasePoolPop ()

也就是说person对象什么时候调用release,是由runloop来控制的,它可能是在某次runloop循环中,runloop休眠之前调用了release;并且从上面的打印结果可以分析出来,viewDidLoadviewWillAppear是在同一次runloop循环中.

ARC环境下,方法中局部变量出了方法会立即释放吗?

我们可以试一下:

局部对象的释放时机

从打印结果可以看到,person对象在viewDidLoad之后,viewWillAppear之前就释放了.说明了ARC自动生成的并不是autorelease,而是在viewDidLoad大括号之前生成了person release.
所以我们应该分两种情况分析:

  1. 如果ARC自动生成的是autorelease,那么局部对象是在runloop某次循环即将休眠的时候释放.
  2. 如果ARC自动生成的是release,那么局部对象就会出了方法就释放.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容