__block变量内存布局是怎样的复杂结构?

摘要:1 内存布局 按照LLVM工程源码Block_private.h中的定义,__block变量的内存布局如下: struct Block_byref { void *isa; struct Block_byref *forwarding; i
1 内存布局 按照LLVM工程源码Block_private.h中的定义,__block变量的内存布局如下: struct Block_byref { void *isa; struct Block_byref *forwarding; int flags; int size; // 可选 void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src); // 可选 void (*byref_destroy)(struct Block_byref *); // 可选 void *variable_layout; // 变量 Type variable; }; isa通常会被赋值为0,后面会有介绍。 forwarding指针在__block变量定义时会指向Block_byref自身。 当Block发生copy时它的值会有变动,会放到Block的copy中写。 flags是标志位,后面会有介绍。 size是当前结构体所占用的字节大小。 byref_keep与Block的copy相关,只有在满足条件时才有,后面会有介绍。 byref_destory与Block的释放相关,只有在满足条件时才有,后面会有介绍。 variable_layout在__block修饰结构体Struct时才会有,后面会介绍。 variable是被定义成__block的变量。 从上图可以看到,当一个Block捕获了一个__block变量时,它的Block_Descriptor中会有copy_helper和dispose_helper。 因为Block_byref也有isa指针,虽然它不能作为一个OC对象看待,但是从结构上看,也符合BLOCK_HAS_COPY_DISPOSE被设置的条件。 但是结构体Block_byref中的byref_keep和byref_destroy仍是可选的。 下面用一个例子来直观感受一下: void blockTest() { // __block 变量 __block int bi = 4; void(^blk)(int, int, int) = ^(int i, int j, int k) { int result = i + j + k + bi; NSLog(@"%d", result); }; // block 外操作 bi bi++; } 变量bi是一个__block变量。 使用clang的rewirte-objc将上面的代码重写为c++代码如下: void blockTest() { // __block 变量 __attribute__((__blocks__(byref))) __Block_byref_bi_0 bi = {(void*)0,(__Block_byref_bi_0 *)&bi, 0, sizeof(__Block_byref_bi_0), 4}; void(*blk)(int, int, int) = ((void (*)(int, int, int))&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA, (__Block_byref_bi_0 *)&bi, 570425344)); // block 外操作 bi (bi.__forwarding->bi)++; } 可以看到__block变量被编译后,成为了Block_byref结构体: struct __Block_byref_bi_0 { void *__isa; __Block_byref_bi_0 *__forwarding; int __flags; int __size; int bi; }; 即使在Block外部访问bi变量,也是通过这个结构体的forwarding指针进行访问。 在Block内部访问bi变量,也是通过forwarding指针: struct __blockTest_block_impl_0 { struct __block_impl impl; struct __blockTest_block_desc_0* Desc; // Block 捕获的 __block 变量 bi __Block_byref_bi_0 *bi; // by ref }; static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself, int i, int j, int k) { __Block_byref_bi_0 *bi = __cself->bi; // bound by ref // Block 内部访问 bi 变量 int result = i + j + k + (bi->__forwarding->bi); NSLog((NSString *)&__NSConstantStringImpl__var_folders_p6_jy49zvqx2656qb8rbq64hc_w0000gn_T_Block_de3226_mi_0, result); } forwarding指针的作用会放到Block的copy中写。 2 isa __block变量结构体的isa指针会固定设置为0。 3 flags 在LLVM工程源码CGBlocks.h中定义了flags: // Flags stored in __block variables. enum BlockByrefFlags { BLOCK_BYREF_HAS_COPY_DISPOSE = (1 << 25), // compiler BLOCK_BYREF_LAYOUT_MASK = (0xF << 28), // compiler BLOCK_BYREF_LAYOUT_EXTENDED = (1 << 28), BLOCK_BYREF_LAYOUT_NON_OBJECT = (2 << 28), BLOCK_BYREF_LAYOUT_STRONG = (3 << 28), BLOCK_BYREF_LAYOUT_WEAK = (4 << 28), BLOCK_BYREF_LAYOUT_UNRETAINED = (5 << 28) }; 3.1 BLOCK_BYREF_HAS_COPY_DISPOSE 设置这个标志的条件和Block结构体类似: 1 __block变量本身就是一个结构体; 2 __block变量是一个OC对象; 3 __block变量是一个C++对象。 具体的设置规则,可以参看LLVM源码CGBlocks.cpp中的函数: BlockByrefHelpers * CodeGenFunction::buildByrefHelpers(llvm::StructType &byrefType, const AutoVarEmission &emission) 只有能成功创建helper函数的Block_byref结构体,才会设置这个标志。 具体代码可以参看LLVM源码CGBlocks.cpp中的函数: void CodeGenFunction::emitByrefStructureInit(const AutoVarEmission &emission) { ... // Build the byref helpers if necessary. This is null if we don't need any. BlockByrefHelpers *helpers = buildByrefHelpers(*byrefType, emission); ... BlockFlags flags; if (helpers) flags |= BLOCK_BYREF_HAS_COPY_DISPOSE; ... } 3.2 BLOCK_BYREF_LAYOUT_MASK 这是一个4bit的掩码,可以从flags中取出取出对应的枚举字段。 3.3 BLOCK_BYREF_LAYOUT_EXTEND 如果__block变量是一个结构体或者是一个C++对象(C++里结构体和class可以互用),那么这个标志会被设置。 同时,Block_byref结构体中的variable_layout会有值。 variable_layout描述了被捕获的结构体中,各个成员变量的类型以及占用的字节数。 对于variable_layout的解析虽然有点复杂,但是实际测试中并没有发现它的用处,即使在Block拷贝时也没有用。 有兴趣的可以继续往下看。 比如有如下的结构体: typedef struct S { int i; X *x; // X 是 OC 对象 }; void blockTest() { __block S s; s.i = 1; s.x = [X new]; void(blk)(int, int, int) = ^(int i, int j, int k) { int result = i + j + k + s.i; NSLog(@"%@", result); }; } 上面代码中的Block捕获了一个结构体__block S。 我们使用lldb打印对应的内存值进行查看: (lldb) po [blk description] <__NSMallocBlock__: 0x600000c00090> # Block 内存值 (lldb) x/8g 0x600000c00090 0x600000c00090: 0x00000001f2d7bb78 0x00000000c3000002 0x600000c000a0: 0x00000001048c80dc 0x00000001048d4240 0x600000c000b0: 0x0000600001700100 # Block_byref 内存值 (lldb) x/8g 0x0000600001700100 0x600001700100: 0x0000000000000000 0x0000600001700100 0x600001700110: 0x0000004013000004 0x00000001048c800c 0x600001700120: 0x00000001048c8070 0x00000001048ce87a 0x600001700130: 0x0000000000000001 0x00006000000150a0 # variable_layout 内存值 (lldb) x/8g 0x00000001048ce87a 0x1048ce87a: 0x6b636f6c42003020 Block_byref内存值0x0000004013000004就是size+flags。 分析0x0000004013000004的低32bit,它的第28bit正好是1,说明BLOCK_BYREF_LAYOUT_EXTEND被设置了。 接下来再看variable_layout。 variable_layout是一个指针,指向的内存中的值是0x6b636f6c42003020。 对于0x6b636f6c42003020,需要从右向左一个字节一个字节的解析,直到遇到00为止。 每一个字节的高4bit定义了数据的类型,低4bit定义了数据占用的字节数。 高4bit的枚举值定义在LLVM源码CGObjcCMac.cpp中: enum BLOCK_LAYOUT_OPCODE { /// An operator which affects how the following layout should be /// interpreted. /// I == 0: Halt interpretation and treat everything else as /// a non-pointer. Note that this instruction is equal /// to '\0'. /// I != 0: Currently unused. BLOCK_LAYOUT_OPERATOR = 0, /// The next I+1 bytes do not contain a value of object pointer type. /// Note that this can leave the stream unaligned, meaning that /// subsequent word-size instructions do not begin at a multiple of /// the pointer size. BLOCK_LAYOUT_NON_OBJECT_BYTES = 1, /// The next I+1 words do not contain a value of object pointer type. /// This is simply an optimized version of BLOCK_LAYOUT_BYTES for /// when the required skip quantity is a multiple of the pointer size. BLOCK_LAYOUT_NON_OBJECT_WORDS = 2, /// The next I+1 words are __strong pointers to Objective-C /// objects or blocks. BLOCK_LAYOUT_STRONG = 3, /// The next I+1 words are pointers to __block variables. BLOCK_LAYOUT_BYREF = 4, /// The next I+1 words are __weak pointers to Objective-C /// objects or blocks. BLOCK_LAYOUT_WEAK = 5, /// The next I+1 words are __unsafe_unretained pointers to /// Objective-C objects or blocks. BLOCK_LAYOUT_UNRETAINED = 6 /// The next I+1 words are block or object pointers with some /// as-yet-unspecified ownership semantics. If we add more /// flavors of ownership semantics, values will be taken from /// this range. /// /// This is included so that older tools can at least continue /// processing the layout past such things. // BLOCK_LAYOUT_OWNERSHIP_UNKNOWN = 7..10, /// All other opcodes are reserved. Halt interpretation and /// treat everything else as opaque. }; 上面枚举中的word-size是一个指针真用的字节数,64bit的机器上通常是8字节: // CGObjcMac.cpp llvm::Constant *CGObjCCommonMac::getBitmapBlockLayout(bool ComputeByrefLayout) { ... // Word Size 的大小 unsigned WordSizeInBits = CGM.getTarget().getPointerWidth(LangAS::Default); ... } 现在让我们从右向左解析0x6b636f6c42003020。 第一个字节是20,高4bit是2,代表BLOCK_LAYOUT_NON_OBJECT_WORDS。 这个值说明当前数据类型是一个非对象类型,参考结构体S的定义,符合预期。 低4bit是0,参考BLOCK_LAYOUT_NON_OBJECT_WORDS中的注释,它代表接下来\((0 + 1) * word\_size\)个字节都是非对象类型。 第二个字节是30,高4bit是3,代表BLOCK_LAYOUT_STRONG。 这个值说明说明当前数据类型是一个OC对象Strong指针,参考结构体S的定义,符合预期。 低4bit时0,参考BLOCK_LAYOUT_STRONG中的注释,它代表接下来\((0 + 1) * word\_size\)个字节都是OC对象Strong指针,也就是占用8字节。 第三个字节是00,高4bit是0,代表BLOCK_LAYOUT_OPERATOR。 这个值说明解析应该停止。 这种情形下,低4bit总是0。 如果被捕获的结构体比较大,则需要解析连续的内存值。 比如有下面的结构体: typedef struct S { int ai; BlockX *a; int bi; BlockX *b; int ci; BlockX *c; int di; BlockX *d; int ei; BlockX *e; }; 使用lldb查看variable_layout的内存值为: (lldb) x/8g 0x000000010017aa1a 0x10017aa1a: 0x3020302030203020 0x6b636f6c42003020 解析的时候,从第一条内存最右边开始,然后接着是第二条内存最右边,直到遇到00结束。 variable_layout的值还有一种内联形式。 如果一个结构体或者C++对象里只有enum BLOCK_LAYOUT_OPCODE中定义的如下类型的成员变量: BLOCK_LAYOUT_STRONG BLOCK_LAYOUT_BYREF BLOCK_LAYOUT_WEAK 并且每种类型的成员变量都是按顺序连续分布,那么variable_layout就会使用内联形式。 variable_layout指针本身的值,就会变成0x0000000000000xyz的形式。 其中x y z都是4bit。 x表示BLOCK_LAYOUT_STRONG的个数; y表示BLOCK_LAYOUT_BYREF的个数; z表示BLOCK_LAYOUT_WEAK的个数。 但是如果variable_layout的值大于等于(1 << 12),那么还是会回退到普通的情形。 比如下面代码中: typedef struct S { BLOCK_LAYOUT_STRONG *x1; BLOCK_LAYOUT_STRONG *x2; BLOCK_LAYOUT_BYREF *y1; BLOCK_LAYOUT_BYREF *y2; BLOCK_LAYOUT_WEAK *z1; BLOCK_LAYOUT_WEAK *z2; }; 就可以使用variable_layout的内联形式, variable_layout本身的值为0x0000000000000222。 如果某种类型没有,那么variable_layout中对应位置就是0,比如: typedef struct S { BLOCK_LAYOUT_STRONG *x1; BLOCK_LAYOUT_STRONG *x2; BLOCK_LAYOUT_WEAK *z1; BLOCK_LAYOUT_WEAK *z2; }; 那么variable_layout本身的值为0x0000000000000202。 如果类型出现的顺序不对,variable_layout也不会使用内联形式,比如: typedef struct S { BLOCK_LAYOUT_BYREF *y1; BLOCK_LAYOUT_BYREF *y2; BLOCK_LAYOUT_STRONG *x1; BLOCK_LAYOUT_STRONG *x2; BLOCK_LAYOUT_WEAK *z1; BLOCK_LAYOUT_WEAK *z2; }; 那么variable_layout仍使用普通形式。 有关variable_layout的内联形式,定义在LLVM源码的CGObjcMac.cpp中: /// 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. uint64_t CGObjCCommonMac::InlineLayoutInstruction( SmallVectorImpl<unsigned char> &Layout) 方法上面的注释已经解释的很清楚了。 3.4 BLOCK_BYREF_LAYOUT_NON_OBJECT 当__block变量是一个非对象类型,比如int时,会设置这个标志。 3.5 BLOCK_BYREF_LAYOUT_STRONG 当__block变量是一个Strong类型的OC变量时,会设置这个标志。 3.6 BLOCK_BYREF_LAYOUT_WEAK 当__block变量是一个Weak类型的OC变量时,会设置这个标志。 __block __weak X *x; 3.7 BLOCK_BYREF_LAYOUT_UNSAFE_UNRETAINED 当__block变量被__unsafe_unretained修饰时,会设置这个标志。 __block __unsafe_unretained X *x; 3.8 Other Flags 除了enum BlockByrefFlags中定义的枚举之外,还有其他两个被使用的枚举: emum { BLOCK_REFCOUNT_MASK = (0xffff), BLOCK_NEEDS_FREE = (1 << 24), } 这两个枚举和Block中的flags是通用的。 3.9 BLOCK_REFCOUNT_MASK 表示引用计数计数掩码。 也就是说,int类型的flags并不是32bit都是作为标志,最低16bit用来表示Block_byref被引用的次数retainCount。 block_byref_retain_count = flags & BLOCK_REFCOUNT_MASK 3.10 BLOCK_NEEDS_FREE 当一个Block_byref是copy出来的,那么就会设置这个标志。