日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

iOS之深入解析如何检测“循环引用”

發(fā)布時(shí)間:2024/5/28 编程问答 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 iOS之深入解析如何检测“循环引用” 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

一、前言

  • Objective-C 使用引用計(jì)數(shù)作為 iPhone 應(yīng)用的內(nèi)存管理方案,引用計(jì)數(shù)相比 GC 更適用于內(nèi)存不太充裕的場(chǎng)景,只需要收集與對(duì)象關(guān)聯(lián)的局部信息來決定是否回收對(duì)象,而 GC 為了明確可達(dá)性,需要全局的對(duì)象信息。引用計(jì)數(shù)固然有其優(yōu)越性,但也正是因?yàn)槿狈?duì)全局對(duì)象信息的把控,導(dǎo)致 Objective-C 無法自動(dòng)銷毀陷入循環(huán)引用的對(duì)象。雖然 Objective-C 通過引入弱引用技術(shù),讓開發(fā)者可以盡可能地規(guī)避這個(gè)問題,但在引用層級(jí)過深,引用路徑不那么直觀的情況下,即使是經(jīng)驗(yàn)豐富的工程師,也無法百分百保證產(chǎn)出的代碼不存在循環(huán)引用。
  • 這時(shí)候就需要有一種檢測(cè)方案,可以實(shí)時(shí)檢測(cè)對(duì)象之間是否發(fā)生了循環(huán)引用,來輔助開發(fā)者及時(shí)地修正代碼中存在的內(nèi)存泄漏問題。要想檢測(cè)出循環(huán)引用,最直觀的方式是遞歸地獲取對(duì)象強(qiáng)引用的其他對(duì)象,并判斷檢測(cè)對(duì)象是否被其路徑上的對(duì)象強(qiáng)引用了,也就是在有向圖中去找環(huán)。明確檢測(cè)方式之后,接下來需要解決的是如何獲取強(qiáng)引用鏈,也就是獲取對(duì)象的強(qiáng)引用,尤其是最容易造成循環(huán)引用的 block。

二、Block 捕獲實(shí)體引用

① 捕獲區(qū)域布局

  • 根據(jù) block 的定義結(jié)構(gòu),可以簡單地將其視為:
struct sr_block_layout {void *isa;int flags;int reserved;void (*invoke)(void *, ...);struct sr_block_descriptor *descriptor;/* Imported variables. */ };// 標(biāo)志位不一樣,這個(gè)結(jié)構(gòu)的實(shí)際布局也會(huì)有差別,這里簡單地放在一起好閱讀 struct sr_block_descriptor {unsigned long reserved; // Block_descriptor_1unsigned long size; // Block_descriptor_1void (*)(void *dst, void *src); // Block_descriptor_2 BLOCK_HAS_COPY_DISPOSEvoid (*dispose)(void *); // Block_descriptor_2const char *signature; // Block_descriptor_3 BLOCK_HAS_SIGNATUREconst char *layout; // Block_descriptor_3 contents depend on BLOCK_HAS_EXTENDED_LAYOUT };
  • 可以看到 block 捕獲的變量都會(huì)存儲(chǔ)在 sr_block_layout 結(jié)構(gòu)體 descriptor 字段之后的內(nèi)存空間中,通過 clang -rewrite-objc 重寫如下代碼語句:
int i = 2; ^{i; };
  • 可以得到 :
struct __block_impl {void *isa;int Flags;int Reserved;void *FuncPtr; };struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;int i;... };
  • __main_block_impl_0 結(jié)構(gòu)中新增了捕獲的 i 字段,即 sr_block_layout 結(jié)構(gòu)體的 imported variables 部分,這種操作可以看作在 sr_block_layout 尾部定義了一個(gè) 0 長數(shù)組,可以根據(jù)實(shí)際捕獲變量的大小,給捕獲區(qū)域申請(qǐng)對(duì)應(yīng)的內(nèi)存空間,只不過這一操作由編譯器完成:
struct sr_block_layout {void *isa;int flags;int reserved;void (*invoke)(void *, ...);struct sr_block_descriptor *descriptor;char captured[0]; };
  • 既然已經(jīng)知道捕獲變量 i 的存放地址,那么就可以通過 *(int *)layout->captured 在運(yùn)行時(shí)獲取 i 的值,得到捕獲區(qū)域的起始地址之后,再來看捕獲區(qū)域的布局問題,考慮以下代碼塊:
int i = 2; NSObject *o = [NSObject new]; void (^blk)(void) = ^{i;o; };
  • 捕獲區(qū)域的布局分兩部分看:順序和大小,先使用老方法重寫代碼塊:
struct __main_block_impl_0 {struct __block_impl impl; // 24struct __main_block_desc_0* Desc; // 8 指針占用內(nèi)存大小和尋址長度相關(guān),在 64 位機(jī)環(huán)境下,編譯器分配空間大小為 8 字節(jié)int i; // 8NSObject *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 的捕獲排序方式,如果要訪問捕獲的 o 對(duì)象指針變量,只需要在捕獲區(qū)域起始地址上偏移 8 字節(jié)即可,可以借助 lldb 的 memory read (x) 命令查看這部分內(nèi)存空間:
(lldb) po *(NSObject **)(layout->captured + 8) 0x0000000000000002 (lldb) po *(NSObject **)layout->captured <NSObject: 0x10073f290> (lldb) p *(int *)(layout->captured + 8) (int) $6 = 2 (lldb) p (int *)(layout->captured + 8) (int *) $9 = 0x0000000100740d18 (lldb) p layout->descriptor->size (unsigned long) $11 = 44 (lldb) x/44bx layout 0x100740cf0: 0x70 0x21 0x7b 0xa6 0xff 0x7f 0x00 0x00 0x100740cf8: 0x02 0x00 0x00 0xc3 0x00 0x00 0x00 0x00 0x100740d00: 0x40 0x1d 0x00 0x00 0x01 0x00 0x00 0x00 0x100740d08: 0xb0 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x100740d10: 0x90 0xf2 0x73 0x00 0x01 0x00 0x00 0x00 0x100740d18: 0x02 0x00 0x00 0x00
  • 和使用 clang -rewrite-objc 重寫時(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ū)域布局分析

  • 首先解決捕獲變量排序的問題,根據(jù) clang 針對(duì)這部分的排序代碼,可以知道,在對(duì)齊字節(jié)數(shù) (alignment) 不相等時(shí),捕獲的實(shí)體按照 alignment 降序排序 (C 結(jié)構(gòu)體比較特殊,即使整體占用空間比指針變量大,也排在對(duì)象指針后面),否則按照以下類型進(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è)例子:
struct foo {void *p; // 8int i; // 4char c; // 4 實(shí)際用到的內(nèi)存大小為 1 };
  • 創(chuàng)建 foo 結(jié)構(gòu)體需要分配的空間大小為 8 + 4 + 4 = 16,關(guān)于結(jié)構(gòu)體的內(nèi)存對(duì)齊方式,編譯器會(huì)按照成員列表的順序一個(gè)接一個(gè)地給每個(gè)成員分配內(nèi)存,只有當(dāng)存儲(chǔ)成員需要滿足正確的邊界對(duì)齊要求時(shí),成員之間才可能出現(xiàn)用于填充的額外內(nèi)存空間,以提升計(jì)算機(jī)的訪問速度(對(duì)齊標(biāo)準(zhǔn)一般和尋址長度一致),在聲明結(jié)構(gòu)體時(shí),讓那些對(duì)齊邊界要求最嚴(yán)格的成員最先出現(xiàn),對(duì)邊界要求最弱的成員最后出現(xiàn),可以最大限度地減少因邊界對(duì)齊而帶來的空間損失。再看以下代碼塊:
struct foo {void *p; // 8int i; // 4char c; // 1 } __attribute__ ((__packed__));
  • attribute ((packed)) 編譯屬性會(huì)告訴編譯器,按照字段的實(shí)際占用子節(jié)數(shù)進(jìn)行對(duì)齊,所以創(chuàng)建 foo 結(jié)構(gòu)體需要分配的空間大小為 8 + 4 + 1 = 13。
  • 結(jié)合以上兩點(diǎn),可以嘗試分析以下 block 捕獲區(qū)域的變量布局情況:
NSObject *o1 = [NSObject new]; __weak NSObject *o2 = o1; __block NSObject *o3 = o1; unsigned long long j = 4; int i = 3; char c = '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 0x21 0x7b 0xa6 0xff 0x7f 0x00 0x00 0x10200d948: 0x02 0x00 0x00 0xc3 0x00 0x00 0x00 0x00 0x10200d950: 0xf0 0x1b 0x00 0x00 0x01 0x00 0x00 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 0x00 0x00 0x00 0x00 0x00 0x00 0x00 // j 0x10200d980: 0x03 0x00 0x00 0x00 0x61 // 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)過上述的一系列分析,捕獲區(qū)域變量的布局方式已經(jīng)大致清楚,接下來回過頭看下 sr_block_descriptor 結(jié)構(gòu)的 layout 字段是用來干什么的?從字面上理解,這個(gè)字段很可能保存了 block 某一部分的內(nèi)存布局信息,比如捕獲區(qū)域的布局信息,依然使用上文的最后一個(gè)例子,看看 layout 的值:
(lldb) p layout->descriptor->layout (const char *) $2 = 0x0000000000000111 ""
  • 可以看到 layout 值為空字符串,并沒有展示出任何直觀的布局信息,看來要想知道 layout 是怎么運(yùn)作的,可以閱讀 block 代碼 和 clang 代碼,繼續(xù)一步步地分析這兩段代碼里面隱藏的信息,這里貼出其中的部分代碼和注釋:
// 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-objectsBLOCK_LAYOUT_NON_OBJECT_WORDS = 2, // N words non-objectsBLOCK_LAYOUT_STRONG = 3, // N words strong pointersBLOCK_LAYOUT_BYREF = 4, // N words byref pointersBLOCK_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ù)量,所以每種類型的變量最多只能有 15 個(gè),此時(shí)的 layout 的值以 0xXYZ 形式呈現(xiàn),其中 X、Y、Z 分別表示捕獲 __strong、__block、__weak 修飾指針變量的個(gè)數(shù),如果其中某個(gè)類型的數(shù)量超過 15 或者捕獲變量的修飾類型不為這三種任何一個(gè)時(shí),比如捕獲的變量由 __unsafe_unretained 修飾,則采用另一種編碼方式,這種方式下,layout 會(huì)指向一個(gè)字符串,這個(gè)字符串的每個(gè)字節(jié)以 0xPN 的形式呈現(xiàn),并以 0x00 結(jié)束,P 表示變量類型,N 表示變量個(gè)數(shù),需要注意的是,N 為 0 表示 P 類型有一個(gè),而不是 0 個(gè),也就是說實(shí)際的變量個(gè)數(shù)比 N 大 1。
  • 需要注意的是,捕獲 int 等基礎(chǔ)類型,不影響 layout 的呈現(xiàn)方式,layout 編碼中也不會(huì)有關(guān)于基礎(chǔ)類型的信息,除非需要基礎(chǔ)類型的編碼來輔助定位對(duì)象指針類型的位置,比如捕獲含有對(duì)象指針字段的結(jié)構(gòu)體。
  • 如下所示:代碼塊沒有捕獲任何對(duì)象指針,所以實(shí)際的 descriptor 不包含 copy 和 dispose 字段:
unsigned long long j = 4; int i = 3; char c = 'a'; void (^blk)(void) = ^{i;c;j; };
  • 去除這兩個(gè)字段后,再輸出實(shí)際的布局信息,結(jié)果為空(0x00 表示結(jié)束),說明捕獲一般基礎(chǔ)類型變量不會(huì)計(jì)入實(shí)際的 layout 編碼:
(lldb) p/x (long)layout->descriptor->layout (long) $0 = 0x0000000100001f67 (lldb) x/8bx layout->descriptor->layout 0x100001f67: 0x00 0x76 0x31 0x36 0x40 0x30 0x3a 0x38
  • 接著嘗試第一種 layout 方式:
NSObject *o1 = [NSObject new]; __block NSObject *o3 = o1; __weak NSObject *o2 = o1; void (^blk)(void) = ^{o1;o2;o3; };
  • 以上代碼塊對(duì)應(yīng)的 layout 值為 0x111,表示三種類型變量每種一個(gè):
(lldb) p/x (long)layout->descriptor->layout (long) $0 = 0x0000000000000111
  • 再嘗試第二種 layout 編碼方式:
NSObject *o1 = [NSObject new]; __block NSObject *o3 = o1; __weak NSObject *o2 = o1; NSObject *o4 = o1; ... // 5 - 18 NSObject *o19 = o1; void (^blk)(void) = ^{o1;o2;o3;o4;... // 5 - 18o19; };
  • 以上代碼塊對(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: 0x3f 0x30 0x40 0x50 0x00 0x76 0x31 0x36

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

  • 由于結(jié)構(gòu)體字段的布局順序在聲明時(shí)就已經(jīng)確定,無法像 block 構(gòu)造捕獲區(qū)域一樣,按照變量類型、修飾符進(jìn)行調(diào)整,所以如果結(jié)構(gòu)體中有類型為對(duì)象指針的字段,就需要一些額外信息來計(jì)算這些對(duì)象指針字段的偏移量,需要注意的是,被捕獲結(jié)構(gòu)體的內(nèi)存對(duì)齊信息和未捕獲時(shí)一致,以尋址長度作為對(duì)齊基準(zhǔn),捕獲操作并不會(huì)變更對(duì)齊信息。
  • 同樣地,先嘗試捕獲只有基本類型字段的結(jié)構(gòu)體:
struct S {char c;int i;long j; } foo; void (^blk)(void) = ^{foo; };
  • 然后調(diào)整 descriptor 結(jié)構(gòu),輸出 layout :
(lldb) x/8bx layout->descriptor->layout 0x100001f67: 0x00 0x76 0x31 0x36 0x40 0x30 0x3a 0x38
  • 可以看到,只有含有基本類型的結(jié)構(gòu)體,同樣不會(huì)影響 block 的 layout 編碼信息。給結(jié)構(gòu)體新增 __strong 和 __weak 修飾的對(duì)象指針字段:
struct S {char c;int i;__strong NSObject *o1;long j;__weak NSObject *o2; } foo; void (^blk)(void) = ^{foo; };
  • 同樣分析輸出 layout :
(lldb) x/8bx layout->descriptor->layout 0x100002f47: 0x20 0x30 0x20 0x50 0x00 0x76 0x31 0x36
  • layout 編碼為0x20 0x30 0x20 0x50 0x00,其中 P 為 2 表示 word 字類型(非對(duì)象),由于字大小一般和指針一致,所以表示占用 8 * (N + 1) 個(gè)字節(jié),第一個(gè) 0x20 表示非對(duì)象指針類型占用了 8 個(gè)字節(jié),也就是 char 類型和 int 類型字段對(duì)齊之后所占用的空間,接著 0x30 表示有一個(gè) __strong 修飾的對(duì)象指針字段,第二個(gè) 0x20 表示非對(duì)象指針 long 類型占用 8 個(gè)字節(jié),最后的 0x50 表示有一個(gè) __weak 修飾的對(duì)象指針字段。由于編碼中包含每個(gè)字段的排序和大小,就可以通過解析 layout 編碼后的偏移量,拿到想要的對(duì)象指針值。 P 還有個(gè) byte 類型,值為 1,和 word 類型有相似的功能,只是表示的空間大小不同。

⑤ 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ì)有差別,簡單地放在一起好閱讀 struct sr_block_byref {void *isa;struct sr_block_byref *forwarding;// contains ref countvolatile int32_t flags; uint32_t size;// requires BLOCK_BYREF_HAS_COPY_DISPOSEvoid (*byref_keep)(struct sr_block_byref *dst, struct sr_block_byref *src);void (*byref_destroy)(struct sr_block_byref *);// requires BLOCK_BYREF_LAYOUT_EXTENDEDconst char *layout; };
  • 以上代碼塊就是 byref 對(duì)應(yīng)的結(jié)構(gòu)體,第一眼看上去,比較困惑為什么還要有 layout 字段,雖然 block 源碼注釋說明 byref 和 block 結(jié)構(gòu)一樣,都具備兩種不同的布局編碼方式,但是 byref 不是只針對(duì)一個(gè)變量嗎,難道和 block 捕獲區(qū)域一樣也可以攜帶多個(gè)捕獲變量?帶著這個(gè)困惑,先看下以下表達(dá)式 :
__block NSObject *o1 = [NSObject new];
  • 使用 clang 重寫之后:
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)前上下文中,可以直接通過 sr_block_byref 的 layout 字段獲取 o1 對(duì)象指針值。可以看到,在包裝如對(duì)象指針這類常規(guī)變量時(shí),layout 字段并沒有起到實(shí)質(zhì)性的作用,那什么條件下的 layout 才表示布局編碼信息呢?如果使用 layout 字段表示編碼信息,那么攜帶的變量又是何處安放的呢?
  • 針對(duì)第一個(gè)問題,先看以下代碼塊 :
__block struct S {NSObject *o1; } foo; foo.o1 = [NSObject new]; void (^blk)(void) = ^{foo; };
  • 使用 clang 重寫之后:
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*);struct S foo; };
  • 和常規(guī)類型一樣,foo 結(jié)構(gòu)體保存在結(jié)構(gòu)體尾部,也就是原本 layout 所在的字段,重寫的代碼中依然看不到 layout 的蹤影,接著輸出 foo :
(lldb) po foo.o1 <NSObject: 0x10061f130> (lldb) p (struct S)a_byref->layout error: Multiple internal symbols found for 'S' (lldb) p/x (long)a_byref->layout (long) $3 = 0x0000000000000100 (lldb) x/56bx a_byref 0x100627c20: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x100627c28: 0x20 0x7c 0x62 0x00 0x01 0x00 0x00 0x00 0x100627c30: 0x04 0x00 0x00 0x13 0x38 0x00 0x00 0x00 0x100627c38: 0x90 0x1b 0x00 0x00 0x01 0x00 0x00 0x00 0x100627c40: 0x00 0x1c 0x00 0x00 0x01 0x00 0x00 0x00 0x100627c48: 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x100627c50: 0x30 0xf1 0x61 0x00 0x01 0x00 0x00 0x00
  • 看來事情并沒有看上去的那么簡單,首先重寫代碼中 foo 字段所在內(nèi)存保存的并不是結(jié)構(gòu)體,而是 0x0000000000000100,這個(gè) 100 是不是看著有點(diǎn)眼熟?沒錯(cuò),這就是 byref 的 layout 信息,根據(jù) 0xXYZ 編碼規(guī)則,這個(gè)值表示有 1 個(gè) __strong 修飾的對(duì)象指針。
  • 接著針對(duì)第二個(gè)問題,攜帶的對(duì)象指針變量存在哪?往下移動(dòng) 8 個(gè)字節(jié),這不就是 foo.o1 對(duì)象指針的值么?總結(jié)下,在存在 layout 的情況下,byref 使用 8 個(gè)字節(jié)保存 layout 編碼信息,并緊跟著在 layout 字段后存儲(chǔ)捕獲的變量。
  • 以上是 byref 的第一種 layout 編碼方式,再嘗試第二種:
__block struct S {char c;NSObject *o1;__weak NSObject *o3; } foo; foo.o1 = [NSObject new]; void (^blk)(void) = ^{foo; };
  • 使用 clang 重寫代碼之后 :
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, int flags=0) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;} };
  • 上面代碼并不是粘貼錯(cuò)誤,貌似 Rewriter 并不能很好地處理這種情況,看來又需要直接去看對(duì)應(yīng)內(nèi)存地址中的值:
(lldb) x/72bx a_byref 0x100755140: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x100755148: 0x40 0x51 0x75 0x00 0x01 0x00 0x00 0x00 0x100755150: 0x04 0x00 0x00 0x13 0x48 0x00 0x00 0x00 0x100755158: 0x10 0x1b 0x00 0x00 0x01 0x00 0x00 0x00 0x100755160: 0xa0 0x1b 0x00 0x00 0x01 0x00 0x00 0x00 0x100755168: 0x8d 0x3e 0x00 0x00 0x01 0x00 0x00 0x00 0x100755170: 0x00 0x5f 0x6b 0x65 0x79 0x00 0x00 0x00 0x100755178: 0xd0 0x6e 0x75 0x00 0x01 0x00 0x00 0x00 0x100755180: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 (lldb) x/8bx a_byref->layout 0x100003e8d: 0x20 0x30 0x50 0x00 0x53 0x52 0x4c 0x61
  • 地址 0x100755168 中保存 layout 編碼字符串的地址 0x0000000100003e8d ,將此字符串轉(zhuǎn)換成十六進(jìn)制后為 0x20 0x30 0x50 0x00。

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

  • 已經(jīng)知道 block / byref 如何布局捕獲區(qū)域內(nèi)存,以及如何獲取關(guān)鍵的布局信息,接下來就可以嘗試獲取 block 強(qiáng)引用的對(duì)象,強(qiáng)引用的對(duì)象可以分成兩部分:
    • 被 block 強(qiáng)引用;
    • 被 byref 結(jié)構(gòu)強(qiáng)引用。
  • 只要獲取這兩部分強(qiáng)引用的對(duì)象就可以了,由于上文已經(jīng)將整個(gè)原理脈絡(luò)理清,所以編寫出可用的代碼并不困難。這兩部分都涉及到布局編碼,先根據(jù) layout 的編碼方式,解析出捕獲變量的類型和數(shù)量:
SRCapturedLayoutInfo *info = [SRCapturedLayoutInfo new];if ((uintptr_t)layout < (1 << 12)) {uintptr_t inlineLayout = (uintptr_t)layout;[info addItemWithType:SR_BLOCK_LAYOUT_STRONG count:(inlineLayout & 0xf00) >> 8];[info addItemWithType:SR_BLOCK_LAYOUT_BYREF count:(inlineLayout & 0xf0) >> 4];[info addItemWithType:SR_BLOCK_LAYOUT_WEAK count:inlineLayout & 0xf]; } else {while (layout && *layout != '\x00') {unsigned int type = (*layout & 0xf0) >> 4;unsigned int count = (*layout & 0xf) + 1;[info addItemWithType:type count:count];layout++;} }
  • 然后遍歷 block 的布局編碼信息,根據(jù)變量類型和數(shù)量,計(jì)算出對(duì)象指針地址偏移,獲取對(duì)應(yīng)的對(duì)象指針值:
- (NSHashTable *)strongReferencesForBlockLayout:(void *)iLayout {if (!iLayout) return nil;struct sr_block_layout *aLayout = (struct sr_block_layout *)iLayout;const char *extenedLayout = sr_block_extended_layout(aLayout);_blockLayoutInfo = [SRCapturedLayoutInfo infoForLayoutEncode:extenedLayout];NSHashTable *references = [NSHashTable weakObjectsHashTable];uintptr_t *begin = (uintptr_t *)aLayout->captured;for (SRLayoutItem *item in _blockLayoutInfo.layoutItems) {switch (item.type) {case SR_BLOCK_LAYOUT_STRONG: {NSHashTable *objects = [item objectsForBeginAddress:begin];SRAddObjectsFromHashTable(references, objects);begin += item.count;} break;case SR_BLOCK_LAYOUT_BYREF: {for (int i = 0; i < item.count; i++, begin++) {struct sr_block_byref *aByref = *(struct sr_block_byref **)begin;NSHashTable *objects = [self strongReferenceForBlockByref:aByref];SRAddObjectsFromHashTable(references, objects);}} break;case SR_BLOCK_LAYOUT_NON_OBJECT_BYTES: {begin = (uintptr_t *)((uintptr_t)begin + item.count);} break;default: {begin += item.count;} break;}}return references; }
  • 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) return nil;struct sr_block_byref *aByref = (struct sr_block_byref *)iByref;NSHashTable *references = [NSHashTable weakObjectsHashTable];int32_t flag = aByref->flags & SR_BLOCK_BYREF_LAYOUT_MASK;switch (flag) {case SR_BLOCK_BYREF_LAYOUT_STRONG: {void **begin = sr_block_byref_captured(aByref);id object = (__bridge id _Nonnull)(*(void **)begin);if (object) [references addObject:object];} break;case SR_BLOCK_BYREF_LAYOUT_EXTENDED: {const char *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 *item in info.layoutItems) {switch (item.type) {case SR_BLOCK_LAYOUT_NON_OBJECT_BYTES: {begin = (uintptr_t *)((uintptr_t)begin + item.count);} break;case SR_BLOCK_LAYOUT_STRONG: {NSHashTable *objects = [item objectsForBeginAddress:begin];SRAddObjectsFromHashTable(references, objects);begin += item.count;} break;default: {begin += item.count;} break;}}} break;default: break;}return references; }

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

  • 上文通過將 block 的布局編碼信息轉(zhuǎn)化為對(duì)應(yīng)字段的偏移量來獲取強(qiáng)引用對(duì)象,還有另外一種比較取巧的方式,也是目前檢測(cè)循環(huán)引用工具獲取 block 強(qiáng)引用對(duì)象的常用方式,比如 facebook 的 FBRetainCycleDetector。
  • 根據(jù) FBRetainCycleDetector 對(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ū)域,就像以下代碼塊一樣 :
struct S {NSObject *o1;NSObject *o2; }; struct S s = {.o2 = [NSObject new] }; void **fake = (void **)&s; // fake[1] 和 s.o2 是一樣的
    • 擴(kuò)展的 byref 結(jié)構(gòu)會(huì)重寫 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),所以可以通過它的強(qiáng)引用標(biāo)識(shí)位判斷 block 的哪塊區(qū)域保存了強(qiáng)引用對(duì)象地址;
    • 遍歷 fake 對(duì)象,保存所有強(qiáng)引用標(biāo)志位被設(shè)置的 byref 結(jié)構(gòu)對(duì)應(yīng)索引,通過這個(gè)索引可以去 block 中找強(qiáng)引用指針地址;
    • 釋放所有的 byref 結(jié)構(gòu);
    • 根據(jù)上面得到的索引,獲取捕獲變量偏移量,偏移量為索引值 * 8 字節(jié) (指針大小) ,再根據(jù)偏移量去 block 內(nèi)存塊中拿強(qiáng)引用對(duì)象地址。
  • 關(guān)于這種方案,需要明確:
    • 首先這種方案也需要在明確 block 內(nèi)存布局的情況下才能夠?qū)嵤?#xff0c;因?yàn)?block ,或者說 block 結(jié)構(gòu)體,實(shí)際執(zhí)行內(nèi)存對(duì)齊時(shí),并沒有按照尋址大小也就是 8 字節(jié)對(duì)齊,假設(shè) block 捕獲區(qū)域的對(duì)齊方式變成如下的這樣 :
struct __main_block_impl_0 {struct __block_impl impl; // 24struct __main_block_desc_0* Desc; // 8 指針占用內(nèi)存大小和尋址長度相關(guān),在 64 位機(jī)環(huán)境下,編譯器分配空間大小為 8 字節(jié)int i; // 4 FakedByref 8NSObject *o1; // 8 FakedByref 8 [這里上個(gè) FakedByref 后 4 個(gè)子節(jié)和當(dāng)前 FakedByref 前 4 字節(jié)覆蓋 o1 對(duì)象指針的 8 字節(jié),導(dǎo)致 miss ]char c; // 1NSObject *o2; // 8 }
    • 那么使用 fake 的方案就會(huì)失效,因?yàn)檫@種方案的前提是 block 內(nèi)存對(duì)齊基準(zhǔn)基于尋址長度,即指針大小。不過 block 對(duì)捕獲的變量按照類型和尺寸進(jìn)行了排序,__strong 修飾的對(duì)象指針都在前面,本來只需要這種類型的變量,并不關(guān)心其它類型,所以即使后面的對(duì)齊方式不滿足 fake 條件也沒關(guān)系,另外捕獲結(jié)構(gòu)體的對(duì)齊基準(zhǔn)是基于尋址長度的,即使結(jié)構(gòu)體有其他類型,也滿足 fake 條件 :
struct __main_block_impl_0 {struct __block_impl impl; // 24struct __main_block_desc_0* Desc; // 8 指針占用內(nèi)存大小和尋址長度相關(guān),在 64 位機(jī)環(huán)境下,編譯器分配空間大小為 8 字節(jié)NSObject *o1; // 8 FakedByref 8NSObject *o2; // 8 FakedByref 8int i; // 4 FakedByref 8char c; // 1 }
    • 可以看到,通過以上代碼塊的排序,讓 o1 和 o2 都被 FakedByref 結(jié)構(gòu)覆蓋,而 i、c 變量本身就不會(huì)在 dispose 函數(shù)中訪問,因此怎么設(shè)置都不會(huì)影響到策略的生效;
    • 第二點(diǎn)是為什么要用擴(kuò)展的 byref 結(jié)構(gòu),而不是隨便整個(gè)重寫 release 的類,這是因?yàn)楫?dāng) block 捕獲了 __block 修飾的指針變量時(shí),會(huì)將這個(gè)指針變量包裝成 byref 結(jié)構(gòu),而 dispose 函數(shù)會(huì)對(duì)這個(gè) byref 結(jié)構(gòu)執(zhí)行 _Block_object_dispose 操作,這個(gè)函數(shù)有兩個(gè)形參,一個(gè)是對(duì)象指針,一個(gè)是 flag,當(dāng) flag 指明對(duì)象指針為 byref 類型,而實(shí)際傳入的實(shí)參不是,就會(huì)出現(xiàn)問題,所以必須用擴(kuò)展的 byref 結(jié)構(gòu);
    • 第三點(diǎn)是這種方式無法處理 __block 修飾對(duì)象指針的情況。
  • 不過這種方式貴在簡潔,無需考慮內(nèi)部每種變量類型具體的布局方式,就可以滿足大部分需要獲取 block 強(qiáng)引用對(duì)象的場(chǎng)景。

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

  • 對(duì)象強(qiáng)引用成員變量的獲取相對(duì)來說直接些,因?yàn)槊總€(gè)對(duì)象對(duì)應(yīng)的類中都有其成員變量的布局信息,并且 runtime 有現(xiàn)成的接口,只需要分析出編碼格式,然后按順序和成員變量匹配即可。獲取編碼信息的接口有兩個(gè), class_getIvarLayout 函數(shù)返回描述 strong ivar 數(shù)量和索引信的編碼信息,相對(duì)的 class_getWeakIvarLayout 函數(shù)返回描述 weak ivar 的編碼信息。
  • 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é)尾。
  • 如下所示:
// 0x0100 @interface A : NSObject {__strong NSObject *s1; } @end
  • 起始非 strong ivar 個(gè)數(shù)為 0,并且接著一個(gè) strong ivar ,得出編碼為 0x01 。
// 0x0100 @interface A : NSObject {__strong NSObject *s1;__weak NSObject *w1; } @end
  • 起始非 strong ivar 個(gè)數(shù)為 0,并且接著一個(gè) strong ivar ,得出編碼為 0x01,接著有個(gè) weak ivar,但是后面沒有 strong ivar,所以忽略。
// 0x011100 @interface A : NSObject {__strong NSObject *s1;__weak NSObject *w1;__strong NSObject *s2; } @end
  • 起始非 strong ivar 個(gè)數(shù)為 0,并且接著一個(gè) strong ivar ,得出編碼為 0x01,接著有個(gè) weak ivar,并且后面緊接著一個(gè) strong ivar ,得出編碼 0x11 ,合并得到 0x0111。
// 0x211100 @interface A : NSObject {int i1;void *p1;__strong NSObject *s1;__weak NSObject *w1;__strong NSObject *s2; } @end
  • 起始非 strong ivar 個(gè)數(shù)為 2,并且緊接著一個(gè) strong ivar,得出編碼 0x21,接著有個(gè) weak ivar,后面緊接著一個(gè) strong ivar ,得出編碼 0x11 ,合并得到 0x2111。
  • 了解了成員變量的編碼格式,剩下的就是如何解碼并依次和成員變量進(jìn)行匹配, FBRetainCycleDetector 已經(jīng)實(shí)現(xiàn)了這部分功能 ,主要原理如下:
    • 獲取所有的成員變量以及 ivar 編碼;
    • 解析 ivar 編碼,跳過非 strong ivar ,獲得 strong ivar 所在索引值 (把對(duì)象分成若干個(gè) 8 字節(jié)內(nèi)存片段);
    • 利用 ivar_getOffset 函數(shù)獲取 ivar 的偏移量,除以指針大小就是自身的索引值 (對(duì)象布局對(duì)齊基準(zhǔn)為尋址長度,這里為 8 字節(jié));
    • 匹配 2、3 步獲得的索引值,得到 strong ivar;
    • 實(shí)現(xiàn)了對(duì)結(jié)構(gòu)體的處理。

四、總結(jié)

  • “Block 捕獲實(shí)體引用”和“對(duì)象成員變量強(qiáng)引用”是檢測(cè)循環(huán)引用兩個(gè)比較關(guān)鍵的點(diǎn),特別是獲取 block 捕獲的強(qiáng)引用對(duì)象環(huán)節(jié),block ABI 中并沒有詳細(xì)說明捕獲區(qū)域布局信息,需要自己結(jié)合 block 源碼以及 clang 生成 block 的 CodeGen 邏輯去推測(cè)實(shí)際的布局信息。

總結(jié)

以上是生活随笔為你收集整理的iOS之深入解析如何检测“循环引用”的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。