4

我搜索了几个有关复制块的主题,但找不到我感兴趣的信息。

当我们定义一个块时,我们有机会从其封闭范围中捕获变量。由于块存储在堆栈中,并且变量由value捕获,所以这里一切都很清楚:

  • 在原始类型的情况下,我们获取额外的变量(也在堆栈上本地化),例如一些const int ,它具有与原始int变量相同的值
  • 在指针的情况下,我们获取特定指针的副本 - 以便指向对象的引用计数增加 1。

现在,我不知道当我们将一个块从堆栈移动(复制)到堆时会发生什么。对于捕获的指针,它很简单——我们获取这些指针的副本。但是捕获的原始类型变量会发生什么?堆上的变量是动态分配的,所以我们只能用指针来引用它们。这意味着我们不能简单地将 eg int变量复制到堆中 - 我们可以动态分配一个int变量,将其分配给某个int *指针,并通过该指针写入适当的值 - 与原始int相同多变的。但是为此,我们需要一些额外的机制,它在幕后工作。此外,当我们在块中捕获一些变量时,块“准备”自己以特定方式对特定大小的变量进行操作 - 如果我们将原始类型的变量更改为指针,它通常会有不同的大小并且它需要不同的处理方式...

那么有人可以告诉我,它是如何深入工作的?还是我在某些时候完全错了?

4

4 回答 4

7

您可以在Block Implementation Specification中找到详细信息。

用一个例子来解释是最容易的。考虑这个包含一个简单块的简单函数:

void outerFunction() {
    int x = 7;
    dispatch_block_t block = ^{
        printf("%d\n", x);
    };
    dispatch_sync(dispatch_get_main_queue(), block);
}

请记住,这dispatch_block_tvoid (^)(void).

要编译该代码,编译器将首先创建两个结构定义:

struct Block_descriptor_1 {
    unsigned long reserved;
    unsigned long size;
    const char *signature;
};

struct Block_literal_1 {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *);
    struct Block_descriptor_1 *descriptor;
    int x;
};

然后它创建一个 type 的全局变量Block_descriptor_1,其中包含块的类型签名的大小Block_literal_1和编码:

struct Block_descriptor_1 block_descriptor_1 = {
    .size = sizeof(struct Block_literal_1),
    .signature = "v4@?0"
};

它创建了一个包含块体的函数:

void outerFunction_block_invoke(void *voidLiteral) {
    struct Block_literal_1 *literal = (struct Block_literal_1 *)voidLiteral;
    printf("%d\n", literal->x);
}

请注意,块的主体已被重写,因此从封闭范围对变量x的访问现在是对块文字成员的访问。

最后,编译器重写原始函数以在堆栈上创建一个块文字并使用该块文字的地址而不是块:

void outerFunction2() {
    int x = 7;
    struct Block_literal_1 block_literal_1 = {
        .isa = __NSConcreteStackBlock,
        .flags  = BLOCK_HAS_SIGNATURE,
        .invoke = outerFunction_block_invoke,
        .descriptor = &block_descriptor_1,
        .x = x
    };
    dispatch_sync(dispatch_get_main_queue(),
        (__bridge dispatch_block_t)&block_literal_1);
}

请注意,块文字以指向特殊 Objective-C 的指针开头Class。这允许将块文字视为一个 Objective-C 对象。

如果将__block属性添加到局部变量x,它会变得更加复杂。在这种情况下,编译器必须创建另一个结构来保存该变量以及有关变量的信息(例如,如果变量是指向对象的指针,如何保留和释放它)。这一切都在我在此答案顶部链接的规范中进行了解释。

于 2013-03-25T22:00:20.923 回答
3

我不确定你在问什么,但也许对块实际工作方式的简单解释会澄清事情。

块实际上是匿名类的实例。该类对每个捕获的变量都有一个 ivar。当块实例化时,所有捕获的变量都被复制到块上各自的 ivars 中,此时存储在堆栈中。

当块被复制到堆中时,它会在堆上创建一个新的块对象,并将堆栈块中的所有 ivars 复制到堆块中(此时它还保留任何捕获的 obj-c 对象)。指向原始值的指针和诸如此类的东西没有混淆;堆上只有一个 malloc'd 区域包含所有捕获的值,就像任何其他 obj-c 对象一样。

同时,块中的实际代码通过使用 的等价物简单地访问捕获的变量,implicit_block_pointer->backing_ivar与对象上的方法访问对象的 ivars 的方式完全相同。

于 2013-03-25T21:33:04.227 回答
2

你正在做的是缺少一个级别的指针,只要你学会使用 Algol-68 编程......(ref loc有人吗?)

[以下内容有所简化,以呈现正在发生的事情的要点。]

当你声明一个变量时,说:

int x;

您正在指示编译器找到一个能够存储 an 表示的位置,并将int该位置用于您使用 name 引用的值x

跳过“查找”位,编译器构造一个内部表,即符号表,它将名称x在这里映射到位置 - 并且位置表示为绝对地址(也称为指针)或从某物的偏移量,例如像“堆栈上的第 7 个位置”。有些东西有时存储在计算机内部的一个特殊命名位置,称为寄存器例如,有一个寄存器存储堆栈指针,因此存储为堆栈指针偏移量的变量位于该寄存器中存储的值的偏移量。

使用该表,编译器看到的x可以确定x存储 ' 值表示的地址。

在机器指令级别读取或写入变量涉及使用获取地址的指令。因此,当您深入了解实际的机器代码时,所有变量都通过指针引用。

现在到你的块案例。当您捕获我们的示例x整数变量时,编译器会在描述该块的结构中为其分配一个位置。在符号表中,它为它创建一个条目x并将其映射为类似于“块变量区域中的第 6 个位置”。块变量区的位置被安排在一个特定的位置,可能是一个寄存器,就像上面的堆栈指针一样,然后机器指令x从该位置找到值作为偏移量。

当块基于堆栈时,块变量区域将在堆栈中,当它基于堆时,它将在堆中,但由于它的位置在块执行之前存储在寄存器中,因此块的代码永远不会需要更改 -x相对于块变量区域始终处于相同的偏移量。

希望所有这些都是有道理的!

于 2013-03-25T21:55:05.207 回答
1

“在幕后工作”的附加机制只是 C 结构成员访问。无论是在堆堆栈上,块都是结构,每个捕获的变量都有成员(它们实际上也是 Objective-C 对象)。执行块时,会调用一个函数,该函数将指向块的指针作为参数。此函数访问捕获的变量,如blockPointer->capturedVar1. blockPointer此时指向哪里并不重要——重要的是在某个结构中为捕获的变量分配了空间。

您可能会发现这很有启发性:http ://clang.llvm.org/docs/Block-ABI-Apple.html

于 2013-03-25T21:59:56.360 回答