1、objc_msgSend相关函数
1.1 函数说明
我们知道,OC的方法调用在底层会被编译成objc_msgSend函数进行调用。
objc_msgSend基础的函数主要有以下2个:
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
这两个函数主要用于消息的发送。
通过objc/message.h头文件,在arm64上,只有这两个函数可用。
在其他架构上,还有几个变形的函数,分别如下:
(1)objc_msgSend_stret:返回结构体类型的消息发送
/* Struct-returning Messaging Primitives
*
* Use these functions to call methods that return structs on the stack.
* On some architectures, some structures are returned in registers.
* Consult your local function call ABI documentation for details.
*
* These functions must be cast to an appropriate function pointer type
* before being called.
*/
objc_msgSend_stret(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ARM64_UNAVAILABLE;
OBJC_EXPORT void
objc_msgSendSuper_stret(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ARM64_UNAVAILABLE;
(2)objc_msgSend_fpret,objc_msgSend_fp2ret:返回浮点数类型的消息转发
/* Floating-point-returning Messaging Primitives
*
* Use these functions to call methods that return floating-point values
* on the stack.
* Consult your local function call ABI documentation for details.
*
* arm: objc_msgSend_fpret not used
* i386: objc_msgSend_fpret used for `float`, `double`, `long double`.
* x86-64: objc_msgSend_fpret used for `long double`.
*
* arm: objc_msgSend_fp2ret not used
* i386: objc_msgSend_fp2ret not used
* x86-64: objc_msgSend_fp2ret used for `_Complex long double`.
*
* These functions must be cast to an appropriate function pointer type
* before being called.
*/
1.2 例子
下面以arm64举例说明,声明下面两个类。
@interface XTPerson : NSObject
@property (copy, nonatomic) NSString *name;
@end
@implementation XTPerson
@synthesize name = _name;
- (void)work {
NSLog(@"%s", __func__);
}
- (CGRect)myRect {
NSLog(@"%s", __func__);
return CGRectMake(0, 0, 20, 20);
}
- (CGFloat)height {
NSLog(@"%s", __func__);
return 1.8f;
}
- (NSString *)name {
NSLog(@"%s", __func__);
return _name;
}
- (void)setName:(NSString *)name {
NSLog(@"%s", __func__);
_name = name;
}
@end
@interface XTWorker : XTPerson
- (void)work;
@end
@implementation XTWorker
- (void)work {
NSLog(@"worker work");
}
@end
(1)objc_msgSend调用
以arm64为例,在真机上如下调用:
XTPerson *person = [[XTPerson alloc] init];
void* (*objc_msgSend_func) (id, SEL, ...);
objc_msgSend_func = (void* (*) (id, SEL, ...)) objc_msgSend;
objc_msgSend_func(person, @selector(work));
[person setName:@"Peter"];
NSLog(@"%@", objc_msgSend_func(person, @selector(name)));
可以看出进行函数指针的转换后,可以通过函数指针对objc_msgSend函数进行调用。
(2)objc_msgSendSuper调用
下面的例子说明如何去调用worker父类的work方法。
XTWorker *worker = [[XTWorker alloc] init];
void* (*objc_msgSendSuper_func) (struct objc_super *, SEL, ...) = (void* (*) (struct objc_super *, SEL, ...))objc_msgSendSuper;
struct objc_super xtSuper = {worker, [XTPerson class]};
objc_msgSendSuper_func(&xtSuper, @selector(work));
2、汇编过程
objc_msgSend实际上做的就是从某个sel查找对应的函数地址的过程。
2.1 函数入口
通过对苹果开源的objc源码可以看出,objc_msgSend函数入口在底层是以汇编形式进行实现。
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
从以上代码可以看出,首先会判断消息的发送者是否为nil,如果是,则直接返回。
2.2 CacheLookup
如果接受者不为nil,将isa赋值到p13中,通过isa获取class对象,并存放在p16中,然后走CacheLookup,只看amr64,实现如下:
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
#endif
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
// wrap-around:
// p10 = first bucket
// p11 = mask (and maybe other bits on LP64)
// p12 = _cmd & mask
//
// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
// So stop when we circle back to the first probed bucket
// rather than when hitting the first bucket again.
//
// Note that we might probe the initial bucket twice
// when the first probed slot is the last entry.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#endif
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
LLookupEnd\Function:
LLookupRecover\Function:
b \MissLabelDynamic
上述过程其实是从cache缓存中查找某个sel对应的函数地址的过程,伪代码大概如下:
p10 = mask|buckets
buckets = p10 & #0xffffffffffff
mask = p10 >> 48
index = cmd & mask
for (i = index; i >= 0; i--) {
if (buckets[i].sel == search_sel) {
goto CacheHit
} else {
if (buckets[i].sel == NULL) {
goto MissLabelDynamic
} else {
//查找下一个缓存位置
}
}
}
for (i = mask; i > index; ++i) {
if (buckets[i].sel == search_sel) {
goto CacheHit
} else {
if (buckets[i].sel == NULL) {
goto MissLabelDynamic
} else {
//查找下一个缓存位置
}
}
}
CacheHit:
XX
MissLabelDynamic:
_objc_msgSend_uncached
问题1:为什么index初始化为mask&cmd
因为对于每个sel,其存放在缓存中的位置序号是按照以下函数计算。
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
}
问题2:为什么buckets[index]有值,但是不等于查找的sel时,index--?
因为缓存存放发送哈希冲突时,在arm64时,是通过index--来解决冲突。具体如下:
#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
#else
#error unexpected configuration
#endif
2.3 objc_msgSend_uncached
如果缓存找到直接调用函数,如果没有找到,调用_objc_msgSend_uncached。
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band r10 is the searched class
// r10 is already the class to search
MethodTableLookup NORMAL // r11 = IMP
jmp *%r11 // goto *imp
END_ENTRY __objc_msgSend_uncached
其核心代码是MethodTableLookup
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
.if $0 == NORMAL
// receiver already in a1
// selector already in a2
.else
movq %a2, %a1
movq %a3, %a2
.endif
movq %r10, %a3
movl $$3, %a4d
call _lookUpImpOrForward
// IMP is now in %rax
movq %rax, %r11
RESTORE_REGS MSGSEND
.if $0 == NORMAL
test %r11, %r11 // set ne for nonstret forwarding
.else
cmp %r11, %r11 // set eq for stret forwarding
.endif
.endmacro
这里会调用lookUpImpOrForward。
该实现是一个C函数,从而退出整个汇编的过程。
从上述的过程中可以看出,汇编代码实现的是从缓存中查找sel对应的impl,如果缓存没有找到,则进入lookUpImpOrForward流程。
2.4 总的流程图
上述整个过程的流程图如下: