1.1 runtime 的概念api
iOS语言Objective-C是一门动态语言,其在runtime(运行时)会作不少事情,好比:消息的查找、消息的转发、动态属性的添加等等,可是runtime究竟是什么呢?runtime就是由C、C++、 汇编为OC提供运行时功能开发出来的一套api.xcode
1.2 runtime的使用方式 缓存
runtime的使用方式主要有三种 : ObjectIve-C调用 @selector()、NSObject的方法 NSSelectorFromString()、sel_registerName. 函数apisass
二.objc_msgSend 的初步了解
bash
首先咱们在main.m 文件里,建立了个LGPerson的对象并调用了sayNB的方法,又写了个C语言函数run,而且调用.好的那咱们如今看下底层在编译的时候到底作了哪些处理. 打开终端 输入命令:clang -rewrite-objc main.m -o main.cpp(输出个.cpp编译后的文件)打开main.cpp文件截取其中一段以下:app
咱们看到 sayNB方法底层编译经过objc_msgSend 底层查找函数的实现、默认带了两个参数 id和SEL id就是函数的接受者在这里就是person,SEL方法编号. 而run函数底层没有编译成objc_msgSend 那是由于C语言中函数名就是函数指针,它经过这个函数名直接就能找到函数实现并不须要objc_msgSend这一中转这一阶段.可是objc_msgSend到底作了什么呢?咱们先在代码里打个断点、打开工程里的Debug -> Debug Workflow - > Always Show Disassembly函数
就是打开汇编调试、咱们在方法上打个断点:oop
发现会进来下面汇编指令:post
sayCode下有个0x100000c63 <+67>: callq *0x397(%rip) ; (void *)0x00007fff63046000: objc_msgSend. 咱们在这行上打上断点而且按住control+xcode中step into进去发现指令会进入系统的libobjc.A.dylib的库里ui
接下来咱们就要在这个系统的库中继续研究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.im/post/5e0ca3…)
.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的查找和方法的动态解析!