1.1 runtime 的概念
iOS语言Objective-C是一门动态语言,其在runtime(运行时)会做很多事情,比如:消息的查找、消息的转发、动态属性的添加等等,但是runtime到底是什么呢?runtime就是由C、C++、 汇编为OC提供运行时功能开发出来的一套api.
1.2 runtime的使用方式
runtime的使用方式主要有三种 : ObjectIve-C调用 @selector()、NSObject的方法 NSSelectorFromString()、sel_registerName. 函数api
二.objc_msgSend 的初步了解
首先我们在main.m 文件里,创建了个LGPerson的对象并调用了sayNB的方法,又写了个C语言函数run,并且调用.好的那我们现在看下底层在编译的时候到底做了哪些处理. 打开终端 输入命令:clang -rewrite-objc main.m -o main.cpp(输出个.cpp编译后的文件)打开main.cpp文件截取其中一段如下:

我们看到 sayNB方法底层编译通过objc_msgSend 底层查找函数的实现、默认带了两个参数 id和SEL id就是函数的接受者在这里就是person,SEL方法编号. 而run函数底层没有编译成objc_msgSend 那是因为C语言中函数名就是函数指针,它通过这个函数名直接就能找到函数实现并不需要objc_msgSend这一中转这一阶段.但是objc_msgSend到底做了什么呢?我们先在代码里打个断点、打开工程里的Debug -> Debug Workflow - > Always Show Disassembly
就是打开汇编调试、我们在方法上打个断点:

发现会进来下面汇编指令:

sayCode下有个0x100000c63 <+67>: callq *0x397(%rip) ; (void *)0x00007fff63046000: objc_msgSend. 我们在这行上打上断点并且按住control+xcode中step into进去发现指令会进入系统的libobjc.A.dylib的库里

接下来我们就要在这个系统的库中继续研究objc_msgSend的流程了.
二.objc_msgSend 的进阶
首先我们去 www.opensource.apple.com/apsl/ 中下载目前苹果开源的私有库libobjc.A.dylib ,然后配置到我们的项目里.我们的objc_msgSend是由汇编写的(由汇编写的好处是:1.可以通过写一个函数保留未知的函数任意的跳转到另外个函数指针 2.汇编语言更接近机器语言 所以使用起来速度会更快)
开始在项目工程全局搜索 objc_msgSend 找到实现objc_msgSend的汇编文件

找到是这个 arm.s 的文件,那我们进文件里看下究竟吧.
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
// person - isa - 类
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
代码解析:
cmp p0, #0 判断是否是 nonepointerisa(是不是纯净的ISA) // person - isa - 类
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class还记得刚刚objc_msgsend 时会默认 传两个参数吗 .id 和 SEL,id 就是当前的对象 它的内存地址首地址是ISA,所以 这个对象 [xo]首地址位 就是isa .因为刚刚第一步已经判断出不是纯净的isa,所以自己的isa内部偏移16位得到 shiftClass也就是获取到当前对象所在的类.
CacheLookup NORMAL // calls imp or objc_msgSend_uncached接着在类的缓存里进行查找.
(对cache_t结构不是很熟悉的小伙伴们,建议看下上篇文章juejin.cn/post/684490…)
.macro CacheLookup
// p1 = SEL, p16 = isa
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
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
由ISA偏移16位得到类的cache,cache里有buckets 和 occupied|mask 分别由p10和p11进行接收。
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask用低32位的 p11接收mask,然后 & _cmd (也就是sel) 得到 (mask_t)(key & mask);就是将要在缓存池buckets 进行查找的索引.
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紧接着就在缓存池里进行查找如果 bucket里的sel和_cmd匹配的话说明在缓存中已经找到,缓存命中.如果没有找到就跳转 2f步骤,找到则返回 , 找不到 JumpMiss .
继续来到 __objc_msgSend_uncached -> MethodTableLookup
最后调用 bl __class_lookupMethodAndLoadCache3, 来到慢速查找流程 .至于慢速查找流程请期待下篇 《论objc_msgSend消息机制正传之消息查找》.
四.总结
当我们调用一个对象或者类方法的时候,系统会调用objc_msgSend进行汇编层面的查找,会什么先用汇编?因为快!我们详细的探索了整个过程,汇编层面主要是对已经缓存过的IMP进行搜索。搜索的路径是存放整个方法的类或者元类的cache_t的bucket!如果找到直接返回整个方法的实现,如果查不到,说明这个方法没有进行缓存!那这个方法是否存在呢?请看下一篇文章,objc_msgSend在C函数_class_lookupMethodAndLoadCache3的查找和方法的动态解析!