__block 变量内存布局详解

1 内存布局

按照LLVM工程源码Block_private.h中的定义,__block变量的内存布局如下:

c 复制代码
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时它的值会有变动,会放到Blockcopy中写。

flags是标志位,后面会有介绍。

size是当前结构体所占用的字节大小。

byref_keepBlockcopy相关,只有在满足条件时才有,后面会有介绍。

byref_destoryBlock的释放相关,只有在满足条件时才有,后面会有介绍。

variable_layout__block修饰结构体Struct时才会有,后面会介绍。

variable是被定义成__block的变量。

从上图可以看到,当一个Block捕获了一个__block变量时,它的Block_Descriptor中会有copy_helperdispose_helper

因为Block_byref也有isa指针,虽然它不能作为一个OC对象看待,但是从结构上看,也符合BLOCK_HAS_COPY_DISPOSE被设置的条件。

但是结构体Block_byref中的byref_keepbyref_destroy仍是可选的。

下面用一个例子来直观感受一下:

objectivec 复制代码
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变量。

使用clangrewirte-objc将上面的代码重写为c++代码如下:

cpp 复制代码
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结构体:

cpp 复制代码
struct __Block_byref_bi_0 {
  void *__isa;
__Block_byref_bi_0 *__forwarding;
 int __flags;
 int __size;
 int bi;
};

即使在Block外部访问bi变量,也是通过这个结构体的forwarding指针进行访问。

Block内部访问bi变量,也是通过forwarding指针:

cpp 复制代码
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指针的作用会放到Blockcopy中写。

2 isa

__block变量结构体的isa指针会固定设置为0

3 flags

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

c 复制代码
// 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中的函数:

cpp 复制代码
BlockByrefHelpers *
CodeGenFunction::buildByrefHelpers(llvm::StructType &byrefType,
                                   const AutoVarEmission &emission)

只有能成功创建helper函数的Block_byref结构体,才会设置这个标志。

具体代码可以参看LLVM源码CGBlocks.cpp中的函数:

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拷贝时也没有用。

有兴趣的可以继续往下看。

比如有如下的结构体:

objectivec 复制代码
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打印对应的内存值进行查看:

bash 复制代码
(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中:

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字节:

cpp 复制代码
// CGObjcMac.cpp
llvm::Constant *CGObjCCommonMac::getBitmapBlockLayout(bool ComputeByrefLayout) {
  ...
  // Word Size 的大小
  unsigned WordSizeInBits = CGM.getTarget().getPointerWidth(LangAS::Default);
  ...
}

现在让我们从右向左解析0x6b636f6c42003020

第一个字节是20,高4bit2,代表BLOCK_LAYOUT_NON_OBJECT_WORDS

这个值说明当前数据类型是一个非对象类型,参考结构体S的定义,符合预期。

4bit0,参考BLOCK_LAYOUT_NON_OBJECT_WORDS中的注释,它代表接下来\((0 + 1) * word\_size\)个字节都是非对象类型。

第二个字节是30,高4bit3,代表BLOCK_LAYOUT_STRONG

这个值说明说明当前数据类型是一个OC对象Strong指针,参考结构体S的定义,符合预期。

4bit0,参考BLOCK_LAYOUT_STRONG中的注释,它代表接下来\((0 + 1) * word\_size\)个字节都是OC对象Strong指针,也就是占用8字节。

第三个字节是00,高4bit0,代表BLOCK_LAYOUT_OPERATOR

这个值说明解析应该停止。

这种情形下,低4bit总是0

如果被捕获的结构体比较大,则需要解析连续的内存值。

比如有下面的结构体:

cpp 复制代码
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的内存值为:

bash 复制代码
(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),那么还是会回退到普通的情形。

比如下面代码中:

cpp 复制代码
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,比如:

cpp 复制代码
typedef struct S {
  BLOCK_LAYOUT_STRONG *x1;
  BLOCK_LAYOUT_STRONG *x2;
  BLOCK_LAYOUT_WEAK *z1;
  BLOCK_LAYOUT_WEAK *z2;
};

那么variable_layout本身的值为0x0000000000000202

如果类型出现的顺序不对,variable_layout也不会使用内联形式,比如:

cpp 复制代码
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中:

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变量时,会设置这个标志。

objectivec 复制代码
__block __weak X *x;

3.7 BLOCK_BYREF_LAYOUT_UNSAFE_UNRETAINED

__block变量被__unsafe_unretained修饰时,会设置这个标志。

objectivec 复制代码
__block __unsafe_unretained X *x;

3.8 Other Flags

除了enum BlockByrefFlags中定义的枚举之外,还有其他两个被使用的枚举:

cpp 复制代码
emum {
 BLOCK_REFCOUNT_MASK =     (0xffff),
 BLOCK_NEEDS_FREE =        (1 << 24),
}

这两个枚举和Block中的flags是通用的。

3.9 BLOCK_REFCOUNT_MASK

表示引用计数计数掩码。

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

cpp 复制代码
block_byref_retain_count = flags & BLOCK_REFCOUNT_MASK

3.10 BLOCK_NEEDS_FREE

当一个Block_byrefcopy出来的,那么就会设置这个标志。

相关推荐
chaoguo12342 天前
Block 内存布局详解
block·内存布局
他们都不看好你,偏偏你最不争气7 天前
【iOS】block
开发语言·ios·objective-c·block·闭包
大熊猫侯佩2 个月前
Swift 6 驱魔实录:揭开 Combine 与 @Sendable 的“血色契约”
swift·block·combine·preconcurrency·sendable·mainactor·isolation
RollingPin3 个月前
iOS探究使用Block方式实现一对多回调能力
ios·block·runtime·数据分发·解耦·动态绑定·一对多回调
KillerNoBlood1 年前
IOS界面传值-OC
ios·objective-c·block·代理
永恒星1 年前
Unity Job System详解(3)——NativeList源码分析
block·jobsystem·nativelist
00圈圈1 年前
iOS Block 详解(Object-C)
c语言·开发语言·block
麦兜和小可的舅舅2 年前
HDFS 块重构和RedundancyMonitor详解
hadoop·hdfs·重构·block·块管理
依旧风轻2 年前
Objective-C使用块枚举的细节
ios·objective-c·block·enumerate