学习MJ的视频课程,整理总结知识点–objc_msgSend
本文主要从Class结构体出发,讲解class_rw_t
的结构及class_rw_t
中的methods
,及Class中的cache_t cache
。
objc_msgSend简介 OC中的方法调用,其实都是转换为objc_msgSend函数的调用
1 2 3 4 5 6 7 8 9 MJPerson *person = [MJPerson alloc] init]; [person personTest]; MJPerson *person = objc_msgSend(objc_msgSend(objc_getClass("MJPerson" ), sel_registerName("alloc" )), sel_registerName("init" )); objc_msgSend(person, sel_registerName("personTest" ));
可以看到,OC的函数调用转成消息发送 我们可以测试sel_registerName("personTest")
、@selector(personTest)
两个函数的地址是相等的,其实它们两个确实是等价的,只不过是在不同环境的不同表现形式。
所以我们可以认为OC的方法调用:消息机制,给方法调用者发消息
objc_msgSend的执行流程可以分为3大阶段
消息发送
动态方法解析 允许动态创建一个方法出来
消息转发 消息转发给另外的对象调用
objc_msgSend消息发送 关于消息发送,我们从objc_msgSend的源码开始解读,这一部分是汇编代码,可能是因为objc_msgSend的调用频率比较高,为了性能的考虑,使用了更底层的汇编来实现。在objc-msg-arm64.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 ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame MESSENGER_START cmp x0, #0 // nil check and tagged pointer check b.le LNilOrTagged // (MSB tagged pointer looks negative) nil的话跳转到LNilOrTagged ldr x13, [x0] // x13 = isa and x16, x13, #ISA_MASK // x16 = class LGetIsaDone : CacheLookup NORMAL // calls imp or objc_msgSend_uncached 缓存查找 参数 为NORMAL LNilOrTagged : b.eq LReturnZero // nil check nil的话直接return // tagged mov x10, #0xf000000000000000 cmp x0, x10 b.hs LExtTag adrp x10, _objc_debug_taggedpointer_classes add x10, x10, _objc_debug_taggedpointer_classes ubfx x11, x0, #60 , #4 ldr x16, [x10, x11, LSL #3 ] b LGetIsaDone LExtTag : // ext tagged adrp x10, _objc_debug_taggedpointer_ext_classes add x10, x10, _objc_debug_taggedpointer_ext_classes ubfx x11, x0, #52 , #8 ldr x16, [x10, x11, LSL #3 ] b LGetIsaDone LReturnZero : // x0 is already zero mov x1, #0 movi d0 , #0 movi d1 , #0 movi d2 , #0 movi d3 , #0 MESSENGER_END_NIL ret END_ENTRY _objc_msgSend
ENTRY
是入口的意思,_objc_msgSend先判断对象是否为空,为空时,直接return,不为空继续往下走。先难道isa去对应位置的缓存中找CacheLookup NORMAL
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 .macro CacheLookup // x1 = SEL, x16 = isa ldp x10, x11, [x16, #CACHE ] // x10 = buckets, x11 = occupied|mask and w12, w1, w11 // x12 = _cmd & mask add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4) ldp x9, x17, [x12] // {x9, x17} = *bucket 1: cmp x9, x1 // if (bucket->sel != _cmd) b.ne 2f // scan more CacheHit $0 // call or return imp 2: // not hit: x12 = not-hit bucket CheckMiss $0 // miss if bucket->sel == 0 cmp x12, x10 // wrap if bucket == buckets b.eq 3f ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket b 1b // loop 3: // wrap: x12 = first bucket, w11 = mask add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4) // Clone scanning loop to miss instead of hang when cache is corrupt. // The slow path may detect any corruption and halt later. ldp x9, x17, [x12] // {x9, x17} = *bucket 1: cmp x9, x1 // if (bucket->sel != _cmd) b.ne 2f // scan more CacheHit $0 // call or return imp 2: // not hit: x12 = not-hit bucket CheckMiss $0 // miss if bucket->sel == 0 cmp x12, x10 // wrap if bucket == buckets b.eq 3f ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket b 1b // loop 3: // double wrap JumpMiss $0 .endmacro
CacheLookup的汇编代码看不懂,但是它的注释写的挺详细,以及一些关键词,我们呢也能分析出大致的流程。 拿着isa在对应的class里面找cache及里面的buckets 根据哈希计算(_cmd & mask)的结果去buckets里面找 找到了调用CacheHit
,返回imp 没找到的话调CheckMiss
,用loop循环找 最终还没找到的话JumpMiss
因为参数是NORMAL
,JumpMiss
的处理流程会调用__objc_msgSend_uncached
因为是未缓存的方法,__objc_msgSend_uncached
流程内会调用MethodTableLookup
,也就是方法列表查找 方法列表查找内部调用__class_lookupMethodAndLoadCache3
我们搜索__class_lookupMethodAndLoadCache3
发现找不到匹配的关键字,因为在OC中,函数转到汇编函数时会多一个_
,我们去掉下划线,搜索_class_lookupMethodAndLoadCache3
,就来到了runtime的api
1 2 3 4 5 IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES , NO , YES ); }
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 IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { // Optimistic cache lookup // 需要从缓存中取 if (cache) { // 从缓存中取 imp = cache_getImp(cls, sel); // 取到了直接返回 if (imp) return imp; } // cls未初始化要先初始化 // 从当前cls取,取到了直接返回 // 说明缓存中没取到,去当前cls的方法列表找`getMethodNoSuper_nolock` { Method meth = getMethodNoSuper_nolock(cls, sel); if (meth) { // 找到了把此方法填入当前cls的缓存中 log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; goto done; } } // 当前cls没找到,回通过superclass指针去父类里面找,流程很类似,也是先从父类的缓存中找,没找到会去父类的方法列表中找 // 还是没找到,尝试动态解析 // No implementation found. Try method resolver once. _class_resolveMethod(cls, sel, inst); // goto retry; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static method_t *search_method_list(const method_list_t *mlist, SEL sel){ int methodListIsFixedUp = mlist->isFixedUp(); int methodListHasExpectedSize = mlist->entsize() == sizeof (method_t); if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1 )) { return findMethodInSortedMethodList(sel, mlist); } else { for (auto& meth : *mlist) { if (meth.name == sel) return &meth; } } }
核心点就是先去当前类的缓存中找,找不到去当前类的方法列表中找,还找不到去父类缓存中找,找不到去父类的方法列表中找,还找不到尝试走动态解析。 思路很清晰,没什么难点,可以通过一个图来表示这个过程
消息方法
objc_msgSend动态方法解析(resolve) 在lookUpImpOrForward
函数内先走消息查找逻辑,找不到时会走到动态方法解析阶段,这部分代码如下
1 2 3 4 5 6 7 8 9 10 11 if (resolver && !triedResolver) { runtimeLock.unlockRead(); _class_resolveMethod(cls, sel, inst); runtimeLock.read(); triedResolver = YES ; goto retry; }
方法找不到时,动态方法解析阶段内部会调用resolveInstanceMethod
或resolveClassMethod
方法,可以在这两个方法内部做动态添加函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void _class_resolveMethod(Class cls, SEL sel, id inst){ if (! cls->isMetaClass()) { _class_resolveInstanceMethod(cls, sel, inst); } else { _class_resolveClassMethod(cls, sel, inst); if (!lookUpImpOrNil(cls, sel, inst, NO , YES , NO )) { _class_resolveInstanceMethod(cls, sel, inst); } } }
假如我们调用一个未实现的对象方法[person test]
,程序运行时,调用test
方法时会走动态方法解析,我们实现resolveInstanceMethod
方法,并在此方法内部动态添加test
函数的实现,即可让程序继续正常执行。
当走到动态方法方法解析阶段时,lookUpImpOrForward
函数接续往下走,会goto retry
,也就是重新走消息发送流程。如果程序正确的动态添加了方法,那么消息发送流程会继续正确的执行,如果没有,会走到消息转发流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 - (void )other { NSLog (@"%s" , __func__); } + (BOOL )resolveInstanceMethod:(SEL)sel { if (sel == @selector (test)) { Method method = class_getInstanceMethod(self , @selector (other)); class_addMethod(self , sel, method_getImplementation(method), method_getTypeEncoding(method)); return YES ; } return [super resolveInstanceMethod:sel]; }
此时调用test
函数,实际上内部调用的是other
函数 如果是调用为实现的类方法,即给类发送消息,也会走同样的流程,不过在动态解析的时候,要记得是给类的元类动态添加方法。
动态方法解析
objc_msgSend消息转发(forward) 如果动态方法解析阶段没有找到方法的实现,会走到消息转发阶段
1 2 3 4 imp = (IMP)_objc_msgForward_impcache;
_objc_msgForward
具体流程不开源,我们在objc-msg-arm64.s
文件中搜索到如下汇编代码
1 2 3 4 5 6 7 ENTRY __objc_msgForwardadrp x17, __objc_forward_handlerldr x17, [x17, __objc_forward_handlerbr x17 END_ENTRY __objc_msgForward
但是在_objc_forward_handler
没有找到消息转发的相关实现。 同时,如果我们调用一个未实现的方法时,运行时会报错,类似如下
1 3 CoreFoundation 0x00007fff3077c8ef ___forwarding___ + 1485
关于___forwarding___
,也就是调用消息转发流程的关键,有人根据汇编写出C语言的伪代码。forwarding .c
___forwarding___.c
简化的伪代码如下
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 int __forwarding__(void *frameStackPointer, int isStret) { id receiver = *(id *)frameStackPointer; SEL sel = *(SEL *)(frameStackPointer + 8 ); const char *selName = sel_getName(sel); Class receiverClass = object_getClass(receiver); if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) { id forwardingTarget = [receiver forwardingTargetForSelector:sel]; if (forwardingTarget && forwardingTarget != receiver) { return objc_msgSend(forwardingTarget, sel, ...); } } if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) { NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel]; if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) { NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer]; [receiver forwardInvocation:invocation]; void *returnValue = NULL ; [invocation getReturnValue:&value]; return returnValue; } } if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) { [receiver doesNotRecognizeSelector:sel]; } kill(getpid(), 9 ); }
我们可以根据C的伪代码分析调用流程。
先判断是否实现forwardingTargetForSelector
,如果实现了此方法,且返回一个对象,那么会同时把这个消息转发到这个对象上去。
1 2 3 4 5 6 - (id )forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector (test)) { return [[MJCat alloc] init]; } return [super forwardingTargetForSelector:aSelector]; }
这个Demo中,是吧test方法这个消息,转发到MJCat
的实例对象,也就是去调用MJCat
对象的test
方法。这就是消息转发的由来。
如果没有实现forwardingTargetForSelector
方法,或者这个方法返回的对象为空,消息转发流程会继续走到methodSignatureForSelector
,拿方法的签名,拿到对象的方法签名后,会调用forwardInvocation
方法,返回值为空时,此时才调用doesNotRecognizeSelector
方法,抛出错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector (test)) { NSMethodSignature *methodSign = [NSMethodSignature signatureWithObjCTypes:"v@:" ]; return methodSign; } return [super methodSignatureForSelector:aSelector]; } - (void )forwardInvocation:(NSInvocation *)anInvocation { }
不仅实例方法支持动态方法解析、消息转发阶段,类方法也是支持的,因为消息发送的本质是要求接受者、方法名
。类对象也是属于对象,当然也是支持这个流程的。前面也提到过,实例方法、类方法其实没有本质区别,只不过存储的位置不一样而已。
消息转发
这一部分可以参考消息转发-Demo 进行试验。
总结 简介OC的消息机制: OC中的方法调用其实都转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名) objc_msgSend有3大阶段 消息发送(去类、父类中查找)、动态方法解析、消息转发
消息转发机制流程 先判断是否实现了forwardingTargetForSelector:
方法,如果实现了此方法且此方法的返回值不为nil,那么就会给返回值对象发送对应的消息SE
如果forwardingTargetForSelector
返回值为nil,那么会调用methodSignatureForSelector:
方法,如果此方法返回值不为nil,那么会调用forwardInvocation:
方法,否则调用doesNotRecognizeSelector:
方法。
参考和源码 objc4
源码:forwarding .c消息转发-Demo