Objective-C runtime機制(5.1)——iOS 內存管理

概述

當我們創建一個對象時:

SWHunter *hunter = [[SWHunter alloc] init];

上面這行代碼在上創建了hunter指針,并在上創建了一個SWHunter對象。目前,iOS并不支持在上創建對象。

iOS 內存分區

iOS的內存管理是基于虛擬內存的。虛擬內存能夠讓每一個進程都能夠在邏輯上“獨占”整個設備的內存。關于虛擬內存,可以參考這里。

iOS又將虛擬內存按照地址由低到高劃分為如下五個區:

這里寫圖片描述
  • 代碼區: 存放APP二進制代碼
  • 常量區:存放程序中定義的各種常量, 包括字符串常量,各種被const修飾的常量
  • 全局/靜態區: 全局變量,靜態變量就放在這里
  • 堆區:在程序運行時調用alloccopymutablecopynew會在堆上分配內存。堆內存需要程序員手動釋放,這在ARC中是通過引用計數的形式表現的。堆分配地址不連續,但整體是地址從低到高地址分配
  • 棧區:存放局部變量,當變量超出作用域時,內存會被系統自動釋放。棧上的地址連續分配,在內存地址由高向低增長

在程序運行時,代碼區,常量區以及全局靜態區的大小是固定的,會變化的只有棧和堆的大小。而棧的內存是有操作系統自動釋放的,我們平常說所的iOS內存引用計數,其實是就堆上的對象來說的。

下面,我們就來看一下,在runtime中,是如何通過引用計數來管理內存的。

tagged pointer

首先,來想這么一個問題,在平常的編程中,我們使用的NSNumber對象來表示數字,最大會有多大?幾萬?幾千萬?甚至上億?

我相信,對于絕大多數程序來說,用不到上億的數字。同樣,對于字符串類型,絕大多數時間,字符個數也在8個以內。

再想另一個方面,自2013年蘋果推出iphone5s之后,iOS的尋址空間擴大到了64位。我們可以用63位來表示一個數字(一位做符號位),這是一個什么樣的概念?231=2147483648,也達到了20多億,而263這個數字,用到的概率基本為零。比如NSNumber *num=@10000的話,在內存中則會留下很多無用的空位。這顯然浪費了內存空間。

蘋果當然也發現了這個問題,于是就引入了tagged pointertagged pointer是一種特殊的“指針”,其特殊在于,其實它存儲的并不是地址,而是<font color=orange>真實的數據和一些附加的信息</font>。

在引入tagged pointer 之前,iOS對象的內存結構如下所示(摘自唐巧博客):

[圖片上傳失敗...(image-a44d7c-1548215032013)]

顯然,本來4字節就可以表示的數值,現在卻用了8字節,明顯的內存浪費。而引入了tagged pointer 后, 其內存布局如下

這里寫圖片描述

可以看到,利用tagged pointer后,“指針”又存儲了對本身,也存儲了和對象相關的標記。這時的tagged pointer里面存儲的不是地址,而是一個數據集合。同時,其占用的內存空間也由16字節縮減為8字節。

我們可以在WWDC2013的《Session 404 Advanced in Objective-C》視頻中,看到蘋果對于Tagged Pointer特點的介紹:

  1. Tagged Pointer專門用來存儲小的對象,例如NSNumber, NSDate, NSString。
  2. Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已。所以,它的內存并不存儲在堆中,也不需要malloc和free。
  3. 在內存讀取上有著3倍的效率,創建時比以前快106倍。

運行如下代碼:

    NSMutableString *mutableStr = [NSMutableString string];
    NSString *immutable = nil;
    #define _OBJC_TAG_MASK (1UL<<63)
    char c = 'a';
    do {
        [mutableStr appendFormat:@"%c", c++];
        immutable = [mutableStr copy];
        NSLog(@"%p %@ %@", immutable, immutable, immutable.class);
    }while(((uintptr_t)immutable & _OBJC_TAG_MASK) == _OBJC_TAG_MASK);

輸出為:

這里寫圖片描述

我們看到,字符串由‘a’增長到‘abcdefghi’的過程中,其地址開頭都是0xa 而結尾也很有規律,是1到9遞增,正好對應著我們的字符串長度,同時,其輸出的class類型為NSTaggedPointerString。在字符串長度在9個以內時,iOS其實使用了tagged pointer做了優化的。

直到字符串長度大于9,字符串才真正成為了__NSCFString類型。

我們回頭分析一下上面的代碼。
首先,iOS需要一個標志位來判斷當前指針是真正的指針還是tagged pointer。這里有一個宏定義_OBJC_TAG_MASK (1UL<<63) ,它表明如果64位數據中,最高位是1的話,則表明當前是一個tagged pointer類型。

然后,既然使用了tagged pointer,那么就失去了iOS對象的數據結構,但是,系統還是需要有個標志位表明當前的tagged pointer 表示的是什么類型的對象。這個標志位,也是在最高4位來表示的。我們將0xa轉換為二進制,得到
1010,其中最高位1xxx表明這是一個tagged pointer,而剩下的3位010,表示了這是一個NSString類型。010轉換為十進制即為2。也就是說,<font color=red>標志位是2的tagger pointer表示這是一個NSString對象。</font>

在runtime源碼的objc-internal.h中,有關于標志位的定義如下:

{
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6, 
    OBJC_TAG_RESERVED_7        = 7, 

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};

最后,讓我們再嘗試分析一下NSString類型的tagged pointer是如何實現的。

我們前面已經知道,在總共64位數據中,高4位被用于標志tagged pointer以及對象類型標識。低1位用于記錄字符串字符個數,那么還剩下59位可以讓我們表示數據內容。

對于字符串格式,怎么來表示內容呢?自然的,我們想到了ASCII碼。對應ASCII碼,a用16進制ASCII碼表示為0x61,b為0x62, 依次類推。在字符串長度增加到8個之前,tagged pointer的內容如下。可以看到,從最低2位開始,分別為61,62,63... 這正對應了字符串中字符的ASCII碼。

這里寫圖片描述

直到字符串增加到7個之上,我們仍然可以分辨出tagged pointer中的標志位以及字符串長度,但是中間的內容部分,卻不符合ASCII的編碼規范了。

這里寫圖片描述

這是因為,iOS對字符串使用了壓縮算法,使得tagged pointer表示的字符串長度最大能夠達到9個。關于具體的壓縮算法,我們就不再討論了。由于蘋果內部會對實現邏輯作出修改,因此我們只要知道有tagged pointer 的概念就好了。有興趣的同學可以看采用Tagged Pointer的字符串,但其內容也有些過時了,和我們的實驗結果并不一致。

我們順便看一下NSNumber的tagged pointer實現:

    NSNumber *number1 = @(0x1);
    NSNumber *number2 = @(0x20);
    NSNumber *number3 = @(0x3F);
    NSNumber *numberFFFF = @(0xFFFFFFFFFFEFE);
    NSNumber *maxNum = @(MAXFLOAT);
    NSLog(@"number1 pointer is %p class is %@", number1, number1.class);
    NSLog(@"number2 pointer is %p class is %@", number2, number2.class);
    NSLog(@"number3 pointer is %p class is %@", number3, number3.class);
    NSLog(@"numberffff pointer is %p class is %@", numberFFFF, numberFFFF.class);
    NSLog(@"maxNum pointer is %p class is %@", maxNum, maxNum.class);
這里寫圖片描述

可以看到,對于MAXFLOAT,系統無法進行優化,輸出的是一個正常的NSNumber對象地址。而對于其他的number值,系統采用了tagged pointer,其‘地址’都是以0xb開頭,轉換為二進制就是1011, 首位1表示這是一個tagged pointer,而011轉換為十進制是3,參考前面tagged pointer的類型枚舉,這是一個NSNumber類型。接下來幾位,就是以16進制表示的NSNumber的值,而對于最后一位,應該是一個標志位,具體作用,筆者也不是很清楚。

isa

由于一個tagged pointer所指向的并不是一個真正的OC對象,它其實是沒有isa屬性的。

在runtime中,可以這樣獲取isa的內容:

#define _OBJC_TAG_SLOT_SHIFT 60
#define _OBJC_TAG_EXT_SLOT_MASK 0xff

inline Class 
objc_object::getIsa() 
{
    // 如果不是tagged pointer,則返回ISA()
    if (!isTaggedPointer()) return ISA();

    // 如果是tagged pointer,取出高4位的內容,查找對應的class
    uintptr_t ptr = (uintptr_t)this;
      
    uintptr_t slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    return objc_tag_classes[slot];
    
}

在runtime中,還有專用的方法用于判斷指針是tagged pointer還是普通指針:

#   define _OBJC_TAG_MASK (1UL<<63)
static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr) 
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

isa 指針(NONPOINTER_ISA)

對象的isa指針,用來表明對象所屬的類類型。
但是如果isa指針僅表示類型的話,對內存顯然也是一個極大的浪費。于是,就像tagged pointer一樣,對于isa指針,蘋果同樣進行了優化。isa指針表示的內容變得更為豐富,除了表明對象屬于哪個類之外,還附加了引用計數extra_rc,是否有被weak引用標志位weakly_referenced,是否有附加對象標志位has_assoc等信息。

這里,我們僅關注isa中和內存引用計數有關的extra_rc 以及相關內容。

首先,我們回顧一下isa指針是怎么在一個對象中存儲的。下面是runtime相關的源碼:

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

typedef struct objc_class *Class;

// ============ 注意!從這一行開始,其定義就和在XCode中objc.h看到的定義不一致,我們需要閱讀runtime的源碼,才能看到其真實的定義!下面是簡化版的定義:============
struct objc_class : objc_object {
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

 
struct objc_object {
private:
    isa_t isa;
}

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

    Class cls;
    uintptr_t bits;

# 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)
    };
}

結合下面的圖,我們可以更清楚的了解runtime中對象和類的結構定義,顯然,類也是一種對象,這就是類對象的含義。

這里寫圖片描述

從圖中可以看出,我們所謂的isa指針,最后實際上落腳于isa_t聯合類型聯合類型 是C語言中的一種類型,簡單來說,就是一種n選1的關系。比如isa_t 中包含有clsbitsstruct三個變量,它們的內存空間是重疊的。在實際使用時,僅能夠使用它們中的一種,你把它當做cls,就不能當bits訪問,你把它當bits,就不能用cls來訪問。

<font color=orange>聯合的作用在于,用更少的空間,表示了更多的可能的類型,雖然這些類型是不能夠共存的。</font>

將注意力集中在isa_t聯合上,我們該怎樣理解它呢?

首先它有兩個構造函數isa_t(), isa_t(uintptr_value), 這兩個定義很清晰,無需多言。

然后它有三個數據成員Class cls, uintptr_t bits, struct 。 其中uintptr_t被定義為typedef unsigned long uintptr_t,占據64位內存。

關于上面三個成員, uintptr_t bitsstruct 其實是一個成員,它們都占據64位內存空間,之前已經說過,聯合類型的成員內存空間是重疊的。在這里,由于uintptr_t bitsstruct 都是占據64位內存,因此它們的內存空間是完全重疊的。而你將這塊64位內存當做是uintptr_t bits 還是 struct,則完全是邏輯上的區分,在內存空間上,其實是一個東西。
<font color=blue>即uintptr_t bitsstruct 是一個東西的兩種表現形式。</font>

實際上在runtime中,任何對struct 的操作和獲取某些值,如extra_rc,實際上都是通過對uintptr_t bits 做位操作實現的。uintptr_t bitsstruct 的關系可以看做,uintptr_t bits 向外提供了操作struct 的接口,而struct 本身則說明了uintptr_t bits 中各個二進制位的定義。

理解了uintptr_t bitsstruct 關系后,則isa_t其實可以看做有兩個可能的取值,Class clsstruct。如下圖所示:

這里寫圖片描述

isa_t作為Class cls使用時,這符合了我們之前一貫的認知:isa是一個指向對象所屬Class類型的指針。然而,僅讓一個64位的指針表示一個類型,顯然不劃算。

因此,<font color=blue>絕大多數情況下,蘋果采用了優化的isa策略,即,isa_t類型并不等同而Class cls, 而是struct。</font>這種情況對于我們自己創建的類對象以及系統對象都是如此,稍后我們會對這一結論進行驗證。

先讓我們集中精力來看一下struct的結構 :

# 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)
    };

struct共占用64位,從低位到高位依次是nonpointerextra_rc。成員后面的表明了該成員占用幾個bit。成員的含義如下:

成員 含義
nonpointer 1bit 標志位。1(奇數)表示開啟了isa優化,0(偶數)表示沒有啟用isa優化。所以,我們可以通過判斷isa是否為奇數來判斷對象是否啟用了isa優化。
has_assoc 1bit 標志位。表明對象是否有關聯對象。沒有關聯對象的對象釋放的更快。
has_cxx_dtor 1bit 標志位。表明對象是否有C++或ARC析構函數。沒有析構函數的對象釋放的更快。
shiftcls 33bit 類指針的非零位。
magic 6bit 固定為0x1a,用于在調試時區分對象是否已經初始化。
weakly_referenced 1bit 標志位。用于表示該對象是否被別的對象弱引用。沒有被弱引用的對象釋放的更快。
deallocating 1bit 標志位。用于表示該對象是否正在被釋放。
has_sidetable_rc 1bit 標志位。用于標識是否當前的引用計數過大,無法在isa中存儲,而需要借用sidetable來存儲。(這種情況大多不會發生)
extra_rc 19bit 對象的引用計數減1。比如,一個object對象的引用計數為7,則此時extra_rc的值為6。

由上表可以看出,和對象引用計數相關的有兩個成員:extra_rchas_sidetable_rc。iOS用19位的extra_rc來記錄對象的引用次數,當extra_rc 不夠用時,還會借助sidetable來存儲計數值,這時,has_sidetable_rc會被標志為1。

我們可以算一下,對于19位的extra_rc ,其數值可以表示2^19 - 1 = 524287。 52萬多,相信絕大多數情況下,都夠用了。

現在,我們來真正的驗證一下,我們上述的結論。<font color=red>注意,做驗證試驗時,必須要使用真機,因為模擬器默認是不開啟isa優化的。</font>

要做驗證試驗,我們必須要得到isa_t的值。在蘋果提供的公共接口中,是無法獲取到它的。不過,通過對象指針,我們確實是可以獲取到isa_t 的值。

讓我們看一下當我們創建一個對象時,實際上是獲得到了什么。

NSObject *obj = [[NSObject alloc] init];

我們得到了obj這個對象,實質上obj是一個指向對象的指針, 即
obj == NSObject *

而在NSObject中,又有唯一的成員Class isa, 而Class實質上是objc_class *。這樣,我們可以用objc_class * 替換掉 NSObject,得到

obj == objc_class **

再看objc_class的定義:

struct objc_class : objc_object {
    。。。
}

objc_class 繼承自objc_object, 因此,在objc_class 內存布局的首地址肯定存放的是繼承自objc_object的內容。從內存布局的角度,我們可以將objc_class 替換為 objc_object 。得到:

obj == objc_object **

objc_object 的定義如下,僅含有一個成員isa_t :

struct objc_object {
private:
    isa_t isa;
}

因此,我們又可以將objc_object 替換為isa_t。得到:

obj == isa_t **

<font color = orange>好了,這里到了關鍵的地方</font>,從現在看,我們得到的obj應該是一個指向 isa_t * 的指針,即 obj是一個指針的指針,obj指向一個指針。 但是,obj真的是指向了一個指針嗎

我們再來看一下isa_t的定義,我們看標志為注意!!!的地方:

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;   // 注意!!! 標志位,表明isa_t *是否是一個真正的指針!!!
        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)
    };

也就是說,當開啟了isa_t優化,nonpointer 置位為1, 這時,isa_t *其實不是一個地址,而是一個實實在在有意義的值,也就是說,蘋果用isa_t * 所占用的64位空間,表示了一個有意義的值,而這64位值的定義,就符合我們上面struct的定義

這時,我們可以將isa_t *改寫為isa_t,這是因為isa_t *的64位并沒有指向任何地址,而是實際表示了isa_t的內容。

繼續上面的公式推導,得到結論:

obj == *isa_t

哈哈,有意思嗎?obj實際上是指向isa_t的指針。繞了這里大一圈,結論竟如此直白。

如果我們想得到isa_t的值,只需要做*obj操作即可,即

 NSLog(@"isa_t = %p", *obj);

之所以用%p輸出,是因為我們要isa_t*本身的值,而不是要取它指向的值。

得出了這個結論,我們就可以通過obj打印出isa_t中存儲的內容了(中間需要做幾次類型轉換,但是實質和上面是一樣的):

 NSLog(@"isa_t = %p", *(void **)(__bridge void*)obj);

我們的實驗代碼如下:

@interface MyObj : NSObject
@end
@implementation MyObj
@end

@interface ViewController ()
@property(nonatomic, strong) MyObj *obj1;
@property(nonatomic, strong) MyObj *obj2;
@property(nonatomic, weak) MyObj *weakRefObj;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    MyObj *obj = [[MyObj alloc] init];
    NSLog(@"1. obj isa_t = %p", *(void **)(__bridge void*)obj);
    _obj1 = obj;
    MyObj *tmpObj = obj;
    NSLog(@"2. obj isa_t = %p", *(void **)(__bridge void*)obj);
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"3. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
    _obj2 = _obj1;
    NSLog(@"4. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
    _weakRefObj = _obj1;
    NSLog(@"5. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
    NSObject *attachObj = [[NSObject alloc] init];
    objc_setAssociatedObject(_obj1, "attachKey", attachObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    NSLog(@"6. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
}
@end

其輸出為:


這里寫圖片描述

直觀的可以看到isa_t的內容都是奇數,說明開啟了isa優化。(nonpointer == 1

接下來我們一行行的分析代碼以及相應的isa_t內容變化:

首先在viewDidLoad方法中,我們創建了一個MyObj實例,并接著打印出isa_t的內容,這時候,MyObj的引用計數應該是1:

- (void)viewDidLoad {
    ...
    MyObj *obj = [[MyObj alloc] init];
    NSLog(@"1. obj isa_t = %p", *(void **)(__bridge void*)obj);
    ...
}

對應的輸出內容為0x1a1000a0ff9:

這里寫圖片描述

大家可以在圖中直觀的看到isa_t此時各位的內容,注意到extra_rc此時為0,因為引用計數等于extra_rc + 1,因此,MyObj對象的引用計數為1,和我們的預期一致。

接下來執行

    _obj1 = obj;
    MyObj *tmpObj = obj;
    NSLog(@"2. obj isa_t = %p", *(void **)(__bridge void*)obj);

由于_obj1MyObj對象是強引用,同時,tmpObj的賦值也默認是強引用,obj的引用計數加2,應該等于3

輸出為0x41a1000a0ff9

這里寫圖片描述

引用計數等于extra_rc + 1 = 2 + 1 = 3, 符合預期。

然后,程序執行到了viewDidAppear方法,并立刻輸出MyObj對象的引用計數。因為此時棧上變量objtmpObj已經釋放,因此引用計數應該減2,等于1

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"3. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
    ...
}

輸出為 0x1a1000a0ff9:

這里寫圖片描述

引用計數等于extra_rc + 1 = 0 + 1 = 1, 符合預期。

接下來我們又賦值了一個強引用_obj2, 引用計數加1,等于2。

    ...
     _obj2 = _obj1;
    NSLog(@"4. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
    ...

輸出為0x21a1000a0ff9 :

這里寫圖片描述

引用計數等于extra_rc + 1 = 1 + 1 = 2, 符合預期。

接下來,我們又將MyObj對象賦值給一個weak引用,此時,引用計數應該保持不變,但是weakly_referenced位應該置1

    ...
    _weakRefObj = _obj1;
    NSLog(@"5. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
    ...

輸出0x25a1000a0ff9:

這里寫圖片描述

可以看到引用計數仍是2,但是weakly_referenced位已經置位1,符合預期。

最后,我們向MyObj對象 添加了一個關聯對象,此時,isa_t的其他位應該保持不變,只有has_assoc標志位應該置位1

    ...
    NSObject *attachObj = [[NSObject alloc] init];
    objc_setAssociatedObject(_obj1, "attachKey", attachObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    NSLog(@"6. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
    ...

輸出0x25a1000a0ffb

這里寫圖片描述

可以看到,其他位保持不變,只有has_assoc被設置為1,符合預期。

OK,通過上面的分析,你現在應該很清楚rumtime里面isa究竟是怎么回事了吧?

PS: 筆者所實驗的環境為iPhone5s + iOS 10。

SideTable

其實在絕大多數情況下,僅用優化的isa_t來記錄對象的引用計數就足夠了。只有在19位的extra_rc盛放不了那么大的引用計數時,才會借助SideTable出馬。

SideTable是一個全局的引用計數表,它記錄了所有對象的引用計數。

為了弄清extra_rcsidetable的關系,我們首先看runtime添加對象引用計數時的簡化代碼。不過在看代碼之前,我們需要弄清楚slowpathfastpath是干啥的。

我們在runtime源碼中有時候,有時在if語句中會看到類似下面這些代碼:

if (fastpath(cls->canAllocFast())){
...
}

if (slowpath(!newisa.nonpointer)) {
...
}

其實將fastpathslowpath去掉是完全不影響任何功能的。之所以將fastpathslowpath 放到if語句中,是為了告訴編譯器,if中的條件是大概率(fastpath)還是小概率(slowpath)事件,從而讓編譯器對代碼進行優化。知道了這些,我們就可以來繼續看源碼了:

#       define RC_HALF  (1ULL<<18)
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    // 如果是tagged pointer,直接返回this,因為tagged pointer不用記錄引用次數
    if (isTaggedPointer()) return (id)this;
    // transcribeToSideTable用于表示extra_rc是否溢出,默認為false
    bool transcribeToSideTable = false;
    
    isa_t oldisa;
    isa_t newisa;
    
    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits); // 將isa_t提取出來
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {  // 如果沒有采用isa優化, 則返回sidetable記錄的內容, 此處slowpath表明這不是一個大概率事件
            return sidetable_retain();
        }
        // 如果對象正在析構,則直接返回nil
        if (slowpath(tryRetain && newisa.deallocating)) {
            return nil;
        }
        // 采用了isa優化,做extra_rc++,同時檢查是否extra_rc溢出,若溢出,則extra_rc減半,并將另一半轉存至sidetable
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        
        if (slowpath(carry)) { // 有carry值,表示extra_rc 溢出
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {  // 如果不處理溢出情況,則在這里會遞歸調用一次,再進來的時候,handleOverflow會被rootRetain_overflow設置為true,從而進入到下面的溢出處理流程
                return rootRetain_overflow(tryRetain);
            }
            
            // 進行溢出處理:邏輯很簡單,先在extra_rc中引用計數減半,同時把has_sidetable_rc設置為true,表明借用了sidetable。然后把另一半放到sidetable中
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))); // 將oldisa 替換為 newisa,并賦值給isa.bits(更新isa_t), 如果不成功,do while再試一遍
    
    //isa的extra_rc溢出,將一半的refer count值放到sidetable中
    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);
    }
    
    return (id)this;
}

添加對象引用計數的源碼邏輯還算清晰,重點看當extra_rc溢出后,runtime是怎么處理的。

在iOS中,extra_rc占有19位,也就是最大能夠表示2^19-1, 用二進制表示就是19個1。當extra_rc等于2^19時,溢出,此時的二進制位是一個1后面跟19個0, 即10000...00。將會溢出的值2^19除以2,相當于將10000...00向右移動一位。也就等于RC_HALF(1ULL<<18),即一個1后面跟18個0。

然后,調用

sidetable_addExtraRC_nolock(RC_HALF);

將另一半的引用計數RC_HALF放到sidetable中。

SideTable數據結構

在runtime中,通過SideTable來管理對象的引用計數以及weak引用。<font color=red>這里要注意,一張SideTable會管理多個對象,而并非一個。</font>
而這一個個的SideTable又構成了一個集合,叫SideTablesSideTables在系統中是全局唯一的。

SideTableSideTables的關系如下圖所示(這張圖會隨著分析的深入逐漸擴充):

這里寫圖片描述

SideTables的類型是是template<typename T> class StripedMap,StripedMap<SideTable> 。我們可以簡單的理解為一個64 * sizeof(SideTable) 的哈希線性數組。

每個對象可以通過StripedMap所對應的哈希算法,找到其對應的SideTableStripedMap 的哈希算法如下,其參數是對象的地址。

static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount; // 這里 %StripeCount 保證了所有的對象對應的SideTable均在這個64長度數組中。
    }

注意到這個SideTables哈希數組是全局的,因此,對于我們APP中所有的對象的引用計數,也就都存在于這64個SideTable中。

具體到每個SideTable, 其中有存儲了若干對象的引用計數。SideTable 的定義如下:

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

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }
};

SideTable包含三個成員:

  • spinlock_t slock :自旋鎖。防止多線程訪問SideTable沖突
  • RefcountMap refcnts:用于存儲對象引用計數的map
  • weak_table_t weak_table : 用于存儲對象弱引用的map

這里我們暫且不去管weak_table, 先看存儲對象引用計數的成員RefcountMap refcnts

RefcountMap類型實際是DenseMap,這是一個模板類

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

關于DenseMap的實際定義,有點復雜,暫時不想看:(

這里只需要將RefcountMap簡單的的理解為是一個mapkeyDisguisedPtr<objc_object>value是對象的引用計數。同時,這個map還有個加強版功能,當引用計數為0時,會自動將對象數據清除。

這也是

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

的含義,即模板類型分別對應:
key,DisguisedPtr<objc_object>類型。
value,size_t類型。
是否清除為vlaue==0的數據,true。

DisguisedPtr<objc_object>中的采樣方法是:

   static uintptr_t disguise(T* ptr) {
        return -(uintptr_t)ptr;
    }
    // 將T按照模板替換為objc_object,即是:
    static uintptr_t disguise(objc_object* ptr) {
        return -(uintptr_t)ptr;
    }

所以,對象引用計數map RefcountMap的key是:-(object *),就是對象地址取負。value就是該對象的引用計數。

我們來看一下OC是如何獲取對象引用計數的:

inline uintptr_t 
objc_object::rootRetainCount()
{
    //case 1: 如果是tagged pointer,則直接返回this,因為tagged pointer是不需要引用計數的
    if (isTaggedPointer()) return (uintptr_t)this;

    // 將objcet對應的sidetable上鎖
    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    // case 2: 如果采用了優化的isa指針
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc; // 先讀取isa.extra_rc
        if (bits.has_sidetable_rc) { // 如果extra_rc不夠大, 還需要讀取sidetable中的數據
            rc += sidetable_getExtraRC_nolock(); // 總引用計數= rc + sidetable count
        }
        sidetable_unlock();
        return rc;
    }
    // case 3:如果沒采用優化的isa指針,則直接返回sidetable中的值
    sidetable_unlock(); // 將objcet對應的sidetable解鎖,因為sidetable_retainCount()中會上鎖
    return sidetable_retainCount();
}

可以看到,runtime在獲取對象引用計數的時候,是考慮了三種情況:(1)tagged pointer, (2)優化的isa, (3)未優化的isa

我們來看一下(2)優化的isa 的情況下:
首先,會讀取extra_rc中的數據,因為extra_rc中存儲的是引用計數減一,所以這里要加回去。
如果extra_rc 不夠大的話,還需要讀取sidetable,調用sidetable_getExtraRC_nolock

#define SIDE_TABLE_RC_SHIFT 2

size_t 
objc_object::sidetable_getExtraRC_nolock()
{
    assert(isa.nonpointer);
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}

注意,這里在返回引用計數前,還做了個右移2位的位操作it->second >> SIDE_TABLE_RC_SHIFT。這是因為在sidetable中,引用計數的低2位不是用來記錄引用次數的,而是分別表示對象是否有弱引用計數,以及是否在deallocing,這估計是為了兼容未優化的isa而設計的:

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  // MSB-ward of weak bit

所以,在sidetable中做加引用加一操作時,需要在第3位上+1:

#define SIDE_TABLE_RC_ONE            (1UL<<2)  // MSB-ward of deallocating bit
refcntStorage += SIDE_TABLE_RC_ONE;

這里sidetable的引用計數值還有一個SIDE_TABLE_RC_PINNED 狀態,表明這個引用計數太大了,連sidetable都表示不出來:

#define SIDE_TABLE_RC_PINNED         (1UL<<(WORD_BITS-1))

OK,到此為止,我們就學習完了runtime中所有的引用計數實現方式。接下來我們還會繼續看和引用計數相關的兩個概念:弱引用和autorelease。

Weekly reference

再來回看一下sidetable 的定義如下:

struct SideTable {
    spinlock_t slock;           // 自旋鎖,防止多線程訪問沖突
    RefcountMap refcnts;        // 對象引用計數map
    weak_table_t weak_table;    // 對象弱引用map
}

spinlock_t slockRefcountMap refcnts的定義我們已經清楚,下面就來看一下weak_table_t weak_table,它記錄了所有弱引用對象的集合。

weak_table_t定義如下:

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries;        // hash數組,用來存儲弱引用對象的相關信息weak_entry_t
    size_t    num_entries;             // hash數組中的元素個數
    uintptr_t mask;                    // hash數組長度-1,會參與hash計算。(注意,這里是hash數組的長度,而不是元素個數。比如,數組長度可能是64,而元素個數僅存了2個)
    uintptr_t max_hash_displacement;   // 可能會發生的hash沖突的最大次數
};

weak_table_t 包含一個weak_entry_t類型的數組,可以通過hash算法找到對應object在數組中的index。這種結構,和sidetables類似,不同的是,weak_table_t是可以動態擴展的,而不是寫死的64個。

這里寫圖片描述

weak_entries實質上是一個hash數組,數組中存儲weak_entry_t類型的元素。weak_entry_t的定義如下:

typedef DisguisedPtr<objc_object *> weak_referrer_t;

#define PTR_MINUS_2 62
/**
 * The internal structure stored in the weak references table. 
 * It maintains and stores
 * a hash set of weak references pointing to an object.
 * If out_of_line_ness != REFERRERS_OUT_OF_LINE then the set
 * is instead a small inline array.
 */
#define WEAK_INLINE_COUNT 4

// out_of_line_ness field overlaps with the low two bits of inline_referrers[1].
// inline_referrers[1] is a DisguisedPtr of a pointer-aligned address.
// The low two bits of a pointer-aligned DisguisedPtr will always be 0b00
// (disguised nil or 0x80..00) or 0b11 (any other address).
// Therefore out_of_line_ness == 0b10 is used to mark the out-of-line state.
#define REFERRERS_OUT_OF_LINE 2

struct weak_entry_t {
    DisguisedPtr<objc_object> referent; // 被弱引用的對象
    
    // 引用該對象的對象列表,聯合。 引用個數小于4,用inline_referrers數組。 用個數大于4,用動態數組weak_referrer_t *referrers
    union {
        struct {
            weak_referrer_t *referrers;                      // 弱引用該對象的對象列表的動態數組
            uintptr_t        out_of_line_ness : 2;           // 是否使用動態數組標記位
            uintptr_t        num_refs : PTR_MINUS_2;         // 動態數組中元素的個數
            uintptr_t        mask;                           // 用于hash確定動態數組index,值實際上是動態數組空間長度-1(它和num_refs不一樣,這里是記錄的是數組中位置的個數,而不是數組中實際存儲的元素個數)。
            uintptr_t        max_hash_displacement;          // 最大的hash沖突次數(說明了最多做max_hash_displacement次hash沖突,肯定會找到對應的數據)
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };

    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }

    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }

    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent)
    {
        inline_referrers[0] = newReferrer;
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
};

根據注釋,DisguisedPtr方法返回的hash值得最低2個字節應該是0b000b11,因此可以用out_of_line_ness == 0b10來表明當前是否在使用數組或動態數組來保存引用該對象的列表。

這樣,sidetable中的weak_table_t weak_table成員的結構如下所示:

這里寫圖片描述

回頭再來看一下,會發現在weak talbe中存在兩個hash 表。

一個是weak_table_t 自身。它可以通過對象地址做hash(hash_pointer(objc_object *) & weak_table->mask),直接找到weak_entries中該對象對應的weak_entry_t

另一個是weak_entry_t中的weak_referrer_t *referrers。它可以通過弱引用該對象的對象指針的指針做hash(w_hash_pointer(objc_object **) & (entry->mask)),直接找到對象指針的指針在referrers中對應的weak_referrer_t *

雖然weak_table_treferrers 是表示意義不同的hash表,但他們的實現以是一樣的,可以看做是同一種hash表。而且還設計的很有技巧。下面,我們就來詳細學習一下hash 表示怎么實現的。

weak table的實現細節

由于weak_entries和referrers中的實現類似,這里我們就以weak_table_t為例,來分析hash表的實現。

weak_table_t定義如下:

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries;        // hash數組,用來存儲弱引用對象的相關信息weak_entry_t
    size_t    num_entries;             // hash數組中的元素個數
    uintptr_t mask;                    // hash數組長度-1,用于和hash值做位與計算,來確定數組下標。(注意,這里是hash數組的長度,而不是元素個數。比如,數組長度可能是64,而元素個數僅存了2個)
    uintptr_t max_hash_displacement;   // 可能會發生的hash沖突的最大次數
};

hash定位

當向weak_table_t 中插入或查找某個元素時,是通過如下hash算法的(以查找為例):

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;

    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); // 觸發bad weak table crash
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    return &weak_table->weak_entries[index];
}

首先,確定hash值可能對應的數組下標begin

size_t begin = hash_pointer(referent) & weak_table->mask;

hash_pointer(referent)將會對referent進行hash操作:

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

這個算法不用深究,知道就是一個hash操作就好了。

有技巧的是后半部分& weak_table->mask,將hash值和mask位與運算。
之前說過,mask 的值等于數組長度-1。而在下面的小節你會了解到,hash數組的長度會以64,128,256規律遞增。總之,數組長度表現為二進制會是1000...0這種形式,即首位1,后面跟n個0。而這個值減1的話,則會變為011...1這種形式,即首位0,后面跟n個1,這即mask的二進制形式。那么用mask & hash_pointer(referent)時,就會保留hash_pointer(referent)的后n位的值,而首位被位與操作置為了0。那么這個值肯定是小于首位是1的數值的,也就是肯定會小于數組的長度。

因此,begin是一個小于數組長度的一個數組下標,且這個下標對應著目標元素的hash值。

確定了初始的數組下標后,就開始嘗試確定元素的真正位置:

 while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask; // hash沖突,做index+1,嘗試下一個相鄰位置,& weak_table->mask 確保了index不會越界,而且會使index自動find數組一圈
        if (index == begin) bad_weak_table(weak_table->weak_entries); // 在數組中轉了一圈還沒找到目標元素,觸發bad weak table crash
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) { // 如果hash沖突大于了最大可能的沖突次數,則說明目標對象不存在于數組中,返回nil
            return nil;
        }
  }

這里,產生了hash沖突后,系統會依次線性循環尋找目標對象的位置。直到找了一圈又回到了起點或大于了可能的hash沖突值。這個max_hash_displacement值是在每個元素插入的時候更新的,它總是記錄在插入時,所發生的hash沖突的最大值。因此在查找時,hash沖突的次數肯定不會大于這個值。

這里最巧妙的是這條語句:

index = (index+1) & weak_table->mask

<font color=orange>它即會讓你向下一個相鄰位置尋找,同時當尋找到最后一個位置時,它又會自動讓你從數組的第一個位置開始尋找。這一切,都歸功于二進制運算的巧妙運用。</font>

hash表自動擴容

這里的weak table的大小是不固定的。當插入新元素時,會調用weak_grow_maybe方法,來判斷是否要做hash表的擴容。該方法實現如下:


#define TABLE_SIZE(entry) (entry->mask ? entry->mask + 1 : 0)

// Grow the given zone's table of weak references if it is full.
static void weak_grow_maybe(weak_table_t *weak_table)
{
    size_t old_size = TABLE_SIZE(weak_table);

    // Grow if at least 3/4 full.
    if (weak_table->num_entries >= old_size * 3 / 4) { // 當大于現有長度的3/4時,會做數組擴容操作。
        weak_resize(weak_table, old_size ? old_size*2 : 64);  // 初次會分配64個位置,之后在原有基礎上*2
    }
}

這里的擴容會調用weak_resize方法。每次擴容都會是原有長度的一倍。這樣,每次擴容的新增空間都會比上一次要大一倍,而不是固定的擴容n個空間。這么做的目的在于,系統認為,當你有擴容需求時,之后又擴容需求的概率就會變大,為了防止頻繁的申請內存,所以,每次擴容強度都會比上一次要大。

hash表自動收縮

當從weak table中刪除元素時,系統會調用weak_compact_maybe判斷是否需要收縮hash數組的空間 :


// Shrink the table if it is mostly empty.
static void weak_compact_maybe(weak_table_t *weak_table)
{
    size_t old_size = TABLE_SIZE(weak_table);

    // Shrink if larger than 1024 buckets and at most 1/16 full.
    if (old_size >= 1024  && old_size / 16 >= weak_table->num_entries) { // 當前數組長度大于1024,且實際使用空間最多只有1/16時,需要做收縮操作
        weak_resize(weak_table, old_size / 8); // 縮小8倍
        // leaves new table no more than 1/2 full
    }
}

hash表resize

無論是擴容還是收縮,最終都會調用到weak_resize方法:

static void weak_resize(weak_table_t *weak_table, size_t new_size)
{
    size_t old_size = TABLE_SIZE(weak_table);

    weak_entry_t *old_entries = weak_table->weak_entries;  // 先把老數據取出來
    weak_entry_t *new_entries = (weak_entry_t *)   // 在為新的size申請內存
        calloc(new_size, sizeof(weak_entry_t));

    // 重置weak_table的各成員
    weak_table->mask = new_size - 1;
    weak_table->weak_entries = new_entries;
    weak_table->max_hash_displacement = 0;
    weak_table->num_entries = 0;  // restored by weak_entry_insert below
    
    if (old_entries) {
        weak_entry_t *entry;
        weak_entry_t *end = old_entries + old_size;
        for (entry = old_entries; entry < end; entry++) {
            if (entry->referent) { // 依次將老的數據插入到新的內存空間
                weak_entry_insert(weak_table, entry);
            }
        }
        free(old_entries); // 釋放老的內存空間
    }
}

/** 
 * Add new_entry to the object's table of weak references.
 * Does not check whether the referent is already in the table.
 */
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
    weak_entry_t *weak_entries = weak_table->weak_entries;
    assert(weak_entries != nil);

    size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_entries[index].referent != nil) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_entries);
        hash_displacement++;
    }

    weak_entries[index] = *new_entry;
    weak_table->num_entries++;

    if (hash_displacement > weak_table->max_hash_displacement) { // 這里記錄最大的hash沖突次數,當查找元素時,hash沖突肯定不會大于這個值
        weak_table->max_hash_displacement = hash_displacement;
    }
}

OK, 上面就是對runtime中weak引用的相關數據結構的分析。關于weak引用數據,是存在于hash表中的。
這關于hash算法映射到數組下標,以及hash表動態的擴容/收縮,還是很有意思的。

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