Block的底層知識(shí)泡仗!

Objective-C 使用引用計(jì)數(shù)作為 iPhone 應(yīng)用的內(nèi)存管理方案,引用計(jì)數(shù)相比 GC 更適用于內(nèi)存不太充裕的場(chǎng)景赡麦,只需要收集與對(duì)象關(guān)聯(lián)的局部信息來(lái)決定是否回收對(duì)象朴皆,而 GC 為了明確可達(dá)性,需要全局的對(duì)象信息泛粹。引用計(jì)數(shù)固然有其優(yōu)越性遂铡,但也正是因?yàn)槿狈?duì)全局對(duì)象信息的把控,導(dǎo)致 Objective-C 無(wú)法自動(dòng)銷(xiāo)毀陷入循環(huán)引用的對(duì)象晶姊。雖然 Objective-C 通過(guò)引入弱引用技術(shù)扒接,讓開(kāi)發(fā)者可以盡可能地規(guī)避這個(gè)問(wèn)題,但在引用層級(jí)過(guò)深,引用路徑不那么直觀的情況下钾怔,即使是經(jīng)驗(yàn)豐富的工程師碱呼,也無(wú)法百分百保證產(chǎn)出的代碼不存在循環(huán)引用。

這時(shí)候就需要有一種檢測(cè)方案宗侦,可以實(shí)時(shí)檢測(cè)對(duì)象之間是否發(fā)生了循環(huán)引用愚臀,來(lái)輔助開(kāi)發(fā)者及時(shí)地修正代碼中存在的內(nèi)存泄漏問(wèn)題。要想檢測(cè)出循環(huán)引用矾利,最直觀的方式是遞歸地獲取對(duì)象強(qiáng)引用的其他對(duì)象姑裂,并判斷檢測(cè)對(duì)象是否被其路徑上的對(duì)象強(qiáng)引用了,也就是在有向圖中去找環(huán)梦皮。明確檢測(cè)方式之后炭分,接下來(lái)需要解決的是如何獲取強(qiáng)引用鏈,也就是獲取對(duì)象的強(qiáng)引用剑肯,尤其是最容易造成循環(huán)引用的 block捧毛。

Block 捕獲實(shí)體引用

往期關(guān)于 Block 的文章 對(duì) Block 的一點(diǎn)補(bǔ)充、用 Block 實(shí)現(xiàn)委托方法让网、Block技巧與底層解析

捕獲區(qū)域布局初探

首先根據(jù) block 的定義結(jié)構(gòu)呀忧,可以簡(jiǎn)單地將其視為:

structsr_block_layout {

void*isa;

intflags;

intreserved;

void(*invoke)(void*, ...);

structsr_block_descriptor *descriptor;

/* Imported variables. */

};

// 標(biāo)志位不一樣,這個(gè)結(jié)構(gòu)的實(shí)際布局也會(huì)有差別溃睹,這里簡(jiǎn)單地放在一起好閱讀

structsr_block_descriptor {

unsignedlongreserved;// Block_descriptor_1

unsignedlongsize;// Block_descriptor_1

void(*)(void*dst,void*src);// Block_descriptor_2 BLOCK_HAS_COPY_DISPOSE

void(*dispose)(void*);// Block_descriptor_2

constchar*signature;// Block_descriptor_3 BLOCK_HAS_SIGNATURE

constchar*layout;// Block_descriptor_3 contents depend on BLOCK_HAS_EXTENDED_LAYOUT

};

可以看到 block 捕獲的變量都會(huì)存儲(chǔ)在 sr_block_layout 結(jié)構(gòu)體 descriptor 字段之后的內(nèi)存空間中而账,下面我們通過(guò) clang -rewrite-objc 重寫(xiě)如下代碼語(yǔ)句 :

int i = 2;

^{

i;

};

可以得到 :

struct__block_impl {

void*isa;

intFlags;

intReserved;

void*FuncPtr;

};

struct__main_block_impl_0 {

struct__block_impl impl;

struct__main_block_desc_0* Desc;

inti;

...

};

__main_block_impl_0 結(jié)構(gòu)中新增了捕獲的 i 字段,即 sr_block_layout 結(jié)構(gòu)體的 imported variables 部分,這種操作可以看作在 sr_block_layout 尾部定義了一個(gè) 0 長(zhǎng)數(shù)組,可以根據(jù)實(shí)際捕獲變量的大小忌怎,給捕獲區(qū)域申請(qǐng)對(duì)應(yīng)的內(nèi)存空間鹊杖,只不過(guò)這一操作由編譯器完成 :

structsr_block_layout {

void*isa;

intflags;

intreserved;

void(*invoke)(void*, ...);

structsr_block_descriptor *descriptor;

charcaptured[0];

};

既然已經(jīng)知道了捕獲變量 i 的存放地址搅吁,那么我們就可以通過(guò) *(int *)layout->captured 在運(yùn)行時(shí)獲取 i 的值。得到了捕獲區(qū)域的起始地址之后,我們?cè)賮?lái)看捕獲區(qū)域的布局問(wèn)題,考慮以下代碼塊 :

inti =2;

NSObject*o = [NSObjectnew];

void(^blk)(void) = ^{

i;

o;

};

捕獲區(qū)域的布局分兩部分看:順序和大小锯茄,我們先使用老方法重寫(xiě)代碼塊 :

struct__main_block_impl_0 {

struct__block_impl impl;// 24

struct__main_block_desc_0* Desc;// 8 指針占用內(nèi)存大小和尋址長(zhǎng)度相關(guān),在 64 位機(jī)環(huán)境下茶没,編譯器分配空間大小為 8 字節(jié)

inti;// 8

NSObject*o;// 8

...

};

按照目前 clang 針對(duì) 64 位機(jī)的默認(rèn)對(duì)齊方式(下文的字節(jié)對(duì)齊計(jì)算都基于此前提條件)肌幽,可以計(jì)算出這個(gè)結(jié)構(gòu)體占用的內(nèi)存空間大小為 24 + 8 + 8 + 8 = 48字節(jié),并且按照上方代碼塊先 i 后 o 的捕獲排序方式抓半,如果我要訪(fǎng)問(wèn)捕獲的 o 對(duì)象指針變量喂急,只需要在捕獲區(qū)域起始地址上偏移 8 字節(jié)即可,我們可以借助 lldb 的 memory read (x) 命令查看這部分內(nèi)存空間 :

(lldb) po *(NSObject **)(layout->captured +8)

0x0000000000000002

(lldb) po *(NSObject **)layout->captured

(lldb) p *(int*)(layout->captured +8)

(int) $6=2

(lldb) p (int*)(layout->captured +8)

(int*) $9=0x0000000100740d18

(lldb) p layout->descriptor->size

(unsignedlong) $11=44

(lldb) x/44bx layout

0x100740cf0:0x700x210x7b0xa60xff0x7f0x000x00

0x100740cf8:0x020x000x000xc30x000x000x000x00

0x100740d00:0x400x1d0x000x000x010x000x000x00

0x100740d08:0xb00x200x000x000x010x000x000x00

0x100740d10:0x900xf20x730x000x010x000x000x00

0x100740d18:0x020x000x000x00

和使用 clang -rewrite-objc 重寫(xiě)時(shí)的猜想不一樣笛求,我們可以從以上終端日志中看出以下兩點(diǎn) :

捕獲變量 i煮岁、o 在捕獲區(qū)域的排序方式為 o讥蔽、i,o 變量地址與捕獲起始地址一致画机,i 變量地址為捕獲起始地址加上 8 字節(jié)

捕獲整形變量 i 在內(nèi)存中實(shí)際占用空間大小為 4 字節(jié)

那么 block 到底是怎么對(duì)捕獲變量進(jìn)行排序,并且為其分配內(nèi)存空間的呢新症?這就需要看 clang 是如何處理 block 捕獲的外部變量了步氏。

捕獲區(qū)域布局分析

首先解決捕獲變量排序的問(wèn)題,根據(jù) clang 針對(duì)這部分的排序代碼徒爹,我們可以知道荚醒,在對(duì)齊字節(jié)數(shù) (alignment) 不相等時(shí),捕獲的實(shí)體按照 alignment 降序排序 (C 結(jié)構(gòu)體比較特殊隆嗅,即使整體占用空間比指針變量大界阁,也排在對(duì)象指針后面),否則按照以下類(lèi)型進(jìn)行排序 :

__strong 修飾對(duì)象指針變量

__block 修飾對(duì)象指針變量

__weak 修飾對(duì)象指針變量

其他變量

再結(jié)合 clang 對(duì)捕獲變量對(duì)齊子節(jié)數(shù)計(jì)算方式 胖喳,我們可以知道泡躯,block 捕獲區(qū)域變量的對(duì)齊結(jié)果趨向于被__attribute__ ((__packed__))修飾了的結(jié)構(gòu)體,舉個(gè)例子 :

structfoo {

void*p;// 8

inti;// 4

charc;// 4 實(shí)際用到的內(nèi)存大小為 1

};

創(chuàng)建 foo 結(jié)構(gòu)體需要分配的空間大小為 8 + 4 + 4 = 16丽焊,關(guān)于結(jié)構(gòu)體的內(nèi)存對(duì)齊方式较剃,這里額外說(shuō)幾句,編譯器會(huì)按照成員列表的順序一個(gè)接一個(gè)地給每個(gè)成員分配內(nèi)存技健,只有當(dāng)存儲(chǔ)成員需要滿(mǎn)足正確的邊界對(duì)齊要求時(shí)写穴,成員之間才可能出現(xiàn)用于填充的額外內(nèi)存空間,以提升計(jì)算機(jī)的訪(fǎng)問(wèn)速度(對(duì)齊標(biāo)準(zhǔn)一般和尋址長(zhǎng)度一致)雌贱,在聲明結(jié)構(gòu)體時(shí)啊送,讓那些對(duì)齊邊界要求最嚴(yán)格的成員最先出現(xiàn),對(duì)邊界要求最弱的成員最后出現(xiàn)欣孤,可以最大限度地減少因邊界對(duì)齊而帶來(lái)的空間損失馋没。再看以下代碼塊 :

structfoo {

void*p;// 8

inti;// 4

charc;// 1

} __attribute__ ((__packed__));

__attribute__ ((__packed__))編譯屬性告訴編譯器,按照字段的實(shí)際占用子節(jié)數(shù)進(jìn)行對(duì)齊导街,所以創(chuàng)建 foo 結(jié)構(gòu)體需要分配的空間大小為 8 + 4 + 1 = 13披泪。

結(jié)合以上兩點(diǎn),我們可以嘗試分析以下 block 捕獲區(qū)域的變量布局情況 :

NSObject*o1 = [NSObjectnew];

__weakNSObject*o2 = o1;

__blockNSObject*o3 = o1;

unsignedlonglongj =4;

inti =3;

charc ='a';

void(^blk)(void) = ^{

i;

c;

o1;

o2;

o3;

j;

};

首先按照 aligment 排序搬瑰,可以得到排序順序?yàn)?[o1 o2 o3] j i c款票,再根據(jù)strong、block泽论、__weak 修飾符對(duì) o1 o2 o3 進(jìn)行排序艾少,可得到最終結(jié)果 o1[8] o3[8] o2[8] j[8] i[4] c[1]。同樣的翼悴,我們使用 lldb 的 x 命令驗(yàn)證分析結(jié)果是否正確 :

(lldb) x/69bx layout

0x10200d940:0x70 0x210x7b 0xa6 0xff 0x7f0x00 0x00

0x10200d948:0x02 0x000x00 0xc3 0x00 0x00 0x00 0x00

0x10200d950: 0xf0 0x1b0x00 0x000x01 0x000x00 0x00

0x10200d958: 0xf8 0x20 0x00 0x00 0x01 0x00 0x00 0x00

0x10200d960: 0xa0 0xf6 0x00 0x02 0x01 0x00 0x00 0x00? // o1

0x10200d968: 0x90 0xd9 0x00 0x02 0x01 0x00 0x00 0x00? // o3

0x10200d970: 0xa0 0xf6 0x00 0x02 0x01 0x00 0x00 0x00? // o2

0x10200d978:0x04 0x000x00 0x000x00 0x000x00 0x00// j

0x10200d980:0x03 0x000x00 0x000x61? ? ? ? ? ? ? ? // i c

(lldb) p o1

(NSObject *) $1 =0x000000010200f6a0

可以看到缚够,小端模式下幔妨,捕獲的 o1 和 o2 指針變量值為 0x10200f6a0 ,對(duì)應(yīng)內(nèi)存地址為 0x10200d960 和 0x10200d970,而 o3 因?yàn)楸?__block 修飾谍椅,編譯器為 o3 捕獲變量包裝了一層 byref 結(jié)構(gòu)误堡,所以其值為 byref 結(jié)構(gòu)的地址 0x102000d990 ,而不是 0x10200f6a0 雏吭,捕獲的 j 變量地址為 0x10200d978锁施,i 變量地址為 0x10200d980,c 字符變量緊隨其后杖们。

Descriptor 的 Layout 信息

經(jīng)過(guò)上述的一系列分析悉抵,捕獲區(qū)域變量的布局方式已經(jīng)大致摸清了,接下來(lái)回過(guò)頭看下 sr_block_descriptor 結(jié)構(gòu)的 layout 字段是用來(lái)干嘛的摘完。從字面上理解姥饰,這個(gè)字段很可能保存了 block 某一部分的內(nèi)存布局信息,比如捕獲區(qū)域的布局信息孝治,我們依舊使用上文的最后一個(gè)例子列粪,看看 layout 的值 :

(lldb) p layout->descriptor->layout

(const char *)$2=0x0000000000000111""

可以看到 layout 值為空字符串,并沒(méi)有展示出任何直觀的布局信息荆秦,看來(lái)要想知道 layout 是怎么運(yùn)作的篱竭,還需要閱讀這一部分的 block 代碼 和 clang 代碼,我們一步步地分析這兩段代碼里面隱藏的信息步绸,這里貼出其中的部分代碼和注釋 :

// block

// Extended layout encoding.

// Values for Block_descriptor_3->layout with BLOCK_HAS_EXTENDED_LAYOUT

// and for Block_byref_3->layout with BLOCK_BYREF_LAYOUT_EXTENDED

// If the layout field is less than 0x1000, then it is a compact encoding

// of the form 0xXYZ: X strong pointers, then Y byref pointers,

// then Z weak pointers.

// If the layout field is 0x1000 or greater, it points to a

// string of layout bytes. Each byte is of the form 0xPN.

// Operator P is from the list below. Value N is a parameter for the operator.

enum{

...

? ? BLOCK_LAYOUT_NON_OBJECT_BYTES=1,// N bytes non-objects

? ? BLOCK_LAYOUT_NON_OBJECT_WORDS=2,// N words non-objects

? ? BLOCK_LAYOUT_STRONG=3,// N words strong pointers

? ? BLOCK_LAYOUT_BYREF=4,// N words byref pointers

? ? BLOCK_LAYOUT_WEAK=5,// N words weak pointers

...

};

// clang

/// InlineLayoutInstruction - This routine produce an inline instruction for the

/// block variable layout if it can. If not, it returns 0. Rules are as follow:

/// If ((uintptr_t) layout) < (1 << 12), the layout is inline. In the 64bit world,

/// an inline layout of value 0x0000000000000xyz is interpreted as follows:

/// x captured object pointers of BLOCK_LAYOUT_STRONG. Followed by

/// y captured object of BLOCK_LAYOUT_BYREF. Followed by

/// z captured object of BLOCK_LAYOUT_WEAK. If any of the above is missing, zero

/// replaces it. For example, 0x00000x00 means x BLOCK_LAYOUT_STRONG and no

/// BLOCK_LAYOUT_BYREF and no BLOCK_LAYOUT_WEAK objects are captured.

首先要解釋的是 inline 這個(gè)詞掺逼,Objective-C 中有一種叫做 Tagged Pointer 的技術(shù),它讓指針保存實(shí)際值瓤介,而不是保存實(shí)際值的地址吕喘,這里的 inline 也是相同的效果,即讓 layout 指針保存實(shí)際的編碼信息刑桑。在 inline 狀態(tài)下氯质,使用十六進(jìn)制中的一位表示捕獲變量的數(shù)量,所以每種類(lèi)型的變量最多只能有 15 個(gè)祠斧,此時(shí)的 layout 的值以 0xXYZ 形式呈現(xiàn)闻察,其中 X、Y琢锋、Z 分別表示捕獲strong辕漂、block、__weak 修飾指針變量的個(gè)數(shù)吴超,如果其中某個(gè)類(lèi)型的數(shù)量超過(guò) 15 或者捕獲變量的修飾類(lèi)型不為這三種任何一個(gè)時(shí)钉嘹,比如捕獲的變量由 __unsafe_unretained 修飾,則采用另一種編碼方式鲸阻,這種方式下跋涣,layout 會(huì)指向一個(gè)字符串缨睡,這個(gè)字符串的每個(gè)字節(jié)以 0xPN 的形式呈現(xiàn),并以 0x00 結(jié)束陈辱,P 表示變量類(lèi)型奖年,N 表示變量個(gè)數(shù),需要注意的是沛贪,N 為 0 表示 P 類(lèi)型有一個(gè)拾并,而不是 0 個(gè),也就是說(shuō)實(shí)際的變量個(gè)數(shù)比 N 大 1鹏浅。需要注意的是,捕獲 int 等基礎(chǔ)類(lèi)型屏歹,不影響 layout 的呈現(xiàn)方式隐砸,layout 編碼中也不會(huì)有關(guān)于基礎(chǔ)類(lèi)型的信息,除非需要基礎(chǔ)類(lèi)型的編碼來(lái)輔助定位對(duì)象指針類(lèi)型的位置蝙眶,比如捕獲含有對(duì)象指針字段的結(jié)構(gòu)體季希。舉幾個(gè)例子 :

unsignedlonglongj =4;

inti =3;

charc ='a';

void(^blk)(void) = ^{

i;

c;

j;

};

以上代碼塊沒(méi)有捕獲任何對(duì)象指針,所以實(shí)際的 descriptor 不包含 copy 和 dispose 字段幽纷,去除這兩個(gè)字段后式塌,再輸出實(shí)際的布局信息,結(jié)果為空(0x00 表示結(jié)束)友浸,說(shuō)明捕獲一般基礎(chǔ)類(lèi)型變量不會(huì)計(jì)入實(shí)際的 layout 編碼 :

(lldb) p/x (long)layout->descriptor->layout

(long) $0=0x0000000100001f67

(lldb) x/8bx layout->descriptor->layout

0x100001f67:0x000x760x310x360x400x300x3a0x38

接著嘗試第一種 layout 方式 :

NSObject*o1 = [NSObjectnew];

__blockNSObject*o3 = o1;

__weakNSObject*o2 = o1;

void(^blk)(void) = ^{

o1;

o2;

o3;

};

以上代碼塊對(duì)應(yīng)的 layout 值為 0x111 峰尝,表示三種類(lèi)型變量每種一個(gè) :

(lldb) p/x (long)layout->descriptor->layout

(long)$0=0x0000000000000111

再?lài)L試第二種 layout 編碼方式 :

NSObject*o1 = [NSObjectnew];

__blockNSObject*o3 = o1;

__weakNSObject*o2 = o1;

NSObject*o4 = o1;

...// 5 - 18

NSObject*o19 = o1;

void(^blk)(void) = ^{

o1;

o2;

o3;

o4;

...// 5 - 18

o19;

};

以上代碼塊對(duì)應(yīng)的 layout 值是一個(gè)地址 0x0000000100002f44 ,這個(gè)地址為編碼字符串的起始地址收恢,轉(zhuǎn)換成十六進(jìn)制后為 0x3f 0x30 0x40 0x50 0x00武学,其中 P 為 3 表示 __strong 修飾的變量,數(shù)量為 15(f) + 1 + 0 + 1 = 17 個(gè)伦意,P 為 4 表示 __block 修飾的變量火窒,數(shù)量為 0 + 1 = 1 個(gè), P 為 5 表示 __weak 修飾的變量驮肉,數(shù)量為 0 + 1 = 1 個(gè) :

(lldb) p/x (long)layout->descriptor->layout

(long) $0=0x0000000100002f44

(lldb) x/8bx layout->descriptor->layout

0x100002f44:0x3f0x300x400x500x000x760x310x36

結(jié)構(gòu)體對(duì)捕獲布局的影響

由于結(jié)構(gòu)體字段的布局順序在聲明時(shí)就已經(jīng)確定了熏矿,無(wú)法像 block 構(gòu)造捕獲區(qū)域一樣,按照變量類(lèi)型离钝、修飾符進(jìn)行調(diào)整票编,所以如果結(jié)構(gòu)體中有類(lèi)型為對(duì)象指針的字段,就需要一些額外信息來(lái)計(jì)算這些對(duì)象指針字段的偏移量奈辰,需要注意的是栏妖,被捕獲結(jié)構(gòu)體的內(nèi)存對(duì)齊信息和未捕獲時(shí)一致,以尋址長(zhǎng)度作為對(duì)齊基準(zhǔn)奖恰,捕獲操作并不會(huì)變更對(duì)齊信息吊趾。同樣地宛裕,我們先嘗試捕獲只有基本類(lèi)型字段的結(jié)構(gòu)體 :

structS {

charc;

inti;

longj;

} foo;

void(^blk)(void) = ^{

foo;

};

然后調(diào)整 descriptor 結(jié)構(gòu),輸出 layout :

(lldb) x/8bx layout->descriptor->layout

0x100001f67:0x000x760x310x360x400x300x3a0x38

可以看到论泛,只有含有基本類(lèi)型的結(jié)構(gòu)體揩尸,同樣不會(huì)影響 block 的 layout 編碼信息。接下來(lái)我們給結(jié)構(gòu)體新增 __strong 和 __weak 修飾的對(duì)象指針字段 :

structS {

charc;

inti;

__strongNSObject*o1;

longj;

__weakNSObject*o2;

} foo;

void(^blk)(void) = ^{

foo;

};

同樣分析輸出 layout :

(lldb) x/8bx layout->descriptor->layout

0x100002f47:0x200x300x200x500x000x760x310x36

layout 編碼為0x20 0x30 0x20 0x50 0x00屁奏,其中 P 為 2 表示 word 字類(lèi)型(非對(duì)象)岩榆,由于字大小一般和指針一致,所以這里表示占用了 8 * (N + 1) 個(gè)字節(jié)坟瓢,第一個(gè) 0x20 表示非對(duì)象指針類(lèi)型占用了 8 個(gè)字節(jié)勇边,也就是 char 類(lèi)型和 int 類(lèi)型字段對(duì)齊之后所占用的空間,接著 0x30 表示有一個(gè) __strong 修飾的對(duì)象指針字段折联,第二個(gè) 0x20 表示非對(duì)象指針 long 類(lèi)型占用了 8 個(gè)字節(jié)粒褒,最后的 0x50 表示有一個(gè) __weak 修飾的對(duì)象指針字段诚镰。由于編碼中包含了每個(gè)字段的排序和大小清笨,我們就可以通過(guò)解析 layout 編碼后的偏移量抠艾,拿到想要的對(duì)象指針值跌帐。P 還有個(gè) byte 類(lèi)型谨敛,值為 1 脸狸,和 word 類(lèi)型有相似的功能炊甲,只是表示的空間大小不同卿啡。

Byref 結(jié)構(gòu)的布局

由 __block 修飾的捕獲變量颈娜,會(huì)先轉(zhuǎn)換成 byref 結(jié)構(gòu),再由這個(gè)結(jié)構(gòu)去持有實(shí)際的捕獲變量粟瞬,block 只負(fù)責(zé)管理 byref 結(jié)構(gòu)裙品。

// 標(biāo)志位不一樣市怎,這個(gè)結(jié)構(gòu)的實(shí)際布局也會(huì)有差別焰轻,這里簡(jiǎn)單地放在一起好閱讀

structsr_block_byref {

void*isa;

structsr_block_byref *forwarding;

volatileint32_tflags;// contains ref count

uint32_tsize;

// requires BLOCK_BYREF_HAS_COPY_DISPOSE

void(*byref_keep)(structsr_block_byref *dst,structsr_block_byref *src);

void(*byref_destroy)(structsr_block_byref *);

// requires BLOCK_BYREF_LAYOUT_EXTENDED

constchar*layout;

};

以上代碼塊就是 byref 對(duì)應(yīng)的結(jié)構(gòu)體。第一眼看上去狞膘,我比較困惑為什么還要有 layout 字段挽封,雖然上文的 block 源碼注釋說(shuō)明了 byref 和 block 結(jié)構(gòu)一樣辅愿,都具備兩種不同的布局編碼方式点待,但是 byref 不是只針對(duì)一個(gè)變量么癞埠,難道和 block 捕獲區(qū)域一樣也可以攜帶多個(gè)捕獲變量苗踪?帶著這個(gè)困惑,我們先看下以下表達(dá)式 :

__blockNSObject*o1 = [NSObjectnew];

使用 clang 重寫(xiě)之后 :

struct__Block_byref_o1_0 {

void*__isa;

__Block_byref_o1_0 *__forwarding;

int__flags;

int__size;

void(*__Block_byref_id_object_copy)(void*,void*);

void(*__Block_byre/* @autoreleasepool */o{ __AtAutoreleasePool __autoreleasepool; e)(void*);

NSObject*o1;

};

和 block 捕獲變量一樣颅夺,byref 攜帶的變量也是保存在結(jié)構(gòu)體尾部的內(nèi)存空間里碗啄,當(dāng)前上下文中稚字,可以直接通過(guò) sr_block_byref 的 layout 字段獲取 o1 對(duì)象指針值胆描〔玻可以看到车吹,在包裝如對(duì)象指針這類(lèi)常規(guī)變量時(shí)窄驹,layout 字段并沒(méi)有起到實(shí)質(zhì)性的作用乐埠,那什么條件下的 layout 才表示布局編碼信息呢丈咐?如果使用 layout 字段表示編碼信息,那么攜帶的變量又是何處安放的呢歹河?我們一個(gè)個(gè)解答秸歧。

針對(duì)第一個(gè)問(wèn)題,先看以下代碼塊 :

__blockstructS {

NSObject*o1;

} foo;

foo.o1= [NSObjectnew];

void(^blk)(void) = ^{

foo;

};

使用 clang 重寫(xiě)之后 :

struct__Block_byref_foo_0 {

void*__isa;

__Block_byref_foo_0 *__forwarding;

int__flags;

int__size;

void(*__Block_byref_id_object_copy)(void*,void*);

void(*__Block_byref_id_object_dispose)(void*);

structS foo;

};

和常規(guī)類(lèi)型一樣经备,foo 結(jié)構(gòu)體保存在結(jié)構(gòu)體尾部造虎,也就是原本 layout 所在的字段算凿,重寫(xiě)的代碼中依然看不到 layout 的蹤影,接著我們?cè)囍敵?foo :

(lldb) po foo.o1

(lldb) p (structS)a_byref->layout

error: Multiple internal symbols foundfor'S'

(lldb) p/x (long)a_byref->layout

(long) $3=0x0000000000000100

(lldb) x/56bx a_byref

0x100627c20:0x000x000x000x000x000x000x000x00

0x100627c28:0x200x7c0x620x000x010x000x000x00

0x100627c30:0x040x000x000x130x380x000x000x00

0x100627c38:0x900x1b0x000x000x010x000x000x00

0x100627c40:0x000x1c0x000x000x010x000x000x00

0x100627c48:0x000x010x000x000x000x000x000x00

0x100627c50:0x300xf10x610x000x010x000x000x00

看來(lái)事情并沒(méi)有看上去的那么簡(jiǎn)單署鸡,首先重寫(xiě)代碼中 foo 字段所在內(nèi)存保存的并不是結(jié)構(gòu)體,而是 0x0000000000000100,這個(gè) 100 是不是看著有點(diǎn)眼熟,沒(méi)錯(cuò),這就是 byref 的 layout 信息,根據(jù) 0xXYZ 編碼規(guī)則蠢络,這個(gè)值表示有 1 個(gè) __strong 修飾的對(duì)象指針结序。接著針對(duì)第二個(gè)問(wèn)題被济,攜帶的對(duì)象指針變量存在哪元媚,我們把視線(xiàn)往下移動(dòng) 8 個(gè)字節(jié)当犯,這不就是 foo.o1 對(duì)象指針的值么趣钱〔泛荆總結(jié)下烙常,在存在 layout 的情況下蚕脏,byref 使用 8 個(gè)字節(jié)保存 layout 編碼信息秦驯,并緊跟著在 layout 字段后存儲(chǔ)捕獲的變量洛心。

以上是 byref 的第一種 layout 編碼方式皂甘,我們?cè)賴(lài)L試第二種 :

__blockstructS {

charc;

NSObject*o1;

__weakNSObject*o3;

} foo;

foo.o1= [NSObjectnew];

void(^blk)(void) = ^{

foo;

};

使用 clang 重寫(xiě)代碼之后 :

struct__Block_byref_foo_0 {

void*__isa;

__Block_byref_foo_0 *__forwarding;

int__flags;

int__size;

void(*__Block_byref_id_object_copy)(void*,void*/* @autoreleasepool */c{ __AtAutoreleasePool __autoreleasepool; _byref

struct__main_block_impl_0 {

struct__block_impl impl;

struct__main_block_desc_0* Desc;

__main_block_impl_0(void*fp,struct__main_block_desc_0 *desc,intflags=0) {

impl.isa= &_NSConcreteStackBlock;

impl.Flags= flags;

impl.FuncPtr= fp;

Desc = desc;

}

};

emmmm …渐夸,上面代碼并不是粘貼錯(cuò)誤墓塌,貌似 Rewriter 并不能很好地處理這種情況,看來(lái)又需要我們直接去看對(duì)應(yīng)內(nèi)存地址中的值了 :

(lldb) x/72bx a_byref

0x100755140:0x000x000x000x000x000x000x000x00

0x100755148:0x400x510x750x000x010x000x000x00

0x100755150:0x040x000x000x130x480x000x000x00

0x100755158:0x100x1b0x000x000x010x000x000x00

0x100755160:0xa00x1b0x000x000x010x000x000x00

0x100755168:0x8d0x3e0x000x000x010x000x000x00

0x100755170:0x000x5f0x6b0x650x790x000x000x00

0x100755178:0xd00x6e0x750x000x010x000x000x00

0x100755180:0x000x000x000x000x000x000x000x00

(lldb) x/8bx a_byref->layout

0x100003e8d:0x200x300x500x000x530x520x4c0x61

地址 0x100755168 中保存了 layout 編碼字符串的地址 0x0000000100003e8d ,將此字符串轉(zhuǎn)換成十六進(jìn)制后為 0x20 0x30 0x50 0x00 ,這些值的含義在結(jié)構(gòu)體對(duì)捕獲布局的影響一節(jié)中已經(jīng)描述過(guò),這里就不重復(fù)說(shuō)明了。

強(qiáng)引用對(duì)象的獲取

目前我們已經(jīng)知道了 block / byref 如何布局捕獲區(qū)域內(nèi)存,以及如何獲取關(guān)鍵的布局信息遍尺,接下來(lái)我們就可以嘗試獲取 block 強(qiáng)引用的對(duì)象了三热,這里我把強(qiáng)引用的對(duì)象分成兩部分 :

被 block 強(qiáng)引用

被 byref 結(jié)構(gòu)強(qiáng)引用

只要獲取這兩部分強(qiáng)引用的對(duì)象呐能,任務(wù)就算完成了首妖,由于上文已經(jīng)將整個(gè)原理脈絡(luò)理清了象踊,所以編寫(xiě)出可用的代碼并不困難栈虚。這兩部分都涉及到布局編碼逆害,我們先根據(jù) layout 的編碼方式,解析出捕獲變量的類(lèi)型和數(shù)量 :

SRCapturedLayoutInfo *info = [SRCapturedLayoutInfonew];

if((uintptr_t)layout < (1<<12)) {

uintptr_t inlineLayout = (uintptr_t)layout;

[infoaddItemWithType:SR_BLOCK_LAYOUT_STRONGcount:(inlineLayout &0xf00) >>8];

[infoaddItemWithType:SR_BLOCK_LAYOUT_BYREFcount:(inlineLayout &0xf0) >>4];

[infoaddItemWithType:SR_BLOCK_LAYOUT_WEAKcount:inlineLayout &0xf];

}else{

while(layout && *layout !='\x00') {

unsignedinttype = (*layout &0xf0) >>4;

unsignedintcount = (*layout &0xf) +1;

[infoaddItemWithType:typecount:count];

layout++;

}

}

然后遍歷 block 的布局編碼信息,根據(jù)變量類(lèi)型和數(shù)量,計(jì)算出對(duì)象指針地址偏移,然后獲取對(duì)應(yīng)的對(duì)象指針值 :

- (NSHashTable*)strongReferencesForBlockLayout:(void*)iLayout {

if(!iLayout)returnnil;

structsr_block_layout *aLayout = (structsr_block_layout *)iLayout;

constchar*extenedLayout = sr_block_extended_layout(aLayout);

_blockLayoutInfo = [SRCapturedLayoutInfo infoForLayoutEncode:extenedLayout];

NSHashTable*references = [NSHashTableweakObjectsHashTable];

uintptr_t *begin = (uintptr_t *)aLayout->captured;

for(SRLayoutItem *itemin_blockLayoutInfo.layoutItems) {

switch(item.type) {

caseSR_BLOCK_LAYOUT_STRONG: {

NSHashTable*objects = [item objectsForBeginAddress:begin];

SRAddObjectsFromHashTable(references, objects);

begin += item.count;

}break;

caseSR_BLOCK_LAYOUT_BYREF: {

for(inti =0; i < item.count; i++, begin++) {

structsr_block_byref *aByref = *(structsr_block_byref **)begin;

NSHashTable*objects = [selfstrongReferenceForBlockByref:aByref];

SRAddObjectsFromHashTable(references, objects);

}

}break;

caseSR_BLOCK_LAYOUT_NON_OBJECT_BYTES: {

begin = (uintptr_t *)((uintptr_t)begin + item.count);

}break;

default: {

begin += item.count;

}break;

}

}

returnreferences;

}

block 布局區(qū)域中的 byref 結(jié)構(gòu)需要進(jìn)行額外的處理煞烫,如果 byref 直接攜帶 __strong 修飾的變量,則不需要關(guān)心 layout 編碼料饥,直接從結(jié)構(gòu)尾部獲取指針變量值即可稀火,否則需要和處理 block 布局區(qū)域一樣沛慢,先得到布局信息,然后遍歷這些布局信息身腻,計(jì)算偏移量愈诚,獲取強(qiáng)引用對(duì)象地址 :

- (NSHashTable*)strongReferenceForBlockByref:(void*)iByref {

if(!iByref)returnnil;

structsr_block_byref *aByref = (structsr_block_byref *)iByref;

NSHashTable*references = [NSHashTableweakObjectsHashTable];

int32_t flag = aByref->flags & SR_BLOCK_BYREF_LAYOUT_MASK;

switch(flag) {

caseSR_BLOCK_BYREF_LAYOUT_STRONG: {

void**begin = sr_block_byref_captured(aByref);

idobject = (__bridgeid_Nonnull)(*(void**)begin);

if(object) [references addObject:object];

}break;

caseSR_BLOCK_BYREF_LAYOUT_EXTENDED: {

constchar*layout = sr_block_byref_extended_layout(aByref);

SRCapturedLayoutInfo *info = [SRCapturedLayoutInfo infoForLayoutEncode:layout];

[_blockByrefLayoutInfos addObject:info];

uintptr_t *begin = (uintptr_t *)sr_block_byref_captured(aByref) +1;

for(SRLayoutItem *itemininfo.layoutItems) {

switch(item.type) {

caseSR_BLOCK_LAYOUT_NON_OBJECT_BYTES: {

begin = (uintptr_t *)((uintptr_t)begin + item.count);

}break;

caseSR_BLOCK_LAYOUT_STRONG: {

NSHashTable*objects = [item objectsForBeginAddress:begin];

SRAddObjectsFromHashTable(references, objects);

begin += item.count;

}break;

default: {

begin += item.count;

}break;

}

}

}break;

default:break;

}

returnreferences;

}

完整代碼我放到了 BlockStrongReferenceObject,代碼并沒(méi)有進(jìn)行過(guò)很?chē)?yán)格的測(cè)試,可能存在一些未處理的邊界條件,需要嘗試 / 討論的同學(xué)可自取。

另一種強(qiáng)引用對(duì)象獲取方式

上文通過(guò)將 block 的布局編碼信息轉(zhuǎn)化為對(duì)應(yīng)字段的偏移量來(lái)獲取強(qiáng)引用對(duì)象慷妙,這一節(jié)介紹另外一種比較取巧的方式隙弛,也是目前檢測(cè)循環(huán)引用工具獲取 block 強(qiáng)引用對(duì)象的常用方式萍启,比如 facebook 的 FBRetainCycleDetector 驳遵。根據(jù)這塊功能對(duì)應(yīng)的源碼,此方式大致原理如下 :

獲取 block 的 dispose 函數(shù) (如果捕獲了強(qiáng)引用對(duì)象,需要利用這個(gè)函數(shù)解引用)

構(gòu)造一個(gè) fake 對(duì)象月弛,此對(duì)象由若干個(gè)擴(kuò)展的 byref 結(jié)構(gòu) (對(duì)象) 組成,其個(gè)數(shù)由 block size 決定,即把 block 劃分為若干個(gè) 8 字節(jié)內(nèi)存區(qū)域翩剪,就像以下代碼塊一樣 :

structS{

NSObject *o1;

NSObject *o2;

};

structSs ={

.o2 = [NSObject new]

};

void**fake = (void**)&s;

// fake[1] 和 s.o2 是一樣的

擴(kuò)展的 byref 結(jié)構(gòu)會(huì)重寫(xiě) release 方法,只在此方法中設(shè)置強(qiáng)引用標(biāo)識(shí)位剃根,不執(zhí)行原釋放邏輯

將 fake 對(duì)象作為參數(shù)苗傅,調(diào)用 dispose 函數(shù)逊桦,dispose 函數(shù)會(huì)去 release 每個(gè) block 強(qiáng)引用的對(duì)象匿情,在這里這些強(qiáng)引用對(duì)象被替換成了我們的 byref 結(jié)構(gòu)玲躯,所以我們可以通過(guò)它的強(qiáng)引用標(biāo)識(shí)位判斷 block 的哪塊區(qū)域保存了強(qiáng)引用對(duì)象地址

遍歷 fake 對(duì)象赡译,保存所有強(qiáng)引用標(biāo)志位被設(shè)置的 byref 結(jié)構(gòu)對(duì)應(yīng)索引只洒,后面通過(guò)這個(gè)索引可以去 block 中找強(qiáng)引用指針地址

釋放所有的 byref 結(jié)構(gòu)

根據(jù)上面得到的索引,獲取捕獲變量偏移量涝开,偏移量為索引值 * 8 字節(jié) (指針大小) 循帐,再根據(jù)偏移量去 block 內(nèi)存塊中拿強(qiáng)引用對(duì)象地址

關(guān)于這種方案,我們需要明確下面幾個(gè)點(diǎn)舀武。

首先這種方案也需要在明確 block 內(nèi)存布局的情況下才能夠?qū)嵤┲粞驗(yàn)?block ,或者說(shuō) block 結(jié)構(gòu)體银舱,實(shí)際執(zhí)行內(nèi)存對(duì)齊時(shí)瘪匿,并沒(méi)有按照尋址大小也就是 8 字節(jié)對(duì)齊,假設(shè) block 捕獲區(qū)域的對(duì)齊方式變成了這樣 :

struct__main_block_impl_0 {

struct__block_impl impl;// 24

struct__main_block_desc_0* Desc;// 8 指針占用內(nèi)存大小和尋址長(zhǎng)度相關(guān)寻馏,在 64 位機(jī)環(huán)境下柿顶,編譯器分配空間大小為 8 字節(jié)

inti;// 4? ? FakedByref 8

NSObject*o1;// 8? ? FakedByref 8 [這里上個(gè) FakedByref 后 4 個(gè)子節(jié)和當(dāng)前 FakedByref 前 4 字節(jié)覆蓋 o1 對(duì)象指針的 8 字節(jié),導(dǎo)致 miss ]

charc;// 1

NSObject*o2;// 8

}

那么使用 fake 的方案就會(huì)失效操软,因?yàn)檫@種方案的前提是 block 內(nèi)存對(duì)齊基準(zhǔn)基于尋址長(zhǎng)度嘁锯,即指針大小。不過(guò) block 對(duì)捕獲的變量按照類(lèi)型和尺寸進(jìn)行了排序聂薪,__strong 修飾的對(duì)象指針都在前面家乘,本來(lái)我們只需要這種類(lèi)型的變量,并不關(guān)心其他類(lèi)型藏澳,所以即使后面的對(duì)齊方式不滿(mǎn)足 fake 條件也沒(méi)關(guān)系仁锯,另外捕獲結(jié)構(gòu)體的對(duì)齊基準(zhǔn)是基于尋址長(zhǎng)度的,即使結(jié)構(gòu)體有其他類(lèi)型翔悠,也滿(mǎn)足 fake 條件 :

struct__main_block_impl_0 {

struct__block_impl impl;// 24

struct__main_block_desc_0* Desc;// 8 指針占用內(nèi)存大小和尋址長(zhǎng)度相關(guān)业崖,在 64 位機(jī)環(huán)境下,編譯器分配空間大小為 8 字節(jié)

NSObject*o1;// 8? ? FakedByref 8

NSObject*o2;// 8? ? FakedByref 8

inti;// 4? ? FakedByref 8

charc;// 1? ? ? ?

}

可以看到蓄愁,通過(guò)以上代碼塊的排序双炕,讓 o1 和 o2 都被 FakedByref 結(jié)構(gòu)覆蓋到了,而 i, c 變量本身就不會(huì)在 dispose 函數(shù)中訪(fǎng)問(wèn)撮抓,所以怎么設(shè)置都不會(huì)影響到策略的生效妇斤。

第二點(diǎn)是為什么要用擴(kuò)展的 byref 結(jié)構(gòu),而不是隨便整個(gè)重寫(xiě)了 release 的類(lèi)過(guò)來(lái),這是因?yàn)楫?dāng) block 捕獲了 __block 修飾的指針變量時(shí)站超,會(huì)將這個(gè)指針變量包裝成 byref 結(jié)構(gòu)荸恕,而 dispose 函數(shù)會(huì)對(duì)這個(gè) byref 結(jié)構(gòu)執(zhí)行Blockobject_dispose 操作,這個(gè)函數(shù)有兩個(gè)形參死相,一個(gè)是對(duì)象指針融求,一個(gè)是 flag ,當(dāng) flag 指明對(duì)象指針為 byref 類(lèi)型算撮,而實(shí)際傳入的實(shí)參不是生宛,就會(huì)出現(xiàn)問(wèn)題,所以這里必須用擴(kuò)展的 byref 結(jié)構(gòu)钮惠。

第三點(diǎn)是這種方式無(wú)法處理 __block 修飾對(duì)象指針的情況。

不過(guò)這種方式貴在簡(jiǎn)潔七芭,無(wú)需考慮內(nèi)部每種變量類(lèi)型具體的布局方式素挽,就可以滿(mǎn)足大部分需要獲取 block 強(qiáng)引用對(duì)象的場(chǎng)景。

對(duì)象成員變量強(qiáng)引用

對(duì)象強(qiáng)引用成員變量的獲取相對(duì)來(lái)說(shuō)直接些狸驳,因?yàn)槊總€(gè)對(duì)象對(duì)應(yīng)的類(lèi)中都有其成員變量的布局信息预明,并且 runtime 有現(xiàn)成的接口,只需要分析出編碼格式耙箍,然后按順序和成員變量匹配即可撰糠。獲取編碼信息的接口有兩個(gè), class_getIvarLayout 函數(shù)返回描述 strong ivar 數(shù)量和索引信的編碼信息辩昆,相對(duì)的 class_getWeakIvarLayout 函數(shù)返回描述 weak ivar 的編碼信息阅酪,這里基于前者進(jìn)行分析。

class_getIvarLayout 返回值是一個(gè) uint8 指針汁针,指向一個(gè)字符串术辐,uint8 在 16 進(jìn)制下占用 2 位,所以編碼以 2 位為一組施无,組內(nèi)首位描述非 strong ivar 個(gè)數(shù)辉词,次位為 strong ivar 個(gè)數(shù),最后一組如果 strong ivar 個(gè)數(shù)為 0猾骡,則忽略瑞躺,且 layout 以 0x00 結(jié)尾。下面舉幾個(gè)例子 :

// 0x0100

@interfaceA:NSObject{

__strongNSObject*s1;

}

@end

起始非 strong ivar 個(gè)數(shù)為 0兴想,并且接著一個(gè) strong ivar 幢哨,得出編碼為 0x01 。

// 0x0100

@interfaceA:NSObject{

__strongNSObject*s1;

__weakNSObject*w1;

}

@end

起始非 strong ivar 個(gè)數(shù)為 0嫂便,并且接著一個(gè) strong ivar 嘱么,得出編碼為 0x01,接著有個(gè) weak ivar,但是后面沒(méi)有 strong ivar 了曼振,所以忽略几迄。

// 0x011100

@interfaceA:NSObject{

__strongNSObject*s1;

__weakNSObject*w1;

__strongNSObject*s2;

}

@end

起始非 strong ivar 個(gè)數(shù)為 0,并且接著一個(gè) strong ivar 冰评,得出編碼為 0x01映胁,接著有個(gè) weak ivar,并且后面緊接著一個(gè) strong ivar 甲雅,得出編碼 0x11 解孙,合并得到 0x0111 。

// 0x211100

@interfaceA:NSObject{

inti1;

void*p1;

__strongNSObject*s1;

__weakNSObject*w1;

__strongNSObject*s2;

}

@end

起始非 strong ivar 個(gè)數(shù)為 2抛人,并且緊接著一個(gè) strong ivar弛姜,得出編碼 0x21,接著有個(gè) weak ivar妖枚,后面緊接著一個(gè) strong ivar 廷臼,得出編碼 0x11 ,合并得到 0x2111 绝页。

了解了成員變量的編碼格式荠商,剩下的就是如何解碼并依次和成員變量進(jìn)行匹配了,F(xiàn)BRetainCycleDetector 已經(jīng)實(shí)現(xiàn)了這部分功能 续誉,主要原理如下 :

獲取所有的成員變量以及 ivar 編碼

解析 ivar 編碼莱没,跳過(guò)非 strong ivar ,獲得 strong ivar 所在索引值 (把對(duì)象分成若干個(gè) 8 字節(jié)內(nèi)存片段)

利用 ivar_getOffset 函數(shù)獲取 ivar 的偏移量酷鸦,除以指針大小就是自身的索引值 (對(duì)象布局對(duì)齊基準(zhǔn)為尋址長(zhǎng)度饰躲,這里為 8 字節(jié))

匹配 2、3 步獲得的索引值臼隔,得到 strong ivar

當(dāng)然 FBRetainCycleDetector 還實(shí)現(xiàn)了對(duì)結(jié)構(gòu)體的處理属铁,這塊就不細(xì)究了。

小結(jié)

以上是我認(rèn)為檢測(cè)循環(huán)引用兩個(gè)比較關(guān)鍵的點(diǎn)躬翁,特別是獲取 block 捕獲的強(qiáng)引用對(duì)象環(huán)節(jié)焦蘑,block ABI 中并沒(méi)有詳細(xì)說(shuō)明捕獲區(qū)域布局信息,需要自己結(jié)合 block 源碼以及 clang 生成 block 的 CodeGen 邏輯去推測(cè)實(shí)際的布局信息盒发,所以得出的結(jié)論不一定正確例嘱,也歡迎感興趣的同學(xué)和我交流。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末宁舰,一起剝皮案震驚了整個(gè)濱河市拼卵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蛮艰,老刑警劉巖腋腮,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡即寡,警方通過(guò)查閱死者的電腦和手機(jī)徊哑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)聪富,“玉大人莺丑,你說(shuō)我怎么就攤上這事《章” “怎么了梢莽?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)奸披。 經(jīng)常有香客問(wèn)我昏名,道長(zhǎng),這世上最難降的妖魔是什么阵面? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任轻局,我火速辦了婚禮,結(jié)果婚禮上膜钓,老公的妹妹穿的比我還像新娘嗽交。我一直安慰自己卿嘲,他們只是感情好颂斜,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著拾枣,像睡著了一般沃疮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上梅肤,一...
    開(kāi)封第一講書(shū)人閱讀 51,521評(píng)論 1 304
  • 那天司蔬,我揣著相機(jī)與錄音,去河邊找鬼姨蝴。 笑死俊啼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的左医。 我是一名探鬼主播授帕,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼浮梢!你這毒婦竟也來(lái)了跛十?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤秕硝,失蹤者是張志新(化名)和其女友劉穎芥映,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奈偏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年坞嘀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片霎苗。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡姆吭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出唁盏,到底是詐尸還是另有隱情内狸,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布厘擂,位于F島的核電站昆淡,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏刽严。R本人自食惡果不足惜昂灵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望舞萄。 院中可真熱鬧眨补,春花似錦、人聲如沸倒脓。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)崎弃。三九已至甘晤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饲做,已是汗流浹背线婚。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盆均,地道東北人塞弊。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像泪姨,于是被迫代替她去往敵國(guó)和親游沿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容