引言
Objective-C是通过消息机制调用方法的,编译器会把所有消息发送转为objc_msgSend方法调用。说到objc_msgSend的汇编实现,大多数人会觉的是因为性能高才用汇编实现,几乎没有文章说其它原因。Objective-C所有方法都会转为objc_msgSend方法调用,然而每个方法参数和返回值都可能不一样,参数和返回值要怎么处理?
- 本文首先会结合Objective-C Runtime机制深入分析objc_msgSend汇编实现。
- 本文最后会从Calling Conventions角度分析objc_msgSend实现,利用Calling Conventions和汇编还可以实现很多黑科技。
Objective-C对象结构
Objective-C中消息发送核心数据结构如下:
//以下代码均为arm64平台
typedef struct objc_class *Class;
typedef struct objc_object *id;
struct objc_object {
isa_t isa;
}
struct objc_class : objc_object {
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; //class_rw_t*
}
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
union isa_t
{
Class cls;
uintptr_t bits;
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
}
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
struct bucket_t {
cache_key_t _key;//实际上是selector
IMP _imp; //实际上是函数指针
}
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
}
NSObject子类的实例都有个isa指针,isa指向Class,Class有superclass、cache、实例方法、属性、protocol等Runtime信息,调用实例方法的时候就是通过isa指针找到Class,然后找到IMP调用实际的方法。
Class本身也是一个对象,也有isa指针,指向meta-class,meta-class也是一个对象,有类方法等属性,调用类方法的时候,就是通过Class对象的isa指针找到meta-class,然后找到IMP调用实际的方法。
实例、Class、meta-class关系如下图,图片来源:
消息机制
当编译器遇到一个方法调用时,它会将方法的调用翻译成以下函数中的一个,
objc_msgSend、 objc_msgSend_stret、 objc_msgSendSuper 和 objc_msgSendSuper_stret。
- 发送给对象的父类的消息会使用 objc_msgSendSuper ;
- 有数据结构作为返回值的方法会使用 objc_msgSendSuper_stret 或 objc_msgSend_stret ;
- 其它的消息都是使用 objc_msgSend 发送的。
objc_msgSend查找selector的IMP,然后调用实际的方法,主要包括以下流程:
- 查看cache是否有selector的IMP,如果有的话直接调用
- 如果没cache,最终会调用lookUpImpOrForward,从类方法列表查找IMP并缓存到cache
- 如果方法列表没有则会查找基类的方法,直到最上层基类(查找基类的时候也是先查找缓存,再查找方法列表)
- 如果基类也没查找到,则返回_objc_msgForward的IMP,走消息转发流程。
我们也可以自己通过class_getMethodImplementation拿到方法IMP(IMP是实际方法的函数指针),然后调用:
//[view addSubview:view2]
void (*funtion_pointer)(id, SEL, UIView*) = (void(*)(id, SEL, UIView*)) class_getMethodImplementation((id)view, @selector(addSubview:));
funtion_pointer(view, @selector(addSubview:), view2)
汇编源码
objc_msgSend汇编源码在Messengers.subproj目录,具体汇编如下:
objc_msgSend汇编代码不长,结合objc源码比较容易看懂。需要注意的是isa和TaggedPointer格式,isa指针不是纯粹的指针,还保存很多其它信息,具体可以参考isa_t union定义,其中只有3到35位才是class指针,所以查找之前会通过mask转换成class指针。
iOS系统为了提高性能和减小内存,使用了TaggedPointer来表示NSNumber、NSIndexPath等对象,对象并没有分配内存空间,而是把对象值保存在指针里面,只有指针无法容纳对象才会分配实际内存。TaggedPointer具体格式如下图,tag index表示具体Class,系统有维护一个全局映射表来保存tag index和Class的关系,具体可以查看objc_tag_index_t定义,查找到具体Class之后就跟正常oc对象一样查找IMP了。
Calling Conventions
arm64架构是通过q0-q7和x0-x7来传函数参数,可以看到objc_msgSend没对这几个寄存器做任何操作,找到IMP后直接通过br x17调用IMP,br告诉cpu不是子程序调用。
Objective-C所以方法发送都是通过objc_msgSend,每个方法返回值和参数都不一样,如果objc_msgSend像普通函数一样处理参数,为了处理不同参数类型和参数个数,可以使用varargs ,Objective-C调用的地方必须包裹成varargs,这样处理非常不灵活,objc_msgSend用了个很巧妙的技巧,就是对参数不做任何处理,查找到IMP后直接调用,因为在objc_msgSend开始执行时,栈帧(stack frame)的状态、数据,和各个寄存器的组合形式、数据,跟调用具体的函数指针(IMP)时所需的状态、数据,是完全一致的,所以我们用xcode调试的时候函数栈是看不到objc_msgSend,看上去就是消息发送过程完全没发生过,跟调用普通的c方法一摸一样。
黑科技
objc_msgSend用很巧妙的技巧处理参数问题,利用这种技巧可以做很多方法,比如可以实现Aspects的效果,在调用实际方法前做些hook操作,hook完后再调实际方法。也可以使用libffi处理参数问题,可以搞很多事情。
引用
Why objc_msgSend Must be Written in Assembly
面向切面 Aspects 源码阅读
What is a meta-class in Objective-C?
Objective-C 中的消息与消息转发