Block 内存布局详解

发布时间:2026/5/19 20:18:40

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_helper与Block的拷贝相关这个成员只有满足特定条件才会存在后面会有介绍。descriptor.dispose_helper与Block的释放相关这个成员只有满足特定条件才会存在后面会有介绍。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就是reservedflags高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?0i8i12i16ldescriptor结构体的内存值也可以打印出来:(lldb) x/8g 0x00000001043fc220 0x1043fc220: 0x0000000000000000 0x0000000000000024可以看到第1个8字节是reserved字段没有赋值保持0。第2个8字节是size字段表示整个Block结构体占用0x24个字节换算成10进制就是36字节捕获的int变量只占用了4字节。由于不满足条件descriptor结构体没有copy_helper和dispose_helper。2 isaBlock结构体最顶部是isa指针说明也可以看成一个OC对象。Block的类型可以有以下3种:StackBlock: 创建在栈上的BlockGlobalBlock: 全局的BlockMallocBlock: 创建在堆上的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不捕获任何变量2Block内部只使用全局变量或者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) };3.1 BLOCK_IS_NOESCAPE表示定义的Block是一个非逃逸的Block但是我在实际中并没有能构造出可以设置这个标志的Block。3.2 BLOCK_HAS_COPY_DISPOSE表示Block_Descritpor结构体中有copy_helper和dispose_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标志也会被设置用来处理Block的copy。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 thetransition在实际测试中这个标志位都是没有被设置的。3.6 BLOCK_HAS_SIGNATURE表示Block对象拥有方法签名:__NSMallocBlock__: 0x600000c15fe0 signature: v20?0i8i12i16 # 签名 invoke : 0x100d9ffac3.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_MASK3.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 invokeinvoke最简单了就是指向Block要调用的函数。唯一需要注意的是这个函数接收的第一个参数是Block对象本身。5 Block DescriptorBlock Descriptor结构体本身比较简单。需要注意的是Block对象引用的是Block Descriptor结构体指针而不是Block Descriptor结构体本身。可选的copy_helper与dispose_helper会放到Block的copy中写。这里主要介绍一下编译器生成的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); }; }上面g和s变量不会被捕获。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。

相关新闻