Block 内存布局详解

1 内存布局

按照LLVM工程源码中的Block-ABI-Apple.rst描述,Block的内存布局如下:

struct Block {
    void *isa;
    int flags;
    int reserved;
    R(*invoke)(Block *, ...);
    struct Block_Descriptor {
        unsigned long int reserved;
        unsigned long int size;
        void(*copy_helper)(void *dst, void *src);
        void(*dispose_helper)(void *src);
    } *descriptor;
    // 被捕获的变量
    ...
}

isa表明Block也是一个OC对象,它的取值后面会说明。

flags是各种标志位,它的取值后面会说明。

reserved是保留字段,不赋值。

invoke是函数指针,指向Block要执行的函数。

descriptor是一个结构体指针,里面包含了Block的各种描述信息。

descriptor.reserved是保留字段,不会进行赋值。

descriptor.size是整个Block结构体的大小。

descriptor.copy_helperBlock的拷贝相关,这个成员只有满足特定条件才会存在,后面会有介绍。

descriptor.dispose_helperBlock的释放相关,这个成员只有满足特定条件才会存在,后面会有介绍。

descriptor后面就是Block捕获的各种变量。

内存布局

下面用一个例子来直观感受一下,假设有下面的Block定义:

int bi = 4;
void(^blk)(int, int, int) = ^(int i, int j, int k) {
    int result = i + j + k + bi;
    NSLog(@"%ld", result);
};

使用lldb查看内存布局如下:

(lldb) po $x0
<__NSStackBlock__: 0x16ba0f280>
 signature: "v20@?0i8i12i16"
 invoke   : 0x1043eff4c (~/Library/Developer/CoreSimulator/Devices/ABFDFFF1-D158-48E1-9C91-0C8642E93E82/data/Containers/Bundle/Application/FC608AF0-55EA-493E-A1D4-851CFD67F9F0/iOSTest.app/iOSTest`__22-[BlockHandler handle]_block_invoke)

可以看到上面的Block是一个__NSStackBlock,地址是0x16ba0f280

下面看下地址0x16ba0f280对应的内存值:

(lldb) x/8g 0x16ba0f280
0x16ba0f280: 0x00000001f2d7bb28 0x00000000c0000000
0x16ba0f290: 0x00000001043eff4c 0x00000001043fc220
0x16ba0f2a0: 0x0000000000000004 

按照上面所述的内存布局:

0x00000001f2d7bb28就是isa指针。

0x00000000c0000000就是reserved+flags,高4字节是reserved,低4字节是flags

0x00000001043eff4c就是invoke指针。

0x00000001043fc220就是descriptor指针。

0x0000000000000004就是捕获的变量bi,它的值是4

我们分别将它们的值打印出来确认一下:

# isa
(lldb) po 0x00000001f2d7bb28
__NSStackBlock__

# invoke
(lldb) image lookup -a 0x00000001043eff4c
      Address: iOSTest[0x0000000100003f4c] (iOSTest.__TEXT.__text + 12020)
      Summary: iOSTest`__22-[BlockHandler handle]_block_invoke at BlockHandler.m:16
      
# descriptor 指针
(lldb) image lookup -a 0x00000001043fc220
      Address: iOSTest[0x0000000100010220] (iOSTest.__DATA_CONST.__const + 0)
      Summary: iOSTest`__block_descriptor_36_e14_v20?0i8i12i16l

descriptor结构体的内存值也可以打印出来:

(lldb) x/8g 0x00000001043fc220
0x1043fc220: 0x0000000000000000 0x0000000000000024

可以看到第18字节是reserved字段,没有赋值,保持0

28字节是size字段,表示整个Block结构体占用0x24个字节,换算成10进制就是36字节,捕获的int变量只占用了4字节。

由于不满足条件,descriptor结构体没有copy_helperdispose_helper

2 isa

Block结构体最顶部是isa指针,说明也可以看成一个OC对象。

Block的类型可以有以下3种:

StackBlock: 创建在栈上的Block

GlobalBlock: 全局的Block

MallocBlock: 创建在堆上的Block

但是,上面所写的Block例子:

int bi = 4;
void(^blk)(int, int, int) = ^(int i, int j, int k) {
    int result = i + j + k + bi;
    NSLog(@"%ld", result);
};

看起来应该是一个StackBlock,但是如果在lldb上打印blk,会发现它是一个MallocBlock:

(lldb) po [blk description]
<__NSMallocBlock__: 0x600000c1c1b0>

原因是在ARC环境下,编译器自动将创建出来的StackBlock进行了Retain操作,导致变成了MallocBlock:

...
 0x104b7bebc <+112>: add    x8, x8, #0x220            ; __block_descriptor_36_e14_v20?0i8i12i16l
 0x104b7bec0 <+116>: str    x8, [sp, #0x48]
0x104b7bec4 <+120>: ldur   w8, [x29, #-0x14]
0x104b7bec8 <+124>: str    w8, [sp, #0x50]
->  0x104b7becc <+128>: bl     0x104b81278               ; symbol stub for: objc_retainBlock

如果想查看StackBlock,就要在Retain之前,像上面一样打印$x0寄存器。

同时,并不仅仅是定义在全局环境下的Block才能成为GlobalBlock

满足下面2个条件,也可以成为GlobalBlock:

1 定义的Block不捕获任何变量;

2 Block内部只使用全局变量或者static变量。

也就是说,下面定义的Block都是GlobalBlock:


void blockTest() {
  // 没有捕获任何变量
  void(^blk)(int, int, int) = ^(int i, int j, int k) {
      int result = i + j + k;
      NSLog(@"%ld", result);
  };
}

int g = 1; // 全局变量
void blockTest() {
  // 只使用全局变量
  void(^blk)(int, int, int) = ^(int i, int j, int k) {
      int result = i + j + k + g;
      NSLog(@"%ld", result);
  };
}

static int s = 1; // 静态变量
void blockTest() {
  // 只使用静态变量
  void(^blk)(int, int, int) = ^(int i, int j, int k) {
      int result = i + j + k + s;
      NSLog(@"%ld", result);
  };
}

void blockTest() {
  static int s = 1; // 局部静态变量
  // 只使用局部静态变量
  void(^blk)(int, int, int) = ^(int i, int j, int k) {
      int result = i + j + k + s;
      NSLog(@"%ld", result);
  };
}

3 flags

LLVM工程源码中的CGBlocks.h中定义了flags:

enum BlockLiteralFlags {
  BLOCK_IS_NOESCAPE      =  (1 << 23),
  BLOCK_HAS_COPY_DISPOSE =  (1 << 25),
  BLOCK_HAS_CXX_OBJ =       (1 << 26),
  BLOCK_IS_GLOBAL =         (1 << 28),
  BLOCK_USE_STRET =         (1 << 29),
  BLOCK_HAS_SIGNATURE  =    (1 << 30),
  BLOCK_HAS_EXTENDED_LAYOUT = (1u << 31)
};

flags

3.1 BLOCK_IS_NOESCAPE

表示定义的Block是一个非逃逸的Block,但是我在实际中并没有能构造出可以设置这个标志的Block😭。

3.2 BLOCK_HAS_COPY_DISPOSE

表示Block_Descritpor结构体中有copy_helperdispose_helper

满足2种情形才会设置这个标志。

1种情形是定义的Block捕获了其他Block:

void(^blk1)(void) = ^{
    NSLog(@"blk1");
};

void(^blk)(int, int, int) = ^(int i, int j, int k) {
    // 捕获 blk1
    blk1();
};

2种情形是捕获一个OC对象:

X *x = [X new]

void(^blk)(int, int, int) = ^(int i, int j, int k) {
    // 捕获对象 x
    NSLog(@"%ld", x.i);
};

由于Block本身也可以看成是一个OC对象,其实这2个条件可以看成是一个条件。

3.3 BLOCK_HAS_CXX_OBJ

表示Block捕获了一个C++对象。

FOO foo; // C++ 对象

void(^blk)(int, int, int) = ^(int i, int j, int k) {
    // 捕获 C++  对象
    NSLog(@"%ld", foo.value());
};

同时BLOCK_HAS_COPY_DISPOSE标志也会被设置,用来处理Blockcopy

3.4 BLOCK_IS_GLOBAL

表示Block是一个全局的Block

3.5 BLOCK_USE_STRET

按照LLVM工程源码中的Block-ABI-Apple.rst的说法,BLOCK_USE_STRET现在已经是一个无用的标志位了:

it had been a transitional marker that did not get deleted after the
transition

在实际测试中,这个标志位都是没有被设置的。

3.6 BLOCK_HAS_SIGNATURE

表示Block对象拥有方法签名:

<__NSMallocBlock__: 0x600000c15fe0>
 signature: "v20@?0i8i12i16" # 签名
 invoke   : 0x100d9ffac

3.7 BLOCK_HAS_EXTENDED_LAYOUT

表示Block有捕获的变量。

3.8 Other Flags

从枚举enum BlockLiteralFlags的定义可以看到,当中缺少了(1 << 24)(1 << 27)

这两个枚举定义在LLVM工程源码中的Block_private.h中:

enum {
    BLOCK_REFCOUNT_MASK =     (0xffff),
    BLOCK_NEEDS_FREE =        (1 << 24),
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25),
    BLOCK_HAS_CTOR =          (1 << 26), /* Helpers have C++ code. */
    BLOCK_IS_GC =             (1 << 27),
    BLOCK_IS_GLOBAL =         (1 << 28),
    BLOCK_HAS_DESCRIPTOR =    (1 << 29)
};

从上面的定义可以看到,大部分的定义和enum BlockLiteralFlags中一样。

3.9 BLOCK_REFCOUNT_MASK

表示引用计数计数掩码。

也就是说,int类型的flags并不是32bit都是作为标志,最低16bit用来表示Block被引用的次数retainCount

block_retain_count = flag & BLOCK_REFCOUNT_MASK

3.10 BLOCK_NEEDS_FREE

如果一个Block调用了copy方法,那么copy出来的Block这个标志位会被设置。

// blk2 会设置 BLOCK_NEEDS_FREE
blk2 = [blk copy];

全局的Block由于copy直接返回自身,所以除外。

3.11 BLOCK_HAS_DESCRIPTOR

枚举enum BlockLiteralFlags(1 << 29)定义的是BLOCK_USE_STRET,和这里有冲突。

在实际测试的过程中,发现这个标志位总是被设置为0,应该没什么作用了。

4 invoke

invoke最简单了,就是指向Block要调用的函数。

唯一需要注意的是,这个函数接收的第一个参数,是Block对象本身。

5 Block Descriptor

Block Descriptor结构体本身比较简单。

需要注意的是,Block对象引用的是Block Descriptor结构体指针,而不是Block Descriptor结构体本身。

可选的copy_helperdispose_helper会放到Blockcopy中写。

这里主要介绍一下编译器生成的Block Descriptor标识中各个字段的意思,比如:

__block_descriptor_40_e8_32s_e14_v20?0i8i12i16l

这个标识符是按照LLVM工程源代码中CGBlocks.cpp文件下的函数生成:

static std::string getBlockDescriptorName(const CGBlockInfo &BlockInfo, CodeGenModule &CGM)

以下面的标识符为例:

__block_descriptor_40_e8_32s_e14_v20?0i8i12i16l

__block_descriptor_是固定字符串。

40是当前Block的占用的字节数,十进制。

_是连接符。

e代表当前语法支持异常。

8代表Block内存对齐的字节数。

32s与捕获的变量有关。

32代表当前捕获的变量,在Block结构体中的所在的偏移量,十进制。

s代表当前捕获的变量类型是一个强引用(Strong)对象类型。

捕获变量类型由CGBlocks.cpp中的函数生成:

static std::string getBlockCaptureStr(const CGBlockInfo::Capture &Cap, CaptureStrKind StrKind, CharUnits BlockAlignment, CodeGenModule &CGM) 

如果Block捕获了多个变量,会有多个(偏移量+类型)拼接进去。

e是固定字符。

14代表Block签名字符串的畅读,也就是后面v20?0i8i12i16的长度,十进制。

v20?0i8i12i16代表Block的签名。

l固定字符,注意是字母l,而不是数字1

需要注意的是,e8_32s这一部分,只有当Block的标志设置了BLOCK_HAS_COPY_DISPOSE才会有,否则不会有这一部分。

6 捕获的变量

6.1 全局变量

如果Block内部引用的是全局变量或者是静态变量,都不会被捕获:

int g = 1; // 全局变量
static int s = 2; // 全局静态变量

void blockTest() {
  static int ss = 3
  void(^blk)(void) = ^{
    NSLog(@"%d", g + s + ss);
  };
}

上面gs变量不会被捕获。

6.2 auto 自动变量

auto自动变量会被捕获,这个是最常见的情形。

6.3 隐式捕获

如果内层Block捕获了一个变量,那么它所有的外层Block都会捕获这个变量,即使外层的Block没有使用这个变量。

int i = 3; // 局部变量

void(^outer)(void) = ^{
  void(^inner)(void) = ^{
    NSLog(@"%d", i);
  };
};

打印outer的内存如下:

(lldb) po [outer description]
<__NSMallocBlock__: 0x600000c2f8a0>
(lldb) x/8g 0x600000c2f8a0
0x600000c2f8a0: 0x00000001f2d7bb78 0x00000000c1000002
0x600000c2f8b0: 0x00000001045a3ee8 0x00000001045b0220
0x600000c2f8c0: 0x0000000000000003 

内层inner捕获了变量i,外层outer即使没有使用i,也会捕获i

posted @ 2026-03-17 21:15  chaoguo1234  阅读(8)  评论(0)    收藏  举报