OC內存管理--引用計數器

原文鏈接OC內存管理--引用計數器

更新于2020-02-14 更新Tagged Pointer的知識點

引用計數的存儲策略

  1. 有些對象如果支持使用Tagged Pointer,蘋果會直接將其指針值作為引用計數返回;
  2. 如果當前設備是64位環境并且使用Objective-C 2.0,那么“一些”對象會使用其isa指針的一部分空間來存儲它的引用計數;
  3. 否則Runtime會使用一張散列表來管理引用計數。

Tagged Pointer

從64bit開始,iOS引入了Tagged Pointer技術,用來優化NSNumber、NSDate、NSString等小對象的內存。

我們知道在沒有使用Tagged Pointer之前,NSNumberNSDate、NSString等對象需要動態分配內存,維護引用計數等。對象的指針存的是一個指向堆空間的地址值。以NSNumber為例,我們在創建NSObject對象的時候,至少需要16個字節的內存大?。ㄟ@個與OC內存對齊所使用的系數有關),另外還要用其他的內存空間來存NSNumber所對應的值,看上去這無疑在增加內存的開銷。

在使用了Tagged Pointer之后,它的指針不再是地址了,而是真正的值,準確的說是Tag+Data。所以,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已。所以,它的內存并不存儲在堆中,也不需要mallocfree。

在內存的讀取上使用了Tagged Pointer技術的對象也會比之前快很多。

下面的代碼用來反映在64位系統下Tagged Pointer的應用:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSNumber *number1 = @1;
        NSNumber *number2 = @2;
        NSNumber *number3 = @3;
        NSNumber *number4 = @4;
        
        NSNumber *numberLarger = @(MAXFLOAT);
        
        NSLog(@"number1 pointer is %p", number1);
        NSLog(@"number2 pointer is %p", number2);
        NSLog(@"number3 pointer is %p", number3);
        NSLog(@"number4 pointer is %p", number4);
        NSLog(@"numberLarger pointer is %p", numberLarger);
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

// 打印結果:
2018-09-25 15:26:05.788382+0800 NSObjectProject[68029:24580896] number1 pointer is 0x9c344c19d780bc93
2018-09-25 15:26:05.789257+0800 NSObjectProject[68029:24580896] number2 pointer is 0x9c344c19d780bca3
2018-09-25 15:26:05.789383+0800 NSObjectProject[68029:24580896] number3 pointer is 0x9c344c19d780bcb3
2018-09-25 15:26:05.789489+0800 NSObjectProject[68029:24580896] number4 pointer is 0x9c344c19d780bcc3
2018-09-25 15:26:05.789579+0800 NSObjectProject[68029:24580896] numberLarger pointer is 0x600001e60d80

上面的打印結果中,除了numberLarger這個變量之外,其他變量都使用Tagged Pointer技術。那如何知道對象是否是Tagged Pointer的對象呢?接著看一下源碼中的定義:

inline bool objc_object::isTaggedPointer() {
    return _objc_isTaggedPointer(this);
}

static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) {
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

_OBJC_TAG_MASK是一個依賴于環境的宏,其相關定義如下:

#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
    // 64-bit Mac - tag bit is LSB
#   define OBJC_MSB_TAGGED_POINTERS 0
#else
    // Everything else - tag bit is MSB
#   define OBJC_MSB_TAGGED_POINTERS 1
#endif

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL
#endif

如果是mac OS 64bit的環境下,_OBJC_TAG_MASK1UL,如果是iOS 64bit的環境下,_OBJC_TAG_MASK1UL<<631UL就是1)。

所以判斷是否Tagged Pointer的對象需要分環境考慮。如果是mac 64bit的環境下,則查看其指針最低有效位是否為1,如果是iOS 64bit的環境下,則使用1UL<<63,即1往左偏移63即0x1000000000000000,即查看最高有效位是否為1。

上述實驗是在iphone手機下運行的結果。我們將上面地址(16進制)的第一位轉成二進制,分別是1001、1001、10011001、0110,做$運算后,除了numberLarger對應的變量以外,其他的都是0x1000000000000000。另外我們在創建對象的時候,只要是從堆空間里創建的對象,它的最低有效位一定是0。這還是因為OC對象的內存對齊使用的系數為16,所以內存地址一定是16的倍數。但是使用了Tagged Pointer技術的對象,它的最低有效位就不一定是0了。從這里也證明了使用Tagged Pointer的對象的內存并不存儲在堆中,也不需要mallocfree。在后續的源碼中,也會看到但凡是Tagged Pointer的對象,很多都會直接return。

到這里我們得出幾個結論:

  1. 從64bit開始,iOS對NSNumber、NSDate、NSString等小對象會優先使用Tagged Pointer技術進行存儲,即指針存儲。當指針存儲不下內容的時候,才會使用堆空間即動態分配內存;
  2. 在mac OS 64bit下,通過看最低有效位是否為1判斷是否Tagged Pointer的對象,在iOS 64bit下,通過查看最高有效位是否為1來判斷;

我們知道,所有對象都有其對應的isa指針,那么引入Tagged Pointer會對isa指針產生什么影響。

isa指針

isa的本質——isa_t聯合體

在Objective-C語言中,類也是對象,且每個對象都包含一個isa指針,isa指針指向該對象所屬的類。
在arm64架構之前,isa就是一個普通的指針,存儲著Class或者Meta-Class對象的內存地址。從arm64開始,Runtime對isa進行了優化,變成了一個union(共用體或者聯合體)結構,還使用位域來存儲更多的信息。

在C語言中,結構體可以包含多個類型不同的成員,各個成員會占用不同的內存,互相之間沒有影響。結構體占用的內存大于等于所有成員占用的內存的總和(成員之間可能會存在縫隙)。而聯合體,是一種與結構體非常接近的數據結構,但是有所區別。共用體占用的內存等于最長的成員占用的內存,這就是說共用體的所有成員占用同一段內存,修改一個成員會影響其余所有成員。

objc_object這個結構體中定義了isa指針,這里我們只看arm64下的相關定義:

struct objc_object {
    isa_t isa;
}

// isa_t的定義
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        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)
    };
# endif

// SUPPORT_PACKED_ISA
#endif
};
  • nonpointer 該變量占用1 bit內存空間,可以有兩個值:0和1,分別代表不同的isa_t的類型:

  • has_assoc 該變量與對象的關聯引用有關。

  • has_cxx_dtor 表示該對象是否有析構函數,如果有析構函數,則需要做析構邏輯;如果沒有,則可以更快的釋放對象。

  • shiftcls 在開啟指針優化的情況下,用33bits存儲類指針的值。在initIsa()中有newisa.shiftcls = (uintptr_t)cls >> 3;這樣的代碼,就是將類指針存在isa中。

  • magic 用于調試器判斷當前對象是真的對象還是沒有初始化的空間

  • weakly_referenced 標志對象是否被指向或者曾經指向一個 ARC 的弱變量,沒有弱引用的對象可以更快釋放。

  • deallocating 標志對象是否正在釋放內存。

  • extra_rc 存儲的是引用計數

  • has_sidetable_rc 當引用計數器過大的時候,那么引用計數會存儲在一個叫SideTable的類的屬性中。

  • ISA_MAGIC_MASK 通過掩碼方式獲取magic值。

  • SA_MASK 通過掩碼方式獲取isa的類指針值。

  • RC_ONERC_HALF 用于引用計數的相關計算。

nonpointer的0表示開啟指針優化即普通的指針,訪問objc_object的isa會直接返回cls變量,cls變量會指向對象所屬的類的結構;1表示開啟指針優化,不能直接訪問objc_object的isa成員變量(此時的isa而是一個Tagged Pointer),isa中包含了類信息、對象的引用計數等信息。

extra_rc占了19位,可以存儲的最大引用計數應該是2^{19} - 1 + 1= 524288(為什么要這么寫是因為extra_rc保存的是值-1,而在獲取引用計數的時候會+1),當超過它就需要SideTables。SideTables內包含一個RefcountMap,用來保存引用計數,根據對象地址取出其引用計數,類型是size_t

這里有個問題,為什么既要使用一個extra_rc又要使用SideTables?

可能是因為歷史問題,以前cpu是32位的,isa中能存儲的引用計數就只有2^7=128。因此在arm64下,引用計數通常是存儲在isa中的。

更具體的會在retain操作的時候講到。

isa_t聯合體里面的宏

SUPPORT_PACKED_ISA

表示平臺是否支持在isa指針中插入除Class之外的信息。

  1. 如果支持就會將Class信息放入isa_t定義的struct內,并附上一些其他信息,例如上面的nonpointer等等;
  2. 如果不支持,那么不會使用isa_t內定義的struct,這時isa_t只使用cls(Class 指針)。

在iOS以及MacOSX設備上,SUPPORT_PACKED_ISA定義為1。

SUPPORT_INDEXED_ISA

SUPPORT_INDEXED_ISA表示isa_t中存放的Class信息是Class的地址。在initIsa()中有:

#if SUPPORT_INDEXED_ISA
newisa.indexcls = (uintptr_t)cls->classArrayIndex();

iOS設備上SUPPRT_INDEXED_ISA是0

isa類型有關的宏

SUPPORT_NONPOINTER_ISA

用于標記是否支持優化的isa指針,其定義:

#if !SUPPORT_INDEXED_ISA  &&  !SUPPORT_PACKED_ISA
#   define SUPPORT_NONPOINTER_ISA 0
#else
#   define SUPPORT_NONPOINTER_ISA 1
#endif

那如何判斷是否支持優化的isa指針?

  1. 已知iOS系統的SUPPORT_PACKED_ISA為1,SUPPORT_INDEXED_ISA為0,從上面的定義可以看出,iOS系統的SUPPORT_NONPOINTER_ISA為1;
  2. 在環境變量中設置OBJC_DISABLE_NONPOINTER_ISA

這里需要注意的是,即使是64位環境下,優化的isa指針并不是就一定會存儲引用計數,畢竟用19bit iOS 系統)保存引用計數不一定夠。另外這19位保存的是引用計數的值減一。

SideTable

在源碼中我們經常會看到SideTable這個結構體。它的定義:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    //省略其他代碼
};

從上面可知,SideTable中有三個成員變量:

  1. slock用于保證原子操作的自旋鎖;
  2. refcnts用于引用計數的hash表;
  3. weak_table用于weak引用的hash表。

這里我們主要看引用計數的哈希表。RefcountMap的定義:typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

可以看出SideTable用來保存引用計數具體是用DenseMap這個類(在llvm-DenseMap.h中)實現的。DenseMapDisguisedPtr<objc_object>keysize_tvalue,DisguisedPtr類是對objc_object *指針及其一些操作進行的封裝,其內容可以理解為對象的內存地址,值的類型為__darwin_size_t,在 darwin 內核一般等同于 unsigned long。其實這里保存的值也是等于引用計數減1。

引用計數的獲取

通過retainCount可以獲取到引用計數器,其定義:

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

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

    sidetable_lock();
    // 加鎖,用匯編指令ldxr來保證原子性
    isa_t bits = LoadExclusive(&isa.bits);
    // 釋放鎖,使用匯編指令clrex
    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();
}

//sidetable_retainCount()函數實現
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. Tagged Pointer的話,直接返回isa本身;
  2. Tagged Pointer,且開啟了指針優化,此時引用計數先從extra_rc中去?。ㄟ@里將取出來的值進行了+1操作,所以在存的時候需要進行-1操作),接著判斷是否有SideTable,如果有再加上存在SideTable中的計數;
  3. Tagged Pointer,沒有開啟了指針優化,使用sidetable_retainCount()函數返回。

手動操作對引用計數的影響

objc_retain()

#if __OBJC2__
__attribute__((aligned(16))) id objc_retain(id obj) {
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}
#else
id objc_retain(id obj) { return [obj retain]; }

首先判斷是否是Tagged Pointer的對象,是就返回對象本身,否則通過對象的retain()返回。

inline id objc_object::retain() {
    assert(!isTaggedPointer());
    // hasCustomRR方法檢查類(包括其父類)中是否含有默認的方法
    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}

首先判斷是否是Tagged Pointer,這個函數并不希望處理的對象是Tagged Pointer;接著通過hasCustomRR函數檢查類(包括其父類)中是否含有默認的方法,有則調用自定義的方法;如果沒有,調用rootRetain()函數。

ALWAYS_INLINE id objc_object::rootRetain() {
    return rootRetain(false, false);
}

//將源碼精簡后的邏輯
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;
    
    isa_t oldisa;
    isa_t newisa;

    // 加鎖,用匯編指令ldxr來保證原子性
    oldisa = LoadExclusive(&isa.bits);
    newisa = oldisa;
    
    if (newisa.nonpointer = 0) {
        // newisa.nonpointer = 0說明所有位數都是地址值
        // 釋放鎖,使用匯編指令clrex
        ClearExclusive(&isa.bits);
        
        // 由于所有位數都是地址值,直接使用SideTable來存儲引用計數
        return sidetable_retain();
    }
    
    // 存儲extra_rc++后的結果
    uintptr_t carry;
    // extra_rc++
    newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
    
    if (carry == 0) {
        // extra_rc++后溢出,進位到side table
        newisa.extra_rc = RC_HALF;
        newisa.has_sidetable_rc = true;
        sidetable_addExtraRC_nolock(RC_HALF);
    }
        
    // 將newisa寫入isa
    StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)
    return (id)this;
}

從上面的可以看到:

  • Tagged Pointer直接返回對象本身;
  • newisa.nonpointer == 0沒有開啟指針優化,直接使用SideTable來存儲引用計數;
  • 開啟指針優化,使用isa的extra_rc保存引用計數,當超出的時候,使用SideTable來存儲額外的引用計數。

objc_release()

#if __OBJC2__
__attribute__((aligned(16)))
void 
objc_release(id obj) {
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}
#else
void objc_release(id obj) { [obj release]; }
#endif

//release()源碼
inline void
objc_object::release()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        rootRelease();
        return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}

這邊的邏輯和objc_retain()的邏輯一致,所以直接看rootRelease()函數,與上面一樣,下面的代碼也是經過精簡的。

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
    if (isTaggedPointer()) return false;
    
    isa_t oldisa;
    isa_t newisa;
    
retry:
    oldisa = LoadExclusive(&isa.bits);
    newisa = oldisa;
    if (newisa.nonpointer == 0) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return sidetable_release(performDealloc);
    }
    
    uintptr_t carry;
    // extra_rc--
    newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
    if (carry == 0) {
        // 需要從SideTable借位,或者引用計數為0
        goto underflow;
    }
    
    // 存儲引用計數到isa
    StoreReleaseExclusive(&isa.bits,
                          oldisa.bits, newisa.bits)
    return false;
    
underflow:
    // 從SideTable借位
    // 或引用計數為0,調用delloc
    
    // 此處省略N多代碼
    // 總結一下:修改Side Table與extra_rc,
    
    // 引用計數減為0時,調用dealloc
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}

從上面可以看到:

  1. 判斷是否是Tagged Pointer的對象,是就直接返回;
  2. 沒有開啟指針優化,使用SideTable存儲的引用計數-1;
  3. 開啟指針優化,使用isa的extra_rc保存的引用計數-1,當carry==0表示需要從SideTable保存的引用計數也用完了或者說引用計數為0,所以執行最后一步;
  4. 最后調用dealloc,所以這也回答了之前的《OC內存管理--對象的生成與銷毀》中dealloc什么時候被調用這個問題,在rootRelease(bool performDealloc, bool handleUnderflow)函數中如果判斷出引用計數為0了,就要調用dealloc函數了。

總結

  1. 引用計數存在什么地方?

    • Tagged Pointer不需要引用計數,蘋果會直接將對象的指針值作為引用計數返回;
    • 開啟了指針優化(nonpointer == 1)的對象其引用計數優先存在isaextra_rc中,大于524288便存在SideTableRefcountMap或者說是DenseMap中;
    • 沒有開啟指針優化的對象直接存在SideTableRefcountMap或者說是DenseMap中。
  2. retain/release的實質

    • Tagged Pointer不參與retain/release
    • 找到引用計數存儲區域,然后+1/-1,并根據是否開啟指針優化,處理進位/借位的情況;
    • 當引用計數減為0時,調用dealloc函數。
  3. isa是什么

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
    
    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    
    • 首先要知道,isa指針已經不一定是類指針了,所以需要用ISA()獲取類指針;
    • Tagged Pointer的對象沒有isa指針,有的是isa_t的結構體;
    • 其他對象的isa指針還是類指針。
  4. 對象的值是什么

    • 如果是Tagged Pointer,對象的值就是指針;
    • 如果非Tagged Pointer, 對象的值是指針指向的內存區域中的值。

補充: 一道多線程安全的題目

以下代碼運行結果

@property (nonatomic, strong) NSString *target;
//....

dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
    dispatch_async(queue, ^{
        self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i];
    });
}

答案:大概率地發生Crash。

Crash的原因:過度釋放。

這道題看著雖然是多線程范圍的,但是解題的最重要思路確是在引用計數上,更準確的來說是看對強引用的理解程度。關鍵知識點如下:

  1. 全局隊列和自定義并行隊列在異步執行的時候會根據任務系統決定開辟線程個數;
  2. target使用strong進行了修飾,Block是會截獲對象的修飾符的;
  3. 即使使用_target效果也是一樣,因為默認使用strong修飾符隱式修飾;
  4. strong的源代碼如下:
objc_storeStrong(id *location, id obj) {
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

假設這個并發隊列創建了兩個線程A和B,由于是異步的,可以同時執行。因此會出現這么一個場景,在線程A中,代碼執行到了objc_retain(obj),但是在線程B中可能執行到了objc_release(prev),此時prev已經被釋放了。那么當A在執行到objc_release(prev)就會過度釋放,從而導致程序crash。

解決方法:

  1. 加個互斥鎖
  2. 使用串行隊列,使用串行隊列的話,其實內部是靠DISPATCH_OBJ_BARRIER_BIT設置阻塞標志位
  3. 使用weak
  4. Tagged Pointer,如果說上面的self.target指向的是一個Tagged Pointer技術的NSString,那程序就沒有問題。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容