聊聊 Objective-C 循环引用的检测

2021-01-15 网络
浏览
[科技新闻]聊聊 Objective-C 循环引用的检测

原题目:聊聊 Objective-C 循环引用的检测

作者 | triplecc

泉源 | triplecc's blog

https://triplecc.github.io/2019/08/15/聊聊循环引用的检测/

Objective-C 使用引用计数作为 iPhone 应用的内存治理方案,引用计数相比 GC 更适用于内存不太丰裕的场景,只需要网络与工具关联的局部信息来决议是否接纳工具,而 GC 为了明确可达性,需要全局的工具信息。引用计数固然有其优越性,但也正是由于缺乏对全局工具信息的把控,导致 Objective-C 无法自动销毁陷入循环引用的工具。虽然 Objective-C 通过引入弱引用手艺,让开发者可以尽可能地规避这个问题,但在引用层级过深,引用路径不那么直观的情形下,纵然是经验丰富的工程师,也无法百分百保证产出的代码不存在循环引用。

这时候就需要有一种检测方案,可以实时检测工具之间是否发生了循环引用,来辅助开发者实时地修正代码中存在的内存泄露问题。要想检测出循环引用,最直观的方式是递归地获取工具强引用的其他工具,并判断检测工具是否被其路径上的工具强引用了,也就是在有向图中去找环。明确检测方式之后,接下来需要解决的是若何获取强引用链,也就是获取工具的强引用,尤其是最容易造成循环引用的 block。

Block 捕捉实体引用

往期关于 Block 的文章 对 Block 的一点弥补、用 Block 实现委托方式、Block技巧与底层剖析

捕捉区域结构初探

首先凭据 block 的界说结构,可以简朴地将其视为:

structsr_block_layout {

void*isa;

intflags;

intreserved;

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

structsr_block_deor *deor;

/* Imported variables. */

};

// 标志位不一样,这个结构的现实结构也会有差异,这里简朴地放在一起好阅读

structsr_block_deor {

unsignedlongreserved; // Block_deor_1

unsignedlongsize; // Block_deor_1

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

void(*dispose)( void*); // Block_deor_2

constchar*signature; // Block_deor_3 BLOCK_HAS_SIGNATURE

constchar*layout; // Block_deor_3 contents depend on BLOCK_HAS_EXTENDED_LAYOUT

};

可以看到 block 捕捉的变量都市存储在 sr_block_layout 结构体 deor 字段之后的内存空间中,下面我们通过 clang -rewrite-objc 重写如下代码语句 :

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 结构中新增了捕捉的 i 字段,即 sr_block_layout 结构体的 imported variables 部门,这种操作可以看作在 sr_block_layout 尾部界说了一个 0 长数组,可以凭据现实捕捉变量的巨细,给捕捉区域申请对应的内存空间,只不外这一操作由编译器完成 :

structsr_block_layout {

void*isa;

intflags;

intreserved;

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

structsr_block_deor *deor;

charcaptured[ 0];

};

既然已经知道了捕捉变量 i 的存放地址,那么我们就可以通过 *(int *)layout->captured 在运行时获取 i 的值。获得了捕捉区域的起始地址之后,我们再来看捕捉区域的结构问题,思量以下代码块 :

inti = 2;

NSObject*o = [ NSObjectnew];

void(^blk)( void) = ^{

i;

o;

};

捕捉区域的结构分两部门看:顺序和巨细,我们先使用老方式重写代码块 :

struct__main_block_impl_0 {

struct__block_impl impl; // 24

struct__main_block_desc_0* Desc; // 8 指针占用内存巨细和寻址长度相关,在 64 位机环境下,编译器分配空间巨细为 8 字节

inti; // 8

NSObject*o; // 8

...

};

根据现在 clang 针对 64 位机的默认对齐方式(下文的字节对齐盘算都基于此条件条件),可以盘算出这个结构体占用的内存空间巨细为 24 8 8 8 = 48字节,而且根据上方代码块先 i 后 o 的捕捉排序方式,若是我要接见捕捉的 o 工具指针变量,只需要在捕捉区域起始地址上偏移 8 字节即可,我们可以借助 lldb 的 memory read (x) 下令查看这部门内存空间 :

(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->deor->size

( unsignedlong) $ 11= 44

(lldb) x/ 44bx layout

0x100740cf0: 0x700x210x7b0xa60xff0x7f0x000x00

0x100740cf8: 0x020x000x000xc30x000x000x000x00

0x100740d00: 0x400x1d0x000x000x010x000x000x00

0x100740d08: 0xb00x200x000x000x010x000x000x00

0x100740d10: 0x900xf20x730x000x010x000x000x00

0x100740d18: 0x020x000x000x00

和使用 clang -rewrite-objc 重写时的料想不一样,我们可以从以上终端日志中看出以下两点 :

  • 捕捉变量 i、o 在捕捉区域的排序方式为 o、i,o 变量地址与捕捉起始地址一致,i 变量地址为捕捉起始地址加上 8 字节
  • 捕捉整形变量 i 在内存中现实占用空间巨细为 4 字节

那么 block 到底是怎么对捕捉变量举行排序,而且为其分配内存空间的呢?这就需要看 clang 是若那边置 block 捕捉的外部变量了。

捕捉区域结构剖析

首先解决捕捉变量排序的问题,凭据 clang 针对这部门的排序代码,我们可以知道,在对齐字节数 (alignment) 不相等时,捕捉的实体根据 alignment 降序排序 (C 结构体对照特殊,纵然整体占用空间比指针变量大,也排在工具指针后面),否则根据以下类型举行排序 :

  • __strong 修饰工具指针变量
  • __block 修饰工具指针变量
  • __weak 修饰工具指针变量
  • 其他变量

再连系 clang 对捕捉变量对齐子节数盘算方式 ,我们可以知道,block 捕捉区域变量的对齐效果趋向于被 __attribute__ ((__packed__)) 修饰了的结构体,举个例子 :

structfoo {

void*p; // 8

inti; // 4

charc; // 4 现实用到的内存巨细为 1

};

建立 foo 结构体需要分配的空间巨细为 8 4 4 = 16,关于结构体的内存对齐方式,这里分外说几句,编译器会根据成员列表的顺序一个接一个地给每个成员分配内存,只有当存储成员需要知足准确的界限对齐要求时,成员之间才可能泛起用于填充的分外内存空间,以提升盘算机的接见速率(对齐尺度一样平常和寻址长度一致),在声明结构体时,让那些对齐界限要求最严酷的成员最先泛起,对界限要求最弱的成员最后泛起,可以最大限度地削减因界限对齐而带来的空间损失。再看以下代码块 :

structfoo {

void*p; // 8

inti; // 4

charc; // 1

} __attribute__ ((__packed__));

__attribute__ ((__packed__)) 编译属性告诉编译器,根据字段的现实占用子节数举行对齐,以是建立 foo 结构体需要分配的空间巨细为 8 4 1 = 13。

连系以上两点,我们可以实验剖析以下 block 捕捉区域的变量结构情形 :

NSObject*o1 = [ NSObjectnew];

__ weakNSObject*o2 = o1;

__block NSObject*o3 = o1;

unsignedlonglongj = 4;

inti = 3;

charc = 'a';

void(^blk)( void) = ^{

i;

c;

o1;

o2;

o3;

j;

};

首先根据 aligment 排序,可以获得排序顺序为 [o1 o2 o3] j i c,再凭据 strong、block、__weak 修饰符对 o1 o2 o3 举行排序,可获得最终效果 o1[8] o3[8] o2[8] j[8] i[4] c[1]。同样的,我们使用 lldb 的 x 下令验证剖析效果是否准确 :

(lldb) x/69bx layout

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

0x10200d948: 0x02 0x000x00 0xc 3 0x00 0x 00 0x00 0x00

0x10200d950: 0xf0 0x1b 0x00 0x000x01 0x000x00 0x00

0x10200d958: 0xf 8 0x20 0x 00 0x00 0x 01 0x00 0x00 0x00

0x10200d960: 0xa0 0xf 6 0x00 0x 02 0x01 0x 00 0x00 0x00 // o1

0x10200d968: 0x90 0xd 9 0x00 0x 02 0x01 0x 00 0x00 0x00 // o3

0x10200d970: 0xa0 0xf 6 0x00 0x 02 0x01 0x 00 0x00 0x00 // o2

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

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

(lldb) p o1

(NSObject *) $1 = 0x000000010200f6a0

Deor 的 Layout 信息

经由上述的一系列剖析,捕捉区域变量的结构方式已经大致摸清了,接下往返过头看下 sr_block_deor 结构的 layout 字段是用来干嘛的。从字面上明白,这个字段很可能保留了 block 某一部门的内存结构信息,好比捕捉区域的结构信息,我们依旧使用上文的最后一个例子,看看 layout 的值 :

(lldb) p layout->deor->layout

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

可以看到 layout 值为空字符串,并没有展示出任何直观的结构信息,看来要想知道 layout 是怎么运作的,还需要阅读这一部门的 block 代码 和 clang 代码,我们一步步地剖析这两段代码内里隐藏的信息,这里贴出其中的部门代码和注释 :

// block

// Extended layout encoding.

// Values for Block_deor_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 这个词,Objective-C 中有一种叫做 Tagged Pointer 的手艺,它让指针保留现实值,而不是保留现实值的地址,这里的 inline 也是相同的效果,即让 layout 指针保留现实的编码信息。在 inline 状态下,使用十六进制中的一位示意捕捉变量的数目,以是每种类型的变量最多只能有 15 个,此时的 layout 的值以 0xXYZ 形式出现,其中 X、Y、Z 划分示意捕捉 strong、block、__weak 修饰指针变量的个数,若是其中某个类型的数目跨越 15 或者捕捉变量的修饰类型不为这三种任何一个时,好比捕捉的变量由 __unsafe_unretained 修饰,则接纳另一种编码方式,这种方式下,layout 会指向一个字符串,这个字符串的每个字节以 0xPN 的形式出现,并以 0x00 竣事,P 示意变量类型,N 示意变量个数,需要注重的是,N 为 0 示意 P 类型有一个,而不是 0 个,也就是说现实的变量个数比 N 大 1。需要注重的是,捕捉 int 等基础类型,不影响 layout 的出现方式,layout 编码中也不会有关于基础类型的信息,除非需要基础类型的编码来辅助定位工具指针类型的位置,好比捕捉含有工具指针字段的结构体。举几个例子 :

unsignedlonglongj = 4;

inti = 3;

charc = 'a';

void(^blk)( void) = ^{

i;

c;

j;

};

以上代码块没有捕捉任何工具指针,以是现实的 deor 不包罗 copy 和 dispose 字段,去除这两个字段后,再输泛起实的结构信息,效果为空(0x00 示意竣事),说明捕捉一样平常基础类型变量不会计入现实的 layout 编码 :

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

( long) $ 0= 0x0000000100001f67

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

0x100001f67: 0x000x760x310x360x400x300x3a0x38

接着实验第一种 layout 方式 :

NSObject*o1 = [ NSObjectnew];

__block NSObject*o3 = o1;

__ weakNSObject*o2 = o1;

void(^blk)( void) = ^{

o1;

o2;

o3;

};

以上代码块对应的 layout 值为 0x111 ,示意三种类型变量每种一个 :

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

(long) $0= 0x0000000000000111

再实验第二种 layout 编码方式 :

NSObject*o1 = [ NSObjectnew];

__block NSObject*o3 = o1;

__ weakNSObject*o2 = o1;

NSObject*o4 = o1;

... // 5 - 18

NSObject*o19 = o1;

void(^blk)( void) = ^{

o1;

o2;

o3;

o4;

... // 5 - 18

o19;

};

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

( long) $ 0= 0x0000000100002f44

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

0x100002f44: 0x3f0x300x400x500x000x760x310x36

结构体对捕捉结构的影响

由于结构体字段的结构顺序在声明时就已经确定了,无法像 block 组织捕捉区域一样,根据变量类型、修饰符举行调整,以是若是结构体中有类型为工具指针的字段,就需要一些分外信息来盘算这些工具指针字段的偏移量,需要注重的是,被捕捉结构体的内存对齐信息和未捕捉时一致,以寻址长度作为对齐基准,捕捉操作并不会调换对齐信息。同样地,我们先实验捕捉只有基本类型字段的结构体 :

structS {

charc;

inti;

longj;

} foo;

void(^blk)( void) = ^{

foo;

};

然后调整 deor 结构,输出 layout :

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

0x100001f67: 0x000x760x310x360x400x300x3a0x38

可以看到,只有含有基本类型的结构体,同样不会影响 block 的 layout 编码信息。接下来我们给结构体新增 __strong 和 __weak 修饰的工具指针字段 :

structS {

charc;

inti;

__ strongNSObject*o1;

longj;

__ weakNSObject*o2;

} foo;

void(^blk)( void) = ^{

foo;

};

同样剖析输出 layout :

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

0x100002f47: 0x200x300x200x500x000x760x310x36

layout 编码为0x20 0x30 0x20 0x50 0x00,其中 P 为 2 示意 word 字类型(非工具),由于字巨细一样平常和指针一致,以是这里示意占用了 8 * (N 1) 个字节,第一个 0x20 示意非工具指针类型占用了 8 个字节,也就是 char 类型和 int 类型字段对齐之后所占用的空间,接着 0x30 示意有一个 __strong 修饰的工具指针字段,第二个 0x20 示意非工具指针 long 类型占用了 8 个字节,最后的 0x50 示意有一个 __weak 修饰的工具指针字段。由于编码中包罗了每个字段的排序和巨细,我们就可以通过剖析 layout 编码后的偏移量,拿到想要的工具指针值。P 另有个 byte 类型,值为 1 ,和 word 类型有相似的功效,只是示意的空间巨细差别。

Byref 结构的结构

由 __block 修饰的捕捉变量,会先转换成 byref 结构,再由这个结构去持有现实的捕捉变量,block 只卖力治理 byref 结构。

// 标志位不一样,这个结构的现实结构也会有差异,这里简朴地放在一起好阅读

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 对应的结构体。第一眼看上去,我对照疑心为什么还要有 layout 字段,虽然上文的 block 源码注释说明晰 byref 和 block 结构一样,都具备两种差别的结构编码方式,然则 byref 不是只针对一个变量么,岂非和 block 捕捉区域一样也可以携带多个捕捉变量?带着这个疑心,我们先看下以下表达式 :

__block NSObject*o1 = [ NSObjectnew];

使用 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 携带的变量也是保留在结构体尾部的内存空间里,当前上下文中,可以直接通过 sr_block_byref 的 layout 字段获取 o1 工具指针值。可以看到,在包装如工具指针这类通例变量时,layout 字段并没有起到实质性的作用,那什么条件下的 layout 才示意结构编码信息呢?若是使用 layout 字段示意编码信息,那么携带的变量又是那边安放的呢?我们一个个解答。

针对第一个问题,先看以下代码块 :

__block structS {

NSObject*o1;

} foo;

foo .o1= [ NSObjectnew];

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*);

structS foo;

};

和通例类型一样,foo 结构体保留在结构体尾部,也就是原本 layout 所在的字段,重写的代码中依然看不到 layout 的踪影,接着我们试着输出 foo :

(lldb) po foo.o1

<NSObject: 0x10061f130>

(lldb) p ( structS)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: 0x000x000x000x000x000x000x000x00

0x100627c28: 0x200x7c0x620x000x010x000x000x00

0x100627c30: 0x040x000x000x130x380x000x000x00

0x100627c38: 0x900x1b0x000x000x010x000x000x00

0x100627c40: 0x000x1c0x000x000x010x000x000x00

0x100627c48: 0x000x010x000x000x000x000x000x00

0x100627c50: 0x300xf10x610x000x010x000x000x00

看来事情并没有看上去的那么简朴,首先重写代码中 foo 字段所在内存保留的并不是结构体,而是 0x0000000000000100,这个 100 是不是看着有点眼熟,没错,这就是 byref 的 layout 信息,凭据 0xXYZ 编码规则,这个值示意有 1 个 __strong 修饰的工具指针。接着针对第二个问题,携带的工具指针变量存在哪,我们把视线往下移动 8 个字节,这不就是 foo.o1 工具指针的值么。总结下,在存在 layout 的情形下,byref 使用 8 个字节保留 layout 编码信息,并紧跟着在 layout 字段后存储捕捉的变量。

以上是 byref 的第一种 layout 编码方式,我们再实验第二种 :

__block structS {

charc;

NSObject*o1;

__ weakNSObject*o3;

} foo;

foo .o1= [ NSObjectnew];

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, intflags= 0) {

impl .isa= &_ NSConcreteStackBlock;

impl .Flags= flags;

impl .FuncPtr= fp;

Desc = desc;

}

};

emmmm …,上面代码并不是粘贴错误,貌似 Rewriter 并不能很好地处置这种情形,看来又需要我们直接去看对应内存地址中的值了 :

(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

强引用工具的获取

现在我们已经知道了 block / byref 若何结构捕捉区域内存,以及若何获取要害的结构信息,接下来我们就可以实验获取 block 强引用的工具了,这里我把强引用的工具分成两部门 :

  • 被 block 强引用
  • 被 byref 结构强引用

只要获取这两部门强引用的工具,义务就算完成了,由于上文已经将整个原理脉络理清了,以是编写出可用的代码并不难题。这两部门都涉及到结构编码,我们先凭据 layout 的编码方式,剖析出捕捉变量的类型和数目 :

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 inttype = (*layout & 0xf0) >> 4;

unsigned intcount = (*layout & 0xf) 1;

[info addItemWithType:type count:count];

layout ;

}

}

然后遍历 block 的结构编码信息,凭据变量类型和数目,盘算出工具指针地址偏移,然后获取对应的工具指针值 :

- ( 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 *item in_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 结构区域中的 byref 结构需要举行分外的处置,若是 byref 直接携带 __strong 修饰的变量,则不需要体贴 layout 编码,直接从结构尾部获取指针变量值即可,否则需要和处置 block 结构区域一样,先获得结构信息,然后遍历这些结构信息,盘算偏移量,获取强引用工具地址 :

- ( 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 = (__bridge id_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 *item ininfo .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,代码并没有举行过很严酷的测试,可能存在一些未处置的界限条件,需要实验 / 讨论的同砚可自取。

另一种强引用工具获取方式

上文通过将 block 的结构编码信息转化为对应字段的偏移量来获取强引用工具,这一节先容另外一种对照取巧的方式,也是现在检测循环引用工具获取 block 强引用工具的常用方式,好比 facebook 的 FBRetainCycleDetector 。凭据这块功效对应的源码,此方式大致原理如下 :

  • 获取 block 的 dispose 函数 (若是捕捉了强引用工具,需要行使这个函数解引用)
  • 组织一个 fake 工具,此工具由若干个扩展的 byref 结构 (工具) 组成,其个数由 block size 决议,即把 block 划分为若干个 8 字节内存区域,就像以下代码块一样 :
struct S {

NSObject *o1;

NSObject *o2;

};

struct S s = {

.o2 = [NSObject new]

};

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

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

  • 扩展的 byref 结构会重写 release 方式,只在此方式中设置强引用标识位,不执行原释放逻辑
  • 将 fake 工具作为参数,挪用 dispose 函数,dispose 函数会去 release 每个 block 强引用的工具,在这里这些强引用工具被替换成了我们的 byref 结构,以是我们可以通过它的强引用标识位判断 block 的哪块区域保留了强引用工具地址
  • 遍历 fake 工具,保留所有强引用标志位被设置的 byref 结构对应索引,后面通过这个索引可以去 block 中找强引用指针地址
  • 释放所有的 byref 结构
  • 凭据上面获得的索引,获取捕捉变量偏移量,偏移量为索引值 * 8 字节 (指针巨细) ,再凭据偏移量去 block 内存块中拿强引用工具地址

关于这种方案,我们需要明确下面几个点。

首先这种方案也需要在明确 block 内存结构的情形下才能够实行,由于 block ,或者说 block 结构体,现实执行内存对齐时,并没有根据寻址巨细也就是 8 字节对齐,假设 block 捕捉区域的对齐方式变成了这样 :

struct__main_block_impl_0 {

struct__block_impl impl; // 24

struct__main_block_desc_0* Desc; // 8 指针占用内存巨细和寻址长度相关,在 64 位机环境下,编译器分配空间巨细为 8 字节

inti; // 4 FakedByref 8

NSObject*o1; // 8 FakedByref 8 [这里上个 FakedByref 后 4 个子节和当前 FakedByref 前 4 字节笼罩 o1 工具指针的 8 字节,导致 miss ]

charc; // 1

NSObject*o2; // 8

}

那么使用 fake 的方案就会失效,由于这种方案的条件是 block 内存对齐基准基于寻址长度,即指针巨细。不外 block 对捕捉的变量根据类型和尺寸举行了排序,__strong 修饰的工具指针都在前面,原本我们只需要这种类型的变量,并不体贴其他类型,以是纵然后面的对齐方式不知足 fake 条件也没关系,另外捕捉结构体的对齐基准是基于寻址长度的,纵然结构体有其他类型,也知足 fake 条件 :

struct__main_block_impl_0 {

struct__block_impl impl; // 24

struct__main_block_desc_0* Desc; // 8 指针占用内存巨细和寻址长度相关,在 64 位机环境下,编译器分配空间巨细为 8 字节

NSObject*o1; // 8 FakedByref 8

NSObject*o2; // 8 FakedByref 8

inti; // 4 FakedByref 8

charc; // 1

}

可以看到,通过以上代码块的排序,让 o1 和 o2 都被 FakedByref 结构笼罩到了,而 i, c 变量自己就不会在 dispose 函数中接见,以是怎么设置都不会影响到计谋的生效。

第二点是为什么要用扩展的 byref 结构,而不是随便整个重写了 release 的类过来,这是由于当 block 捕捉了 __block 修饰的指针变量时,会将这个指针变量包装成 byref 结构,而 dispose 函数会对这个 byref 结构执行 Blockobject_dispose 操作,这个函数有两个形参,一个是工具指针,一个是 flag ,当 flag 指明工具指针为 byref 类型,而现实传入的实参不是,就会泛起问题,以是这里必须用扩展的 byref 结构。

第三点是这种方式无法处置 __block 修饰工具指针的情形。

不外这种方式贵在简练,无需思量内部每种变量类型详细的结构方式,就可以知足大部门需要获取 block 强引用工具的场景。

工具成员变量强引用

工具强引用成员变量的获取相对来说直接些,由于每个工具对应的类中都有其成员变量的结构信息,而且 runtime 有现成的接口,只需要剖析出编码花样,然后按顺序和成员变量匹配即可。获取编码信息的接口有两个, class_getIvarLayout 函数返回形貌 strong ivar 数目和索引信的编码信息,相对的 class_getWeakIvarLayout 函数返回形貌 weak ivar 的编码信息,这里基于前者举行剖析。

class_getIvarLayout 返回值是一个 uint8 指针,指向一个字符串,uint8 在 16 进制下占用 2 位,以是编码以 2 位为一组,组内首位形貌非 strong ivar 个数,次位为 strong ivar 个数,最后一组若是 strong ivar 个数为 0,则忽略,且 layout 以 0x00 末端。下面举几个例子 :

// 0x0100

@interface A : NSObject {

__ strongNSObject*s1;

}

@end

起始非 strong ivar 个数为 0,而且接着一个 strong ivar ,得出编码为 0x01 。

// 0x0100

@interface A : NSObject {

__ strongNSObject*s1;

__ weakNSObject*w1;

}

@end

起始非 strong ivar 个数为 0,而且接着一个 strong ivar ,得出编码为 0x01,接着有个 weak ivar,然则后面没有 strong ivar 了,以是忽略。

// 0x011100

@interface A : NSObject {

__ strongNSObject*s1;

__ weakNSObject*w1;

__ strongNSObject*s2;

}

@end

起始非 strong ivar 个数为 0,而且接着一个 strong ivar ,得出编码为 0x01,接着有个 weak ivar,而且后面紧接着一个 strong ivar ,得出编码 0x11 ,合并获得 0x0111 。

// 0x211100

@interface A : NSObject {

inti1;

void*p1;

__ strongNSObject*s1;

__ weakNSObject*w1;

__ strongNSObject*s2;

}

起始非 strong ivar 个数为 2,而且紧接着一个 strong ivar,得出编码 0x21,接着有个 weak ivar,后面紧接着一个 strong ivar ,得出编码 0x11 ,合并获得 0x2111 。

了解了成员变量的编码花样,剩下的就是若何解码并依次和成员变量举行匹配了,FBRetainCycleDetector 已经实现了这部门功效 ,主要原理如下 :

  • 获取所有的成员变量以及 ivar 编码
  • 剖析 ivar 编码,跳过非 strong ivar ,获得 strong ivar 所在索引值 (把工具分成若干个 8 字节内存片断)
  • 行使 ivar_getOffset 函数获取 ivar 的偏移量,除以指针巨细就是自身的索引值 (工具结构对齐基准为寻址长度,这里为 8 字节)
  • 匹配 2、3 步获得的索引值,获得 strong ivar
  • 固然 FBRetainCycleDetector 还实现了对结构体的处置,这块就不细究了。
小结

以上是我以为检测循环引用两个对照要害的点,特别是获取 block 捕捉的强引用工具环节,block ABI 中并没有详细说明捕捉区域结构信息,需要自己连系 block 源码以及 clang 天生 block 的 CodeGen 逻辑去推测现实的结构信息,以是得出的结论不一定准确,也迎接感兴趣的同砚和我交流。

参考

[1] Circle - a cycle collector for Objective-C ARC https://github.com/mikeash/Circle/blob/master/Circle/CircleIVarLayout.m

[2]FBRetainCycleDetector https://github.com/facebook/FBRetainCycleDetector

[3]Automatic memory leak detection on iOS https://code.fb.com/ios/automatic-memory-leak-detection-on-ios/

憧憬太空的马斯克,为什么没去造「更先进」的火箭?

气尖发动机通过结构的改变调整了燃气喷出后的膨胀状态,使其「自适应」不同高度的环境背压(阻力),与同一量级的钟形喷管发动机相比,气尖喷管使得发动机产生的推力被最大化利用,携带更少的推进剂意味着会降低火箭发…