重识 Objective-C Runtime - 看透 Type 与 Value

耐旱的仙人掌· 2016-11-07
本文来自 sunnyxx的博客 ,作者 耐旱的仙人掌

这是重识 Objective-C Runtime系列文章的其中一篇:

看透 Type 与 Value

对于 C 语言来说,Type 就个比较虚幻的东西,它唯一的目的便是让编译器知道一段数据的长度,来决定如何存取,举个例子:

int i = 123;char c = (char)i;

这段代码声明了一个 int 类型的变量和一个 char 类型的变量,有初始化和类型强转过程,在 x86_64 架构下,这两行代码的汇编如下:

movl $123, -4(%rbp)movl -4(%rbp), %eaxmovb %al, %clmovb %cl, -5(%rbp)

汇编看起来混乱,但却能最真实的反映出程序的运行过程,逐行解释下:

movl $123, -4(%rbp)

move 指令就是简单的值拷贝,这条指令中出现的 movl 表示按低 32 位的长度来拷贝(也就是一个 int 的长度),与之相似的还有 8 位的 movb(char)、16 位的 movw (short)、64 位的 movq (long in 64) 等;$123 即字面常量值;-4(%rbp) 代表 base pointer - 栈基地址寄存器,偏移 4 字节的位置。这个指令执行后内存如下所示:

1478480922648889.png

movl -4(%rbp), %eax

将刚才 4 字节长度内存赋值给 %eax 寄存器,它是最常用的通用寄存器之一,名为 accumulator,在 64 位架构下,rax 表示这个寄存器的完全体,eax 表示它的低 32 位,ax 表示低 16 位,ah 表示第 8~16 位,al 表示最低的 8 位。这样抠门的设计一部分因为兼容历史的 32 架构,一方面也是为了更充分利用寄存器这个宝贵的资源:

1478480923294523.png

movb %al, %cl

按 8 位长度 (char) 将 a 寄存器的最低 8 位移动到 c 寄存器(count register)的低 8 位。这一个指令就在做 int 到 char 的类型转换,把 123 存在寄存器的低 32 位上,再把寄存器的最低 8 位取出来,相当于把 00000000000000000000000001111011 截断成了 01111011。

movb %cl, -5(%rbp)

最后,再把刚才的结果按 8 字节的长度拷贝到 %rbp 偏移 5 的位置,完成这个 char 类型栈变量的赋值:

1478480923810031.png

因此,对于 C 这种静态语言,Type 信息只用于编译器解析,除了静态检查外还影响生成:

  1. 相应长度的指令 (是 movq、movl 还是 movb ?)

  2. 寄存器长度的选用(是 rax、eax 还是 al ?)

  3. 栈变量内存大小的确定,也可以说是 sp 的位置( sp 表示 Stack Pointer, 它和 Base Pointer 配合管理栈内存的分配与回收,所谓“分配”栈内存只是用如 subq $32, %rsp 的指令将 sp 向低地址移动)

然而,对于动态语言,Type 不仅在编译期起到上述作用,还需要保留到运行时,让动态调用得以实现,被称作 Type Encodings,对于 Objective-C 所有 Type 的编码,都可以在这个官方文档中查到,里面的编码和用 @encode() 生成的一致,比如:

@encode(int) => "i"@encode(float) => "f"@encode(id) => "@"@encode(SEL) => ":"@encode(CGRect) => "{CGRect={CGPoint=dd}{CGSize=dd}}" // 64

Objective-C Class 中每个实例变量的 Type 信息全部被编码,Runtime 也提供了 ivar_getTypeEncoding 来访问。
同时,为支持消息的转发和动态调用,Objective-C Method 的 Type 信息也被以 “返回值 Type + 参数 Types” 的形式组合编码,还需要考虑到 self 和 _cmd 这两个隐含参数:

- (void)foo; => "v@:"- (int)barWithBaz:(double)baz; => "iv@:d"

注:上面的方法的 Encoding 使用新的格式,旧的格式中包含调用栈大小和布局信息,如 i24@0:8i16i20,表示调用栈帧共 24 字节大小,后面每个参数跟着的数字表示该参数在调用栈的偏移值,在 x86_64 和 ARM 成为主流后,调用的 Calling Conventions 发生巨大变化,开始借助寄存器传参,所以在“参数压栈”时代的这种编码方式逐渐被废弃。

方法的编码可以使用 method_getTypeEncoding 获取,在 Cocoa 层,被 NSMethodSignature 封装,并提供了一些便捷的解析方法。

多说一句,纯 Swift 声称自己是静态的语言,因为在编译后,任何结构都会被 Name Mangling 压缩成一个符号,比如下面的方法:

class Sark {    func foo(bar: Int) -> Int {        return bar;    }}

经过 Name Mangling 的符号是 _TFC12TestSwift4Sark3foofT3barSi_Si,虽然把结构都拍扁了,但该有的信息都在,Module、Class、Method、参数和返回值类型等,按照一定的格式进行了编码,感兴趣可以看这篇文章