我搜索了几个关于复制块的主题,但我找不到我感兴趣的信息。
当我们定义一个块时,我们有机会从其封闭范围中捕获变量。由于块存储在堆栈中,变量由值捕获,所以一切都清楚了:
现在,我不知道当我们将一个块从堆栈移动(复制)到堆时会发生什么。对于捕获的指针,它很简单 - 我们获取这些指针的副本。但是捕获的原始类型变量会发生什么?堆上的变量是动态分配的,因此我们只能用指针来引用它们。这意味着我们不能简单地复制例如 int 变量到堆 - 我们可以动态分配一个 int 变量,将它分配给一些 int * 指针,并通过该指针写入相应的value - 与原始 int 变量中的值相同。但为此,我们需要一些额外的机制,在幕后工作。另外,当我们在块中捕获一些变量时,阻塞“准备”自己以特定大小和特定方式对变量进行操作 - 如果我们将原始类型的变量更改为指针,它通常会有不同的大小,它将需要不同的处理方式......
有人可以告诉我,它是如何深入工作的?或者我在某些方面完全错了?
答案 0 :(得分: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_t
是void (^)(void)
的typedef。
要编译该代码,编译器将首先创建两个结构定义:
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;
};
然后它创建一个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
,则会变得更复杂。在这种情况下,编译器必须创建另一个结构来保存该变量以及有关变量的信息(如果变量是指向对象的指针,如何保留和释放它)。这一点在我在本答案顶部链接的规范中进行了解释。
答案 1 :(得分:3)
我不确定你在问什么,但也许对块实际工作方式的简单解释会澄清事情。
块实际上是匿名类的实例。该类为每个捕获的变量都有一个ivar。当块被实例化时,所有捕获的变量都被复制到块上的各自的ivars中,此时块存储在堆栈中。
当块被复制到堆时,它会在堆上创建一个新的块对象,并将所有ivars从堆栈块复制到堆块(此时它还会保留所有捕获的obj-c对象)。对于原始值和诸如此类的指针没有混淆;堆上只有一个malloc'd区域,包含所有捕获的值,就像任何其他obj-c对象一样。
同时,块中的实际代码只使用等效的implicit_block_pointer->backing_ivar
访问捕获的变量,与对象上的方法访问对象的ivars的方式完全相同。
答案 2 :(得分:2)
你正在做的是缺少一定程度的指针,只要你学会使用Algol-68编程...(ref loc
任何人?)
[以下内容有所简化,以展示正在发生的事情的要点。]
声明变量时,请说:
int x;
您正在指示编译器找到能够存储int
表示的位置,并将该位置用于您使用名称x
引用的值。
跳过“查找”位,编译器构造一个内部表,即符号表,它将名称x
映射到位置 - 并且位置表示为绝对地址(又名指针)或作为某事物的抵消,例如就像“堆叠中的第7个位置”。 某些有时会存储在计算机内一个名为 register 的特殊命名位置,例如:存在堆栈指针的寄存器,因此存储为堆栈指针的偏移量的变量与存储在该寄存器中的值的偏移量相同。
使用此表,看到x
的编译器可以确定存储x
值表示的地址。
在机器指令级读取或写入变量涉及使用带有地址的指令。因此,当您到达实际的机器代码时,所有变量都通过指针引用。
现在你的块案例。捕获示例x
整数变量时,编译器会在描述块的结构中为其分配位置。在符号表中,它为x
创建一个条目,并将其映射为“块变量区域中的第6个位置”。 块变量区域的位置被安排在一个特定的位置,可能是一个寄存器,就像上面的堆栈指针一样,然后机器指令找到x
的值作为偏离该位置。
当块是基于堆栈的时,块变量区域将在堆栈上,当它基于堆时它将在堆中,但是因为它的位置在块执行之前存储在寄存器中,所以代码为块永远不需要改变 - x
总是与块变量区域相同的偏移量。
希望这一切都有意义!
答案 3 :(得分:1)
“在幕后工作”的附加机制只是C struct成员访问。无论是在堆栈堆上,块都是具有每个捕获变量的成员的结构(它们实际上也是Objective-C对象)。执行块时,将调用一个函数,该函数将指向块的指针作为参数。此函数访问捕获的变量,如blockPointer->capturedVar1
。在blockPointer
指向此处的位置并不重要 - 所有重要的是在某个结构中为捕获的变量分配空间。
您可能会发现这具有启发性:http://clang.llvm.org/docs/Block-ABI-Apple.html