Aug 6th, 2018,更新:添加思维导图
Aug 5th, 2018,更新:添加部分参考链接;
基础要求
有一定C/C++,OC,汇编基础,因为主要涉及苹果开源库Runtime和CoreFoundation相关代码的阅读和理解,文章涉及C代码和部分汇编代码的阅读,实践有限。
Runtime初探
有英文基础的,推荐使用官方在线文档Objective-C Runtime,还有官方已经不再Updated的文档,有兴趣可以了解一下 Objective-C Runtime Programming Guide(官方已停止更新的文档),文档中的信息有限,但是对于想要了解一下Runtime的还是很充足的,这篇文章主要关注的是比较深入的问题,主要都是在阅读代码并追踪代码逻辑来分析。
OC是一门面向对象的动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。对于消息传递和转发,看过大神iOS程序犭袁的《招聘一个靠谱的 iOS》—参考答案(三)的人相信对Runtime消息传递中的obj_msgSend
和obj_msgForward
有所了解,objc_msgSend
在官网上还有一些解释,但是如果想要了解更多,还是需要看源码的~objc_msgForward
也是,如_class_lookupMethodAndLoadCache3
,lookUpImpOrForward
等。
因为OC的特性,所以编译时,会把OC的代码转成C/C++代码,感兴趣的可以在终端上,cd
到main.m
所在到目录后,使用
clang -rewrite-objc main.m
重新编译生成对应的main.cpp
文件。
在OC中,方法调用 就是消息传递的过程,比如在main.m中定义
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
[p eat];
}
return 0;
}
对应的C/C++代码就是
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("eat"));
}
return 0;
}
可以看出来,使用的就是id objc_msgSend(id self, SEL op, ...)
来向指定的对象发消息,objc_msgSend的作用就是处理发送到指定对象(self)
的消息(op)
。
在 方法调用 中,OC代码会先被编译成C/C++代码,即使用obj_msgSend
传递消息,如果(op)
没有对应的实现(IMP)
时,objc_msgSend
会先尝试 动态解析方法(Dynamically Resolving Methods) ,如果还是没有找到对应的实现,则会返回_obj_msgForward
继而进入 消息转发(Forwarding Messages) ,包含备用接收者和完整消息转发两种,如果还是没有找到对应的实现,则会进入 出错处理(Error Handling),返回NSObject
的doesNotRecognizeSelector
,整个方法调用的流程就是这样,越早处理,代价越小,直到引起崩溃。
整理后,大致的过程是这样,具体的过程在深入部分讲解:
- 动态解析方法,重载
resolveInstanceMethod:
(或resolveClassMethod:
)方法,为该 Class 动态添加实现,返回YES,被标记,然后重新开始,对象会返回新的实现以被obj_msgSend
使用;如果仍没实现,往下; - 消息转发之备用接收者,重载
forwardingTargetForSelector:
方法,重载该方法,存在可以接受消息的对象,则返回非 nil 对象。否则返回 nil ,继续往下;(注意,这里不要返回 self ,否则会形成死循环)
- 消息转发之消息重定向,重载
methodSignatureForSelector:
和forwardInvocation:
方法,Runtime先调用methodSignatureForSelector:
将选择器SEL的信息封装到NSInvocation
对象中,然后调用forwardInvocation:
时作为参数传入,而forwardInvocation:
则可将消息发送给任何指定的对象处理。 - 出错处理,调用
doesNotRecognizeSelector:
方法,一般使用的是NSObject,不需要重载
以上的方法,可以在我的GitHub Demo中找到相关演示,都是声明在基类 NSObject 中
调用的顺序可以自行验证,如使用(void)instrumentObjcMessageSends(BOOL)
打印Runtime日志,篇幅所致,读者可自行百度用法,或参考本文所使用的GitHub代码。
Runtime深入
以上是从了解如何使用消息的传递和转发机制角度来探索Runtime,比较容易理解,Runtime作为OC核心的内容,是很复杂的,实际上还有很多疑问,如,Runtime是如何实现消息的传递和转发的?以下是我参考搜索到的文章,总结出来的,理解不一定够,希望在写下这个以后能有进一步的理解,文末总结附上思维导图。
下载Runtime开源库,通过开源的库我们就可以发现,整个Runtime库是由C语言和汇编语言写的,其中,obj_msgSend
就是用汇编语言写的,以下分析时使用的是obj-msg-arm64.s
汇编源码中包含一些注释,其实阅读起来很是很简单的,就是没有称手的工具可以像成熟IDE那样快速浏览,因为使用的是汇编语言,虽然速度上会快很多,但是需要适应不同平台不同的代码,其中obj-msg-arm64.s
是arm64上的,关键代码如下:
1.进入汇编,开始调用_objc_msgSend方法
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)
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
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]
b LGetIsaDone
LExtTag:
// 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
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
2.通过宏CacheLookup进入缓存寻找SEL
_objc_msgSend调用了宏CacheLooup
,其中,CacheLooup
是再缓存中寻找对应selector的实现IMP,如果在缓存中找到了IMP,则调用IMP,如果没有找到,则会跳转到__objc_msgSend_uncached
。
此汇编文件中也包含了CacheLooup的代码:
/********************************************************************
*
* CacheLookup NORMAL|GETIMP|LOOKUP
*
* Locate the implementation for a selector in a class method cache.
*
* Takes:
* x1 = selector
* x16 = class to be searched
*
* Kills:
* x9,x10,x11,x12, x17
*
* On exit: (found) calls or returns IMP
* with x16 = class, x17 = IMP
* (not found) jumps to LCacheMiss
*
********************************************************************/
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
.macro CacheHit
.if $0 == NORMAL
MESSENGER_END_FAST
br x17 // call imp
.elseif $0 == GETIMP
mov x0, x17 // return imp
ret
.elseif $0 == LOOKUP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz x9, LGetImpMiss
.elseif $0 == NORMAL
cbz x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
.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
3.缓存中没有找到,进入__objc_msgSend_uncached方法
然后就可以顺藤摸瓜找到__objc_msgSend_uncached
,这个方法是在未缓存的方法列表中寻找SEL,找到后就掉用,由源码可知方法中又跳到了宏MethodTableLookup
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search
MethodTableLookup
br x17
END_ENTRY __objc_msgSend_uncached
宏MethodTableLookup
是在方法列表中寻找,调用了方法__class_lookupMethodAndLoadCache3
,寻找IMP,并把IMP添加到缓存
.macro MethodTableLookup
// push frame
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
// imp in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
.endmacro
4.调用__class_lookupMethodAndLoadCache3进入方法列表中寻找
__class_lookupMethodAndLoadCache3
没有被放在汇编文件中,而是在开源的文件objc-runtime-new.mm中。
__class_lookupMethodAndLoadCache3
调用了lookUpImpOrForward()
,其作用时寻找IMP并缓存起来,如果没有找到的话,则会进入_class_resolveMethod()
尝试寻找动态解析方法,并且因为lookUpImpOrForward()
的参数之一resolver
为YES,所以,肯定都是允许动态解析的
/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.read();
if (!cls->isRealized()) {
// Drop the read-lock and acquire the write-lock.
// realizeClass() checks isRealized() again to prevent
// a race while the lock is down.
runtimeLock.unlockRead();
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
retry:
runtimeLock.assertReading();
// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
5.调用_class_resolveMethod()方法尝试动态解析
_class_resolveMethod()
的源码在objc-class.mm中
/***********************************************************************
* _class_resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
assert(cls->isMetaClass());
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
如上面Runtime初探可知,如果对象实现了resolveInstanceMethod(或resolveClassMethod)则resolved
为YES,对应的IMP也会被缓存起来。由步骤4的lookUpImpOrForward()
可知,动态解析方法后,不管成功与否,都会进行一次重试,但也仅仅一次,如果实现了相应的动态解析方法,则因为缓存中已经被加入了新的IMP,所以会顺利执行IMP,从而结束整个objc_msgSend
;如果未重载实现动态解析方法,则在因为缓存中没有IMP而且因为lookUpImpOrForward()
中的triedResolver
已经被置为YES,所以会返回一个默认的IMP_objc_msgForward_impcache
,并缓存之。
6.进入转发
因为lookUPImpOrForward()
返回的是一个_objc_msgForward_impcache
,在汇编文件__objc_msgSend_uncached
的宏MethodTableLookup
执行完以后会被调用,执行消息转发机制
/********************************************************************
*
* id _objc_msgForward(id self, SEL _cmd,...);
*
* _objc_msgForward is the externally-callable
* function returned by things like method_getImplementation().
* _objc_msgForward_impcache is the function pointer actually stored in
* method caches.
*
********************************************************************/
STATIC_ENTRY __objc_msgForward_impcache
MESSENGER_START
nop
MESSENGER_END_SLOW
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr x17, [x17, __objc_forward_handler@PAGEOFF]
br x17
END_ENTRY __objc_msgForward
7.消息转发机制,objc_setForwardHandler设置__objc_forward_handler
以下部分没有找到对应的源码信息,都是从网络上整理过来的,
objc_setForwardHandler
的调用不是在Runtime中,而是在 CoreFoundation 源文件CFRuntime.c中的__CFInitialize()
,不过苹果公布的源码中没有包含相关代码的调用,网络上大神: Hmmm, What's that Selector?中文可以参考这个,如图:
红色标出的那三个指令就是把
__CF_forwarding_prep_0
和
___forwarding_prep_1___
作为参数调用
objc_setForwardHandler
方法。
再由下面这个崩溃时堆栈抛出的信息来看,可以看出
_CF_forwarding_prep_0
函数调用了
___forwarding___
函数,接着又调用了
doesNotRecognizeSelector
方法,最后抛出异常.
而消息转发的逻辑几乎全在
__forwarding__
中:
void __forwarding__(BOOL isStret, void *frameStackPointer, ...) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 4);
Class receiverClass = object_getClass(receiver);
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget) {
return objc_msgSend(forwardingTarget, sel, ...);
}
}
const char *className = class_getName(object_getClass(receiver));
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix);
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"-[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
sel_getName(sel),
receiver);
}
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
sel_getName(sel),
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature
frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}
const char *selName = sel_getName(sel);
SEL *registeredSel = sel_getUid(selName);
if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} else if (class_respondsToSelector(receiverClass, @selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}
// [The point of no return](http://www.youtube.com/watch?v=AyyCz9z1s_M).
kill(getpid(), 9);
}
基本逻辑就是按照上面Runtime初探中那样运行的。
实际测试中,在我的Demo里,如果我实现了所有上述5
个方法后,即+(BOOL)resolveInstanceMethod:(SEL)sel
、+(BOOL)resolveClassMethod:(SEL)sel
、-(id)forwardingTargetForSelector:(SEL)aSelector
、-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
、-(void)forwardInvocation:(NSInvocation *)anInvocation
,并故意让5
个方法都被执行到,最后再执行doesNotRecognizeSelector:
,理想中应该是每个方法执行一次,但是实际情况是resolveInstanceMethod:
会被执行了两次,而且涉及到一个方法_forwardStackInvocation:
,输出如下:
2018-08-05 21:29:49.317390+0800 MCRnRExploration[11739:1817234] Eating
2018-08-05 21:29:49.318847+0800 MCRnRExploration[11739:1817234] +[Person resolveInstanceMethod:] recv run, order:1
2018-08-05 21:29:49.319489+0800 MCRnRExploration[11739:1817234] -[Person forwardingTargetForSelector:] recv run, order:2
2018-08-05 21:29:49.320052+0800 MCRnRExploration[11739:1817234] -[Person methodSignatureForSelector:] recv run, order:3
2018-08-05 21:29:49.320643+0800 MCRnRExploration[11739:1817234] +[Person resolveInstanceMethod:] recv _forwardStackInvocation:, order:4
2018-08-05 21:29:49.321243+0800 MCRnRExploration[11739:1817234] -[Person forwardInvocation:] recv run, order:5
2018-08-05 21:29:49.321742+0800 MCRnRExploration[11739:1817234] -[Person run]: unrecognized selector sent to instance 0x1004869f0
2018-08-05 21:29:49.324693+0800 MCRnRExploration[11739:1817234] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person run]: unrecognized selector sent to instance 0x1004869f0'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff48d7a2fb __exceptionPreprocess + 171
1 libobjc.A.dylib 0x00007fff6f6ebc76 objc_exception_throw + 48
2 CoreFoundation 0x00007fff48e12da4 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
3 MCRnRExploration 0x0000000100001bab -[Person forwardInvocation:] + 251
4 CoreFoundation 0x00007fff48cf03ac ___forwarding___ + 748
5 CoreFoundation 0x00007fff48cf0038 _CF_forwarding_prep_0 + 120
6 MCRnRExploration 0x0000000100001c72 main + 130
7 libdyld.dylib 0x00007fff702da145 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
调查后发现,在Foundation.framework的NSXPSDistantObject类中,iOS11.1中发生了变化,新增了一个方法-(void)_forwardStackInvocation:(id)arg1,而在之前的iOS10.2中就没有,这个问题暂时就搁着了,有高手了解的话,可以评论里分享一下,谢谢了!!!
总结
总结成思维导图如下(图片较大,推荐使用查看原图)
相关文章链接:
GitHub消息传递及消息转发Demo
iOS11.1 NSXPSDistantObject
iOS10.2 NSXPSDistantObject
iOS 常见知识点(一):Runtime
objc_setForwardHandler的注册和实现参考
▼▼▼
Hmmm, What's that Selector?
Objective-C 消息发送与转发机制原理
▲▲▲
以上两篇需要对苹果开源库Runtime和CoreFoundation有一定了解,至少也要求下载这两个库,着重在CoreFoundation上,因为objc_setForwardHandler是在CoreFoundation的CFRuntime中被使用的
相关资料下载路径:
Runtime开源库(压缩包下载)
Runtime源码库(在线浏览)
CoreFoundation开源库(压缩包下载)
CoreFoundation开源库(在线浏览)
GitHub上Runtime源码库
Objective-C Runtime
Objective-C Runtime Programming Guide(官方已经停止更新,推荐上面那个)