前言
现在越来越多的app都使用了JSPatch实现app热修复,而JSPatch 能做到通过 JS 调用和改写 OC 方法最根本的原因是 Objective-C 是动态语言,OC 上所有方法的调用/类的生成都通过 Objective-C Runtime 在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法,也可以替换某个类的方法为新的实现,理论上你可以在运行时通过类名/方法名调用到任何 OC 方法,替换任何类的实现以及新增任意类。今天就来详细解析一下OC中runtime最为吸引人的地方。
目录
- 1.objc_msgSend函数简介
- 2.消息发送Messaging阶段—objc_msgSend源码解析
- 3.消息转发Message Forwarding阶段
- 4.forwardInvocation的例子
- 5.入院考试
- 6.Runtime中的优化
一.objc_msgSend函数简介
最初接触到OC Runtime,一定是从[receiver message]这里开始的。[receiver message]会被编译器转化为:
1 |
id objc_msgSend ( id self, SEL op, ... ); |
这是一个可变参数函数。第二个参数类型是SEL。SEL在OC中是selector方法选择器。
1 |
typedef struct objc_selector *SEL; |
objc_selector是一个映射到方法的C字符串。需要注意的是@selector()选择子只与函数名有关。不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。由于这点特性,也导致了OC不支持函数重载。
在receiver拿到对应的selector之后,如果自己无法执行这个方法,那么该条消息要被转发。或者临时动态的添加方法实现。如果转发到最后依旧没法处理,程序就会崩溃。
所以编译期仅仅是确定了要发送消息,而消息如何处理是要运行期需要解决的事情。
objc_msgSend函数究竟会干什么事情呢?从这篇「objc_msgSend() Tour」文章里面可以得到一个比较详细的结论。
- Check for ignored selectors (GC) and short-circuit.
- Check for nil target.
If nil & nil receiver handler configured, jump to handler
If nil & no handler (default), cleanup and return. - Search the class’s method cache for the method IMP(use hash to find&store method in cache)
-1. If found, jump to it.
-2. Not found: lookup the method IMP in the class itself corresponding its hierarchy chain.
If found, load it into cache and jump to it.
If not found, jump to forwarding mechanism.
总结一下objc_msgSend会做一下几件事情:
1.检测这个 selector是不是要忽略的。
2.检查target是不是为nil。
如果这里有相应的nil的处理函数,就跳转到相应的函数中。
如果没有处理nil的函数,就自动清理现场并返回。这一点就是为何在OC中给nil发送消息不会崩溃的原因。
3.确定不是给nil发消息之后,在该class的缓存中查找方法对应的IMP实现。
如果找到,就跳转进去执行。
如果没有找到,就在方法分发表里面继续查找,一直找到NSObject为止。
4.如果还没有找到,那就需要开始消息转发阶段了。至此,发送消息Messaging阶段完成。这一阶段主要完成的是通过select()快速查找IMP的过程。
二. 消息发送Messaging阶段—objc_msgSend源码解析
在这篇文章Obj-C Optimization: The faster objc_msgSend中看到了这样一段C版本的objc_msgSend的源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include id c_objc_msgSend( struct objc_class /* ahem */ *self, SEL _cmd, ...) { struct objc_class *cls; struct objc_cache *cache; unsigned int hash; struct objc_method *method; unsigned int index; if( self) { cls = self->isa; cache = cls->cache; hash = cache->mask; index = (unsigned int) _cmd & hash; do { method = cache->buckets[ index]; if( ! method) goto recache; index = (index + 1) & cache->mask; } while( method->method_name != _cmd); return( (*method->method_imp)( (id) self, _cmd)); } return( (id) self); recache: /* ... */ return( 0); } |
该源码中有一个do-while循环,这个循环就是上一章里面提到的在方法分发表里面查找method的过程。
不过在obj4-680里面的objc-msg-x86_64.s文件中实现是一段汇编代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
/******************************************************************** * * id objc_msgSend(id self, SEL _cmd,...); * ********************************************************************/ .data .align 3 .globl _objc_debug_taggedpointer_classes _objc_debug_taggedpointer_classes: .fill 16, 8, 0 ENTRY _objc_msgSend MESSENGER_START NilTest NORMAL GetIsaFast NORMAL // r11 = self->isa CacheLookup NORMAL // calls IMP on success NilTestSupport NORMAL GetIsaSupport NORMAL // cache miss: go search the method lists LCacheMiss: // isa still in r11 MethodTableLookup %a1, %a2 // r11 = IMP cmp %r11, %r11 // set eq (nonstret) for forwarding jmp *%r11 // goto *imp END_ENTRY _objc_msgSend ENTRY _objc_msgSend_fixup int3 END_ENTRY _objc_msgSend_fixup STATIC_ENTRY _objc_msgSend_fixedup // Load _cmd from the message_ref movq 8(%a2), %a2 jmp _objc_msgSend END_ENTRY _objc_msgSend_fixedup |
来分析一下这段汇编代码。
乍一看,如果从LCacheMiss:这里上下分开,可以很明显的看到objc_msgSend就干了两件事情—— CacheLookup 和 MethodTableLookup。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
///////////////////////////////////////////////////////////////////// // // NilTest return-type // // Takes: $0 = NORMAL or FPRET or FP2RET or STRET // %a1 or %a2 (STRET) = receiver // // On exit: Loads non-nil receiver in %a1 or %a2 (STRET), or returns zero. // ///////////////////////////////////////////////////////////////////// .macro NilTest .if $0 == SUPER || $0 == SUPER_STRET error super dispatch does not test for nil .endif .if $0 != STRET testq %a1, %a1 .else testq %a2, %a2 .endif PN jz LNilTestSlow_f .endmacro |
NilTest是用来检测是否为nil的。传入参数有4种,NORMAL / FPRET / FP2RET / STRET。
objc_msgSend 传入的参数是NilTest NORMAL
objc_msgSend_fpret 传入的参数是NilTest FPRET
objc_msgSend_fp2ret 传入的参数是NilTest FP2RET
objc_msgSend_stret 传入的参数是NilTest STRET
如果检测方法的接受者是nil,那么系统会自动clean并且return。
GetIsaFast宏可以快速地获取到对象的 isa 指针地址(放到 r11
寄存器,r10会被重写;在 arm 架构上是直接赋值到 r9)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
.macro CacheLookup ldrh r12, [r9, #CACHE_MASK] // r12 = mask ldr r9, [r9, #CACHE] // r9 = buckets .if $0 == STRET || $0 == SUPER_STRET and r12, r12, r2 // r12 = index = SEL & mask .else and r12, r12, r1 // r12 = index = SEL & mask .endif add r9, r9, r12, LSL #3 // r9 = bucket = buckets+index*8 ldr r12, [r9] // r12 = bucket->sel 2: .if $0 == STRET || $0 == SUPER_STRET teq r12, r2 .else teq r12, r1 .endif bne 1f CacheHit $0 1: cmp r12, #1 blo LCacheMiss_f // if (bucket->sel == 0) cache miss it eq // if (bucket->sel == 1) cache wrap ldreq r9, [r9, #4] // bucket->imp is before first bucket ldr r12, [r9, #8]! // r12 = (++bucket)->sel b 2b .endmacro |
r12里面存的是方法method,r9里面是cache。r1,r2是SEL。在这个CacheLookup函数中,不断的通过SEL与cache中的bucket->sel进行比较,如果r12 = = 0,则跳转到LCacheMiss_f标记去继续执行。如果r12找到了,r12 = =1,即在cache中找到了相应的SEL,则直接执行该IMP(放在r10中)。
程序跳到LCacheMiss,就说明cache中无缓存,未命中缓存。这个时候就要开始下一阶段MethodTableLookup的查找了。