Objective-C引用計數(shù)原理

在 Objective-C 2.0 中,我們無需手動進行內(nèi)存管理趾徽,因為ARC會自動幫我們在編譯的時候惨远,在合適的地方對對象進行retainrelease操作。
本文將結(jié)合runtime 750版本源碼 探究 ARC 環(huán)境下引用計數(shù)的實現(xiàn)原理北秽。

如何存儲引用計數(shù)

從 5S 開始,iPhone 都采用了64位架構(gòu)的處理器贺氓,為了節(jié)省內(nèi)存和提高執(zhí)行效率,蘋果提出了Tagged Pointer的概念蔑水,專門用來存儲小的對象,例如 NSNumber 和 NSDate肤粱。這一類變量本身的值需要占用的內(nèi)存大小常常不需要8字節(jié),拿整數(shù)來說领曼,4個字節(jié)所能表示的有符號整數(shù)可以達到20多億(2^31=2147483648蛮穿,另外 1 位作為符號位),基本可以處理大多數(shù)情況践磅。所以我們將一個對象的指針(64位下8字節(jié))拆成兩部分,一部分用來存儲數(shù)據(jù)府适,另一部分作為特殊標記,表示這是一個 Tagged Pointer, 不指向任何一個地址逻淌。也就是當某些類使用 Tagged Pointer 來存儲數(shù)據(jù)后,它就不是一個對象了卡儒,因為它并沒有指向任何地址,變成了一個披著對象皮的普通變量而已骨望,而對于這一類的‘對象’欣舵,它的內(nèi)存是分配在中,由系統(tǒng)分配以及釋放邻遏,所以它的引用計數(shù)也沒有意義了虐骑,當然你仍然可以使用CFGetRetainCount方法去獲取它的引用計數(shù)赎线,返回的是它的指針地址糊饱。

image

而在某些平臺中(比如arm64),isa 實例的一部分空間也會被用來存儲引用計數(shù)另锋,當引用計數(shù)超過一定值之后,runtime 會使用一張散列表(哈希表)來管理其引用計數(shù)夭坪;如果不使用 isa 存儲引用計數(shù)則會直接存儲到散列表中。

isa 指針

用64位(8字節(jié))來存儲一個內(nèi)存地址顯然是種浪費戏仓,于是可以將一部分的空間用來存儲引用計數(shù)。當 isa 指針第一位為1時即表示使用優(yōu)化的 isa 指針赏殃,這里列出64位環(huán)境下的 isa 結(jié)構(gòu):

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_NONPOINTER_ISA

# if __arm64__
#   define ISA_MASK        0x00000001fffffff8ULL
#   define ISA_MAGIC_MASK  0x000003fe00000001ULL
#   define ISA_MAGIC_VALUE 0x000001a400000001ULL
    struct {
        uintptr_t indexed           : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
        uintptr_t magic             : 9;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
    
// SUPPORT_NONPOINTER_ISA
#endif

};

SUPPORT_NONPOINTER_ISA表示是否支持在 isa 指針內(nèi)添加額外的信息间涵,例如引用計數(shù),析構(gòu)狀態(tài)勾哩,被__weak變量引用的情況等。目前僅支持 arm64架構(gòu)的設(shè)備支持迅矛。

變量名 含義
indexed 0 表示普通的 isa 指針,1 表示可以存儲引用計數(shù)
has_assoc 表示該對象是否包含 associated object(關(guān)聯(lián)對象)
has_cxx_dtor 表示該對象是否有 C++ 的析構(gòu)函數(shù)
shiftcls 類的指針
magic 固定值為 0xd2诬乞,用于在調(diào)試時分辨對象是否未完成初始化
weakly_referenced 表示該對象是否有過 weak 對象钠导,如果沒有,則析構(gòu)時更快
deallocating 表示該對象是否正在析構(gòu)
has_sidetable_rc 表示該對象的引用計數(shù)值是否過大無法存儲在 isa 指針
extra_rc 存儲引用計數(shù)值減一后的結(jié)果

在64位環(huán)境下牡属,isa 會存儲引用計數(shù),當 has_sidetable_rc 的值為1時逮栅,那么溢出的引用計數(shù)將會存儲在一張全局散列表中窗宇,也就是引用計數(shù) = isa保存的引用計數(shù) + 哈希表保存的引用計數(shù) + 1特纤。后面會詳細講到。

哈希表 DenseMap

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

template<typename KeyT, typename ValueT,
         bool ZeroValuesArePurgeable = false, 
         typename KeyInfoT = DenseMapInfo<KeyT> >
class DenseMap
    : public DenseMapBase<DenseMap<KeyT, ValueT, ZeroValuesArePurgeable, KeyInfoT>,
                          KeyT, ValueT, KeyInfoT, ZeroValuesArePurgeable> 
{
    // ...省略
}
                          

runtime 使用 DenseMap 哈希表(也叫散列表粪躬,類似NSDictionary)的別名RefcountMap來存儲引用計數(shù)。DenseMap 繼承于 DenseMapBase 這個 C++ 類镰官,通過觀察 DenseMapBase 的內(nèi)部實現(xiàn)我們可以發(fā)現(xiàn)以下幾點:

  • 鍵 KeyT 的類型為DisguisedPtr<objc_object>吗货,這個類是對objc_object *指針及其一些操作進行的封裝,目的是不受內(nèi)存泄漏工具leaks的檢測
  • 值 ValueT 的類型為 size_t, size_t在64位環(huán)境下等同于 unsigned long宙搬。保存的值等于引用計數(shù)減一
  • 模板的 KeyInfoT 類型為 DenseMapInfo<KeyT>,在這里等同于DenseMapInfo<DisguisedPtr<objc_object>>害淤。DenseMapInfo 封裝了比較重要的方法,用于在哈希表中查找 key 映射的內(nèi)容
template<typename T>
struct DenseMapInfo<T*> {
  static inline T* getEmptyKey() {
    uintptr_t Val = static_cast<uintptr_t>(-1);
    return reinterpret_cast<T*>(Val);
  }
  static inline T* getTombstoneKey() {
    uintptr_t Val = static_cast<uintptr_t>(-2);
    return reinterpret_cast<T*>(Val);
  }
  static unsigned getHashValue(const T *PtrVal) {
      return ptr_hash((uintptr_t)PtrVal);
  }
  static bool isEqual(const T *LHS, const T *RHS) { return LHS == RHS; }
};

指針哈希算法實現(xiàn):

#if __LP64__
static inline uint32_t ptr_hash(uint64_t key)
{
    key ^= key >> 4;
    key *= 0x8a970be7488fda55;
    key ^= __builtin_bswap64(key);
    return (uint32_t)key;
}
#endif

雖然不完美镶奉,但是速度很快(注釋說的。哨苛。。)


簡單來講建峭,DenseMap 通過對象的指針地址來映射其引用計數(shù)

SideTable

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
    // ...省略
}

介紹完存儲引用計數(shù)的哈希表决摧,那么這個哈希表是存儲在哪里的呢?
答案是保存在一個叫做SideTable的結(jié)構(gòu)體中掌桩,通過觀察它的結(jié)構(gòu)組成,我們可以可以看到有三個成員變量slock, refcntsweak_table波岛。

  • slock是一個自旋鎖,保證線程安全
  • refcnts的類型是 RefcountMap则拷,也就是上一節(jié)提到過的 DenseMap 類型的別名曹鸠。用來保存引用計數(shù)
  • weak_table用來保存__weak修飾的指針斥铺。當一個對象 delloc 時,通過這個表將這些指向要釋放對象的用__weak修飾的指針置為nil仅父,避免野指針的情況出現(xiàn)浑吟。

StripedMap

知道引用計數(shù)的哈希表是保存在SideTable中,那么SideTable實例保存在哪里呢组力?
答案是在一個全局的StripedMap<SideTable *>類型的靜態(tài)變量SideTableBuf

alignas(StripedMap<SideTable>) static uint8_t 
    SideTableBuf[sizeof(StripedMap<SideTable>)];

static void SideTableInit() {
    new (SideTableBuf) StripedMap<SideTable>();
}
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

之所以在初始化時將 SideTableBuf 定義成 uint8_t 是因為方便計算內(nèi)存大小,在SideTables()方法中我們可以看到SideTableBuf會被強制轉(zhuǎn)換成StripedMap<SideTable>*類型燎字。實際上 SideTableBuf 也是哈希表,根據(jù)指針地址映射到相應(yīng)的SideTable類型的變量笼蛛。下面是StripedMap這個類的定義:

template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif
    struct PaddedT {
        T value alignas(CacheLineSize);
    };
    PaddedT array[StripeCount];

  static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }
    public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    // ...省略
}

StripedMap中有一個PaddedT類型的數(shù)組array蛉鹿,在模擬器中容量為64,在真機中為8妖异。PaddedT結(jié)構(gòu)體大小為64個字節(jié),其成員變量 value 的類型實際是我們之前傳入 SideTable他膳。當系統(tǒng)調(diào)用SideTable& table = SideTables()[]時首先會執(zhí)行SideTables()得到SideTableBuf, 然后在StripedMap中執(zhí)行T& operator[] (const void *p)方法獲取相應(yīng)的SideTable棕孙。

T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    
static unsigned int indexForPointer(const void *p) {
    uintptr_t addr = reinterpret_cast<uintptr_t>(p);
    return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
} 

indexForPointer()函數(shù)中返回相應(yīng) SideTable 的index。(addr >> 4) ^ (addr >> 9)這一步我也不是很懂蟀俊,應(yīng)該是類似于產(chǎn)生一個隨機數(shù),后面的% StripeCount返回一個 [0, StripeCount)的數(shù)欧漱,也就是相應(yīng) SideTable 的index。所以一個 SideTable 應(yīng)該是對應(yīng)許多的對象的缚甩。


保存引用計數(shù)的哈希表保存在SideTable結(jié)構(gòu)體中谱净,而SideTable保存在一個全局的靜態(tài)變量StripedMap<SideTable> SideTableBuf中壕探。在真機下,SideTableBuf能夠儲存8個SideTable實例李请。StripedMap的方法indexForPointer()通過對象的指針計算出相應(yīng) SideTable 的 index。一個 SideTable 對應(yīng)多個對象

獲取引用計數(shù)

在 ARC 環(huán)境下我們可以使用方法CFGetRetainCount得到對象的引用計數(shù)导盅。在 runtime 中,通過調(diào)用objc_objectrootRetainCount()獲取引用計數(shù):

inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}
  1. isTaggedPointer在前面我們已經(jīng)分析過了如果是Tagged Pointer類型的對象時是怎么樣的白翻。此時對象在棧中分配绢片,由系統(tǒng)自動銷毀內(nèi)存(先進后出),所以此時對它求引用計數(shù)返回其地址。
    下面讓我們重點看一下sidetable_retainCount()這個方法
  2. 當 isa 的 nonpointer = 1 的情況我們開頭也分析過了底循,此時 isa 指針也用來存儲引用計數(shù),如果引用計數(shù)溢出則將溢出部分存儲在哈希表中
  3. 下面讓我們研究一下不使用isa優(yōu)化是怎么從哈希表中獲取引用計數(shù)的
uintptr_t
objc_object::sidetable_retainCount()
{
    SideTable& table = SideTables()[this];

    size_t refcnt_result = 1;
    
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        // this is valid for SIDE_TABLE_RC_PINNED too
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}
  1. 首先得到 SideTable 實例熙涤。
  2. 成員變量 refcnts 就是之前說的保存引用計數(shù)的哈希表,在哈希表中根據(jù)指針值查找引用計數(shù)灭袁。
  3. it->second >> SIDE_TABLE_RC_SHIFT 注意result從第三位才開始保存數(shù)據(jù),所以需要將數(shù)據(jù)向右移動2位才能取到引用計數(shù)倦炒。第1位用來保存該對象是否被用__weak修飾的變量引用,第2位用來表示該對象是否正在析構(gòu)
  4. 將右移后得到的數(shù)+1(refcnt_result)后返回逢唤。這也是為什么之前說哈希表保存的引用計數(shù)是實際值 -1 之后的值的原因。

Retain

在非 ARC 環(huán)境中可以使用retainrelease方法對引用計數(shù)進行加減操作鳖藕,在 ARC 環(huán)境中我們無需也無法使用這兩個方法操作引用計數(shù),但是你可以使用CFRetain()對對象進行 retain 操作著恩。最終會調(diào)用 objc_objectrootRetain方法

inline id 
objc_object::rootRetain()
{
    assert(!UseGC);

    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

類似于上一節(jié)中獲取引用計數(shù)的方法,當對象屬于Tagged Pointer時則返回該對象喉誊。所以我們接著看sidetable_retain()方法:

id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    
    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

首先得到 SideTable 實例。從實例中得到存儲引用技術(shù)的哈希表refcnts伍茄,在哈希表中根據(jù)對象的地址找到對應(yīng)的引用計數(shù)refcntStorage,判斷引用計數(shù)的值是否有溢出例获,如果沒有則對引用計數(shù) + 1,返回對象榨汤。
上一節(jié)我們講過 refcntStorage 中第三位才開始用來存儲引用計數(shù),所以讀數(shù)時需要先往右邊移動兩位件余,那為什么這里的代碼沒有呢?

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE            (1UL<<2)  // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED         (1UL<<(WORD_BITS-1))

#define SIDE_TABLE_RC_SHIFT 2

注意觀察SIDE_TABLE_RC_ONE的定義,是一個8字節(jié)的 unsigned long 類型旬渠,值為1,向左偏移了兩位告丢。refcntStorage += SIDE_TABLE_RC_ONE兩者相加的話則直接從第三位開始相加了,所以可以使用 SIDE_TABLE_RC_ONE 對引用計數(shù)進行 +1 和 -1 操作岖免。
同樣的,在上面的代碼中, SIDE_TABLE_RC_PINNED用來判斷引用計數(shù)值是否有溢出颅湘。

Release

release 最終會調(diào)用 objc_object的方法rootRelease()

inline bool 
objc_object::rootRelease()
{
    if (isTaggedPointer()) return false;
    return sidetable_release(true);
}

uintptr_t objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) {
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
        it->second -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
}

在這個方法你可以知道為什么哈希表中保存的引用計數(shù)是實際值 -1 之后的值。
it->second < SIDE_TABLE_DEALLOCATING用來判斷保存的引用計數(shù)值是否小于1闯参,如果小于1的話則對該值標記為正在析構(gòu):it->second |= SIDE_TABLE_DEALLOCATING;,并且在隨后對該對象發(fā)送 delloc 消息新博。
舉個例子,一個對象 sark脚草,實際的引用計數(shù)為1,在哈希表中保存的值為0,當這個對象進行release操作后嚼贡,sark 的引用計數(shù)變成了0,也就是需要進行銷毀操作了粤策。而到了該方法中,會判斷保存的引用計數(shù)的值是否小于1叮盘,如果是的話則進行 delloc 操作,并且將哈希表中存儲的值標記為正在析構(gòu)狀態(tài)柔吼。而 sark 原先保存著的引用計數(shù)值就是 =0,這樣設(shè)計避免了在哈希表存儲的引用計數(shù)出現(xiàn)負數(shù)的情況愈魏。

alloc,new培漏, copy 和 mutableCopy

copy 以及 mutableCopyNSCopyingNSMutableCopying協(xié)議上的方法,需要在各類上自己去實現(xiàn)copyWithZone:mutableCopyWithZone:方法牌柄。無論是深拷貝還是淺拷貝都會增加引用計數(shù)。

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}
+ (id)alloc {
    return _objc_rootAlloc(self);
}

[cls alloc]以及[cls allocWithZone:nil]方法最終會調(diào)用callAlloc()方法珊佣,所以 alloc 和 new 這兩個方法后面都會調(diào)用callAlloc()這個方法,因為 Objective-C 2.0 忽視垃圾回收和 NSZone,那么后續(xù)的調(diào)用順序依次是為:

callAlloc()
class_createInstance()
_class_createInstanceFromZone
calloc()

calloc()函數(shù)相比于malloc()函數(shù)的優(yōu)點是它將分配的內(nèi)存區(qū)域初始化為0咒锻,相當于malloc()后再用memset()方法初始化一遍守屉。

單例

其實這一節(jié)是對上一節(jié)內(nèi)容的補充。
記得我剛出來工作的時候胸梆,單例是這樣寫的:

@implementation Son
+ (instancetype)shareManager
{
    static Son *son;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        son = [super allocWithZone:nil];
    });
    return son;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    return [self shareManager];
}
@end

當時組長問我為什么要這樣子寫(因為跟他們寫的方式不一樣),我也答不上來碰镜,因為這種代碼都是直接google的。但是看了callAlloc()實現(xiàn)之后我明白為什么了绪颖。
在上一節(jié)我們已經(jīng)知道了 alloc 和 new 都會接著調(diào)用callAlloc()

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

如果類重載了allocWithZone方法甜奄,那么cls->ISA()->hasCustomAWZ()將會返回YES窃款,也就是說當我們用alloc或者new創(chuàng)建實例的時候课兄,就不會走系統(tǒng)的方法烟阐,而會走重載的allocWithZone方法了。我們在重載allocWithZone方法時返回[self shareManager](注意此時的self代表Son類), 因為shareManager方法返回的是一個靜態(tài)變量蜒茄。

還有一個需要注意的點就是在shareManager中,我們使用son = [super allocWithZone:nil];初始化實例檀葛,為什么不使用son = [[super alloc] init];來初始化呢?
代碼中的[super alloc];在編譯后會變成objc_msgSendSuper(objc_super super, @selector(alloc))(大致意思是這樣)屿聋。其中objc_super是一個結(jié)構(gòu)體,只有兩個成員變量id receiverClass class润讥,receiver 仍是 self(Son類), class 為 Father類伙判。當我們想通過[super alloc]創(chuàng)建實例的時候,會從 Father類中查找 +alloc 方法宴抚,如果沒有實現(xiàn)則在 NSObject 中查找 +alloc 方法甫煞。而方法里面的參數(shù) self 仍舊為 Son 類而不是 Father 類,所以還是會去調(diào)用重載的allocWithZone方法抚吠,導致死循環(huán)。

引用

Objective-C 引用計數(shù)原理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末喊式,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子萧朝,更是在濱河造成了極大的恐慌,老刑警劉巖检柬,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異里逆,居然都是意外死亡,警方通過查閱死者的電腦和手機原押,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诸衔,“玉大人,你說我怎么就攤上這事署隘。” “怎么了磁餐?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長诊霹。 經(jīng)常有香客問我,道長脾还,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任鄙漏,我火速辦了婚禮,結(jié)果婚禮上怔蚌,老公的妹妹穿的比我還像新娘。我一直安慰自己桦踊,他們只是感情好椅野,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布竟闪。 她就那樣靜靜地躺著,像睡著了一般炼蛤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鲸湃,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音暗挑,去河邊找鬼笋除。 笑死炸裆,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的烹看。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼惯殊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了土思?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤己儒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后闪湾,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡途样,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了何暇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嫁审。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡赖晶,死狀恐怖辐烂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情纠修,我是刑警寧澤胳嘲,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布扣草,位于F島的核電站颜屠,受9級特大地震影響鹰祸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蛙婴,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望街图。 院中可真熱鬧,春花似錦餐济、人聲如沸耘擂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽滚朵。三九已至,卻和暖如春辕近,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背移宅。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留漏峰,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓浅乔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親靖苇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353