去年(2019)底看到 Mike Ash关于 objc_msgSend
的文章,当时心里有点讶异:怎么Swift当道的时代,ObjC居然还在更新?!
objc_msgSend 的原型
从系统头文件将定义直接复制过来了,如下:
#if !OBJC_OLD_DISPATCH_PROTOTYPES
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ ) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#else
/**
* Sends a message with a simple return value to an instance of a class.
*
* @param self A pointer to the instance of the class that is to receive the message.
* @param op The selector of the method that handles the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method.
*
* @note When it encounters a method call, the compiler generates a call to one of the
* functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.
* Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper;
* other messages are sent using \c objc_msgSend. Methods that have data structures as return values
* are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
*/
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#endif
在OBJC_OLD_DISPATCH_PROTOTYPES
为真时,就是以前的定义,参数从左到右分别是实例对象,实例对象的方法,和可变参数。乍一看,这个定义非常的“正确”,而且objc_msgSend
在 runtime 中占据非常重要的地位。那为什么还会有改进呢?
ABI的匹配
对于原来的实现,最后的参数是一个可变参数,在转化成最终的函数调用时,系统需要将其转化成“固定”参数的调用。比如按照定义,调用者将参数 self 放入某个寄存器来传递,执行者去该寄存器取该参数,并认为是该类型的。但问题是,如果两者不一致问题就打了。而不同处理器架构上,这样的处理过程是不一样的。
Intel 架构对可变参数函数的处理
对标准的System V ABI for x86-64,参数是这样传递到寄存器的:
- 整型参数:依次使用 rdi, rsi, rcx, r8 和 r9。
- 浮点参数:使用 SSE 寄存器 xmm0 ~ xmm7 (每个128位)
当调用含可变参数函数时,可变参数的实际个数使用寄存器 al 存储;整型返回值放置在 rax 和 rdx,浮点型返回值放置在 xmm0 和 xmm1。
但是,当调用可变参数函数时,C语言中会将某些特定的数据类型字节数变宽:比 int 字节数少的会使用 int 的字节宽度,float 会使用 double 的字节数。对于整型数据而言,这不会有影响,因为数据优先存储在低位,高位为零。但是对于浮点数而言,float和double各个位数的定义不一样,不能像整型那样简单地进行高位填充。因此, 对于含可变参数的函数而言,传 float 类型的参数就会造成错误。
Mike Ashe 给了一个例子:
// Should use the old variadic prototype for objc_msgSend.
//#define OBJC_OLD_DISPATCH_PROTOTYPES 1
#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface Foo : NSObject @end
@implementation Foo
- (void)log: (float)x {
printf("%f\n", x);
}
@end
int main(int argc, char **argv) {
id obj = [Foo new];
[obj log: (float)M_PI];
objc_msgSend(obj, @selector(log:), (float)M_PI);
}
// output:
3.141593
3370280550400.000000
注:在Xcode中,可以通过设置 Enable Strict Checking of objc_msgSend Calls
为 NO
在模拟器上验证上述代码。
ARM64 架构对可变参数函数的处理
众所周知,iOS上使用的 ARM64 处理器,其使用的是 ARM64 标准 ABI 的变体:
- 整型参数:依次使用 x0 ~ x7。
- 浮点参数:依次使用 v0 ~ v7。
- 其余参数存储在栈上,返回值放置在对应的传参寄存器中。
对于含可变参数的函数,可变参数一直放置在栈上。因此,对于固定参数函数和可变参数函数而言,ABI 就不一致了。
新的 objc_msgSend
如开头所示,新的 objc_msgSend
定义: void objc_msgSend(void /* id self, SEL op, ... */ )
。在使用的时候,需要强制转换。作为对上述例子的“修正”,使用新 prototype 发送消息的时候需要指定参数、返回类型等:
((void (*)(id, SEL, float)) objc_msgSend)(obj, @selector(log:), (float)M_PI);
显而易见,通过这样的强制转换,让调用者不得不注意类型的一致性,从而避免异常问题发生。
需要说明的是,大概在Xcode6时代,新的 objc_msgSend 已经成为 Xcode中的默认选项。根据 Mike Ashe 的说明,之所以称为“New Prototype” 是因为把这个设置体现在了系统头文件里并在Apple的官方文档中体现了出来。
总结 Summary
严格的类型检查可以降低代码出现异常的几率,因此:
- 尽量使用“新”的
objc_msgSend
如果需要自己传递消息 - 对 Mac 平台,使用可变参数形式的
objc_msgSend
时要注意避免 float 参数
Comments