【问题标题】:How are NSBlock objects created?NSBlock 对象是如何创建的?
【发布时间】:2013-11-22 00:19:11
【问题描述】:

我将在这个问题的开头说明我将要问的内容仅用于教育目的,也可能仅用于调试目的。

如何在 Objective C 运行时内部创建块对象?

我看到了代表各种块类型的类的层次结构,层次结构中最高的超类,在NSObject 之下,是NSBlock。类数据转储显示它实现了+ alloc+ allocWithZone:+ copy+ copyWithZone: 方法。其他块子类都没有实现这些类方法,这让我相信,也许是错误的,NSBlock 负责块处理。

但这些方法似乎不会在块的生命周期内的任何时候被调用。我用自己的实现交换了实现,并在每个实现中放置了一个断点,但它们从未被调用。用NSObject 的实现做类似的练习给了我我想要的。

所以我假设块以不同的方式实现?任何人都可以阐明这个实现是如何工作的?即使我无法挂钩块的分配和复制,我也想了解内部实现。

【问题讨论】:

  • 我不认为可以使用[NSBlock alloc] 来创建块,这就是为什么不调用它们的原因。

标签: objective-c objective-c-blocks objective-c-runtime


【解决方案1】:

tl;博士

编译器直接将块文字转换为结构和函数。这就是您看不到alloc 呼叫的原因。


讨论

虽然块是成熟的 Objective-C 对象,但这一事实很少在它们的使用中暴露出来,使它们成为非常有趣的野兽。

第一个怪癖是块通常在堆栈上创建(除非它们是全局块,即不参考周围上下文的块),然后仅在需要时才在堆上移动。时至今日,它们是唯一可以在堆栈上分配的 Objective-C 对象。

可能由于分配中的这种怪异,语言设计者决定只允许通过块文字创建块(即使用^ 运算符)。 这样,编译器就可以完全控制块分配。

正如clang specification 中所解释的,编译器会自动为遇到的每个块字面量生成两个结构和至少一个函数:

  • 块文字结构
  • 块描述符结构
  • 块调用函数

例如文字

^ { printf("hello world\n"); }

在 32 位系统上,编译器将生成以下内容

struct __block_literal_1 {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(struct __block_literal_1 *);
    struct __block_descriptor_1 *descriptor;
};

void __block_invoke_1(struct __block_literal_1 *_block) {
    printf("hello world\n");
}

static struct __block_descriptor_1 {
    unsigned long int reserved;
    unsigned long int Block_size;
} __block_descriptor_1 = { 0, sizeof(struct __block_literal_1), __block_invoke_1 };

(顺便说一句,该块符合全局块的条件,因此它将在内存中的固定位置创建)

所以块是 Objective-C 对象,但以低级方式:它们只是带有 isa 指针的结构。尽管从形式上看它们是NSBlock 的具体子类的实例,但Objective-C API 从未用于分配,所以这就是你看不到alloc 调用的原因:文字直接翻译成结构由编译器。

【讨论】:

  • 我不相信全局块是在堆上创建的;您链接到的文档清楚地显示(在您拉出的代码 sn-p 之后)该块将是一个堆栈块。 (这是有道理的;如果它没有运行时数据,你为什么要把它放在堆上?)另外,我不相信它们是唯一被堆栈分配的objective-c 对象。在 64 位 ABI 下,整个对象可能嵌入到您的指针中;所以他们甚至可以只是寄存器分配。 :)
  • @JesseRusak 全局块非常类似于 NSString 文字。它们生活在一个固定的位置,因为它们是恒定的对象。在堆栈上创建它们需要在每次构建堆栈框架时分配它们,但是由于它们不包含对周围上下文的任何引用,所以这被优化掉了。这在这里得到了很好的解释:cocoawithlove.com/2009/10/how-blocks-are-implemented-and.html
  • @GabrielePetronella 我仍然反对你的说法“块有资格作为全局块,所以它将在堆上创建”——全局块不是堆分配的;他们居住的固定位置不在堆上。
  • 正确的强调是不能在堆外分配Objective-C对象。编译器和运行时在少数地方使用全局存储和堆栈存储,例如块对象、常量字符串对象和标记指针对象。但是如果没有奇特的技巧就无法做到这一点。
  • 这些技巧通常从 (1) 使用 alloca() 分配内存,(2) 使用 object_setClass() 在该内存中设置一个 isa 开始。如果任何代码在该对象上调用 -retain,则需要额外的复杂性。
【解决方案2】:

如其他答案中所述,块对象是直接在全局存储中(由编译器)或在堆栈上(由编译的代码)创建的。它们最初不是在堆上创建的。

Block 对象类似于桥接的 CoreFoundation 对象:Objective-C 接口是底层 C 接口的覆盖。块对象的-copyWithZone:方法调用_Block_copy()函数,但有些代码直接调用_Block_copy()。这意味着-copyWithZone: 上的断点不会捕获所有副本。

(是的,您可以在纯 C 代码中使用块对象。有一个 qsort_b() 函数和一个 atexit_b() 函数,嗯,可能就是这样。)

【讨论】:

  • 嗨,所以全局存储不等于堆栈?
【解决方案3】:

块基本上是编译器的魔法。与普通对象不同,它们实际上是直接在堆栈上分配的——它们仅在您复制它们时才被放置在堆上。

您可以阅读 Clang 的 block implementation specification 以了解幕后发生的事情。据我了解,简短的版本是定义了一个结构类型(表示块及其捕获的状态)和一个函数(调用块),并且对块的任何引用都替换为具有的结构类型的值它的调用指针设置为生成的函数,并用适当的状态填充其字段。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-12-05
    • 1970-01-01
    • 2015-05-07
    • 2013-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多