1、objc_msgSend是什么?
我们要分析objc_msgSend,首先我们要知道objc_msgSend是什么?带着问题找答案:
首先我们创建一个工程,
- 打开终端,
cd main.m所在的文件, - 然后
ls - 然后输入
clang -rewrite-objc main.m
这样就会在main.m所在的文件夹下有一个.cpp后缀的文件,打开后,到最底部main函数:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
}
return 0;
}
简化一下:
LGPerson *person = objc_msgSend(objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
}
现在是不是objc_msgSend(objc_getClass("LGPerson"), sel_registerName("alloc"))很熟悉,就是objc_msgSend(id self, sel _cmd)这个函数,通过这两行代码我们就可以得出;
- 方法的本质就是通过
objc_msgSend这个函数发送消息.
2、objc_msgSend底层的探索
通过我们看苹果开源的代码,我们发现objc_msgSend是用汇编写的,那这样写有什么好处呢?
- 速度快。汇编更加容易被机器语言识别。
- 为了解决一些隐藏的问题。比如说,我们经常会发送一些消息,有时候会有一些特殊类型的值,这些值对于我们静态的C/C++来说无法识别。
首先我们在源码中直接搜索objc_msgSend,找到汇编的类,从ENTRY _objc_msgSend开始:
补充:寄存器 可以看一下这篇iOS arm64汇编中寄存器和基本指令
汇编首先我们分析
cmp p0, #0 //cmp就是判断第一个寄存器中是否为空,如果为空说明当前没有接收者,就是下面这个判断
#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 // p16 = class
在这里将x0指向内存地址的值isa赋值给p13,然后通过GetClassFromIsa_p16返回当前的类class。
我们查找isa已经完毕,接下来CacheLookup也就是从缓存中查询,然后我们在源码中可以看到:
* CacheLookup NORMAL|GETIMP|LOOKUP //正常/获取IMP/慢速查找
// 我们这里分析正常情况NORMAL
.macro CacheLookup
// p1 = SEL, p16 = isa
//[x16, #CACHE] 平移16个字节得到cache,16个字节包括(isa:8字节,superClass:8字节)
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
上面整了一大堆,我们拆开来分析。其实这一部分:
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
- 就是类平移16个字节得到
cache_t,16个字节包括(isa:8字节,superClass:8字节), -
cache一个结构体,里面包含了结构体指针_buckets和_mask、_occupied -
p10里面存储的是bucket_t,p11里面存储的是_mask | _occupied - 然后
and w12, w1, w11就是进行_cmd & mask操作拿到散列表中的下标key - 拿到
key之后就进行add p12, p10, p12, LSL #(1+PTRSHIFT),就是拿到真正的buckets - 这一步
ldp p17, p9, [x12]就是拿到bucket_t里面的imp和sel。
拿到imp和sel之后进行判断
(上面这一部分其实跟cache_t里面bucket_t * cache_t::find(cache_key_t k, id receiver)这个函数实现的功能一样)
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
就是把传入的方法和本地存储的方法进行对比,如果一样进行CacheHit返回imp,缓存命中,不一样就是进入第2个判断
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
进入CheckMiss继续查找,然后进行cmp p12, p10 // wrap if bucket == buckets
判断,找到bucket之后又进行了第3个判断b.eq 3f,这是为什么?
add p12, p12, w11, UXTW #(1+PTRSHIFT)
这是为了存储一份方便下次查找。
然后我们看看CheckMiss是怎么执行的
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
在这段代码中我们可以知道我们进行的是NORMAL方式查找,所以执行cbz p9, __objc_msgSend_uncached,然后我们全局搜索它,来到
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
来到了这里,执行MethodTableLookup,我们再看一下这个方法:
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sq
//一些位移操作
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
// IMP in x0
mov x17, x0
//一些位移操作
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
由这里来到了__class_lookupMethodAndLoadCache3,进入到慢速查找的方法中。
汇编操作到这里就结束了,下面就要进入到了C/C++。
总结:
当我们调用一个对象或者类方法的时候,系统会调用
objc_msgSend函数进行的查找,而objc_msgSend是用汇编写的,为什么用汇编?
- 1、因为速度快!
- 2、在C语言中不可能通过写一个函数来保留位置的参数并且跳转到任意一个函数指针。C语言没有满足做这件事情的必要特性。
然后objc_msgSend是一个快速查找流程,是直接通过缓存里面直接去查找caceh_t,如果cache_t找不到,就通过慢速查找。
然后我们进入汇编: - 首先从
ENTRY _objc_msgSend开始 - 对当前消息的接受者进行
(id self, sel _cmd)判断处理 - 然后在进行
SUPPORT_TAGGED_POINTERS判断处理 - 然后进行
GetClassFromIsa_p16 p13位于运算处理得到class - 经过
CacheLookup查找相应的缓存,过程中我们通过分析cache_t处理bucket,经过哈希处理得到key对应的bucket - 找不到就进入到
CheckMiss,然后进入到__objc_msgSend_uncached - 开启一个新方法
STATIC_ENTRY __objc_msgSend_uncached,就是不从缓存里面找,开始进入到方法列表中查找 - 进入
MethodTableLookup查找之前先进行参数的准备save parameter register和sel、_cmd,调用C函数__class_lookupMethodAndLoadCache3,这样过渡到C函数的慢速查找。














网友评论