Marcus 的回答非常好,但这里有更多细节(我一直想复习阅读生成的程序集;必须实际尝试解释它是最好的方法)。
NSObject *obj1 = [[NSObject alloc] init]; // Line 1
编译器将两个函数调用编译为objc_msgSend()
. 第一个调用类+alloc
上的方法NSObject
。该函数调用的结果成为调用该方法的第二个函数调用的第一个参数(目标对象)-init
。
然后,调用的结果init
将存储在堆栈中您已声明为已命名的内存块中,该内存块具有指向 NSObject 实例的指针obj1
类型。
您可以在调试器中单步执行这一行,因为该行上有一个已执行的表达式。如果代码写成:
NSObject *obj1; // declaration
obj1 = [[NSObject alloc] init];
然后你会发现你不能单步执行声明。
在ARC** 下的obj1 = [[NSObject alloc] init];, the value of
obj1 is *undefined* under Manual Retain Release, but **will be automatically set to
nil`(0) 之前(从而消除了 Marcus 指出的错误来源)。
[obj1 release]; // Line 2
这一行调用NSObject 指向的实例上的release
方法。obj1
NSObject *obj2; // Line 3
这条线实际上什么也没做。如果打开编译器的优化器,则根本不会生成任何代码。如果没有优化器,编译器可能会碰撞堆栈指针sizeof(NSObject*)
以在堆栈上保留名称为 的空间obj2
。
而且,您不能在调试器中单步执行它,因为在该行上没有要执行的表达式。
值得注意的是,您可以将代码重写为:
[[[NSObject alloc] init] release];
就执行而言,这实际上与您编写的原始代码相同。如果没有优化器,它会有点不同,因为它不会在堆栈上存储任何东西。使用优化器,它可能会生成与原始代码相同的代码。优化器非常擅长在不需要时消除局部变量(这也是调试优化代码如此困难的部分原因)。
鉴于这种:
(11) void f()
(12) {
(13) NSObject *obj1 = [[NSObject alloc] init]; // Line 1
(14)
(15) [obj1 release]; // Line 2
(16)
(17) NSObject *obj2; // Line 3
(18)}
这是未优化的 x86_64 程序集。忽略“修复”的东西。看callq
线条;它们是对 objc_msgSend() 的实际调用,如上所述。在 x86_64 上,%rdi——一个寄存器——是所有函数调用的参数 0。因此, %rdi 是方法调用的目标所在。%rax 是用于返回值的寄存器。
所以,当你看到一个 callq ,然后是movq %rax, %rdi
,然后是另一个 callq 时,它表示“获取第一个的返回值callq
并将其作为第一个参数传递给下一个callq
。
至于你的变量,你会movq %rax, -8(%rbp)
在callq
. 这表示“获取由 . 返回的任何内容callq
,将其写入堆栈上的当前位置,然后将堆栈指针向下移动 8 个位置(堆栈向下增长)”。不幸的是,程序集没有显示变量名。
_f: ## @f
.cfi_startproc
Lfunc_begin0:
.loc 1 12 0 ## /tmp/asdfafsd/asdfafsd/main.m:12:0
## BB#0:
pushq %rbp
Ltmp2:
.cfi_def_cfa_offset 16
Ltmp3:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp4:
.cfi_def_cfa_register %rbp
subq $32, %rsp
leaq l_objc_msgSend_fixup_release(%rip), %rax
leaq l_objc_msgSend_fixup_alloc(%rip), %rcx
.loc 1 13 0 prologue_end ## /tmp/asdfafsd/asdfafsd/main.m:13:0
Ltmp5:
movq L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rdx
movq %rdx, %rdi
movq %rcx, %rsi
movq %rax, -24(%rbp) ## 8-byte Spill
callq *l_objc_msgSend_fixup_alloc(%rip)
movq L_OBJC_SELECTOR_REFERENCES_(%rip), %rsi
movq %rax, %rdi
callq _objc_msgSend
movq %rax, -8(%rbp)
.loc 1 15 0 ## /tmp/asdfafsd/asdfafsd/main.m:15:0
movq -8(%rbp), %rax
movq %rax, %rdi
movq -24(%rbp), %rsi ## 8-byte Reload
callq *l_objc_msgSend_fixup_release(%rip)
.loc 1 18 0 ## /tmp/asdfafsd/asdfafsd/main.m:18:0
addq $32, %rsp
popq %rbp
ret
Ltmp6:
Lfunc_end0:
对于傻笑,看看打开优化器生成的程序集(-Os - 最快,最小,部署代码的默认值):
首先要注意的——这又回到问题(3)——是在第一个和最后一个指令之外没有任何操作。%rbp
也就是说,没有任何东西被推入或拉出堆栈;从字面上看,没有证据表明obj1
并且obj2
曾经被声明过,因为编译器不需要它们来生成等效代码。
一切都是通过寄存器完成的,你会注意到有两个move %rax, %rdi
. 第一个是“获取 the 的结果+alloc
并将其用作调用的第一个参数-init
”,第二个是“获取 the 的结果-init
并将其用作-release
.
在旁边; %rsi
是函数调用的第二个参数在 x86_64 上的位置。对于方法调用——对于objc_msgSend()
函数调用——该参数将始终包含要调用的方法(选择器)的名称。
Lfunc_begin0:
.loc 1 12 0 ## /tmp/asdfafsd/asdfafsd/main.m:12:0
## BB#0:
pushq %rbp
Ltmp2:
.cfi_def_cfa_offset 16
Ltmp3:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp4:
.cfi_def_cfa_register %rbp
.loc 1 13 0 prologue_end ## /tmp/asdfafsd/asdfafsd/main.m:13:0
Ltmp5:
movq L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rdi
leaq l_objc_msgSend_fixup_alloc(%rip), %rsi
callq *l_objc_msgSend_fixup_alloc(%rip)
movq L_OBJC_SELECTOR_REFERENCES_(%rip), %rsi
movq %rax, %rdi
callq *_objc_msgSend@GOTPCREL(%rip)
.loc 1 15 0 ## /tmp/asdfafsd/asdfafsd/main.m:15:0
leaq l_objc_msgSend_fixup_release(%rip), %rsi
movq l_objc_msgSend_fixup_release(%rip), %rcx
movq %rax, %rdi
popq %rbp
jmpq *%rcx # TAILCALL
Ltmp6:
Lfunc_end0:
如果你想了解更多关于方法分派的知识,我写了一点指南。这是 objc_msgSend() 的几个版本过时,但仍然相关。
请注意,ARM 代码在哲学上以相同的方式工作,但生成的程序集会有点不同,而且会更多。
我还是不明白为什么我不能跨过第3行^^
如果您查看生成的程序集,则不会为变量声明生成任何内容。至少不是直接的。最接近的是movq %rax, -8(%rbp)
移动init
into 的结果,但那是在两个函数调用之后。
对于NSObject *obj2;
,编译器不会生成任何代码。即使禁用优化器也不行。
那是因为变量声明不是表达式;除了为您(开发人员)提供一个用于保存值的标签之外,它实际上并没有做任何事情。只有当您实际使用该变量时才会生成代码。
因此,当您进入调试器时,它会跳过该行,因为无事可做。