Runtime 方法调用本质
OC是一门runtime语言,OC调用方法的实际,其实就是消息转发,我们可以通过底层C++源码来探索方法调用的本质:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
举个例子:
OC:[person test];
C++:((void(*)(id, SEL))(void*)objc_msgSend)((id)person, sel_registerName("test"));
通过这段源码可以看出c++底层源码中方法调用最终都是通过objc_msgSend函数,这种模式在OC领域中叫做消息机制,表示给方法的调用者发送消息,上面中person代表消息接受者;test代表消息名称
消息几个过程
消息发送阶段:负责从类或者父类的缓存列表或者baseMethodList方法列表中查找方法;
动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现
消息转发阶段:如果消息发送阶段没有找到方法,动态解析又没有添加对应的方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理。
如果前面这几个过程都没有实现到的话,那么就会爆出一个经典的错误方法:unrecognzied selector sent to instance,接下来我们来细看这几个步骤:
消息发送阶段
在runtime源码中搜索_objc_msgSend查看其源码内部实现,在objc-msg-arm64.s汇编文件中可以查看到_objc_msgSend函数的实现流程图和源码图:
_objc_msgSend流程图 _objc_msgSend函数上述汇编源码中会首先判断消息接受者x0,是否为nil,如果为nil则会执行LNilOrTagged,LNiOrTagged内部会执行LReturnZero(内部直接return 0);
如果传入的消息接受者不为nil,这直接往下执行,CacheLookUp,CacheLookUp内部会对方法列表进行查找,如果找到则执行CacheHit,进而调用方法,否则执行CheckMiss,CheckMiss内部调用__objc_msgSend_uncached
CacheLoopup可以看到CacheLookup内部方法缓存列表进行查找,如果找到则执行CacheHit,进而调用方法,否则就会执行CheckMiss,CheckMiss内部调用__objc_msgSend_uncached.
__objc_msgSend_uncached__objc_msgSend_uncached 内部会执行MethodTableLookup也就是方法列表查找
__class_lookupMethodAndLoadCache3__class_lookupMethodAndLoadCache3 是汇编语言,而对于方法_class_lookupMethodAndLoadCache3在汇编中会生成__class_lookupMethodAndLoadCache3,所以这个时候我们发现我们能在C++源码中找到这个方法的实现_class_lookupMethodAndLoadCache3
部分源码图具体全部源码可以直接找源码来看,这里不一一描述,下面我将通过一张图来说明一下这个流程先:
_class_lookMethodAndLoadCache3内部流程总结图:
总结图动态解析阶段
接上面流程图的红色部分继续寻找下去,该部分的源码是:
_class_resolveMethod动态解析入口 _class_resolveMethod动态内部实现从内部代码中实现可以看到动态解析方法之后,会将triedResolver = YES;那么下次就不会在进行动态解析阶段了,虽然会重新执行retry,会重新对方法查找一遍,但是之后无论我们有没有动态解析成功,都不会再次进行动态解析。
动态解析方法的实现和验证
动态解析对象方法时,会调用+(BOOL)resolveInstanceMethod:(SEL)sel方法。
动态解析类方法时,会调用+(BOOL)resolveClassMethod:(SEL)sel方法。
Person 类动态解析 动态解析测试上面的代码中可以看出,person在调用方法test时候,test其实在person.m中并没有实现,但是可以看到[person test]之后并没有爆出错误,反而能执行成功,通过上面对消息发送我们知道,当本类和父类cache和class_rw_t中都找不到方法时,就会进行动态解析的方法,也就是说会自动调用类的resolveInstanceMethod:方法进行动态查找,因此当我们手动执行resolveInstanceMethod的时候,可以按照我们的逻辑自己来实现,从上面中可以看到我们resolveInstanceMethod方法可以看到内部使用到了class_addMethod,并且用other方法类替代test方法的实现,接下来看看class_addMethod函数具体实现
class_addMethod 函数
class_addMethod(__unsafe_unretainedClass cls, SEL name, IMP imp,constchar*types)
第一个参数:cls给那个类添加方法
第二个参数:SEL name添加方法的名称
第三个参数:IMP imp方法的实现
第四个参数:types方法类型
另外还有一个class_getInstanceMethod方法获取Method,这个Method其实是一个objc_method类型结构体,其实也可以理解为内部结构同method_t结构体相同,method_t是代表方法的结构体,其内部包含SEL、type、IMP,我们通过自定义method_t结构体,将objc_method强转为method_t查看方法是否能够动态添加成功
objc_method强转为method_t从终端打印信息可以看出,我们的猜测是对的
不依赖method_getImplementation函数和method_getTypeEncoding函数获取方法的imp和type,手动书写,如下图所示:
手动书写动态解析类方法
类方法动态解析无论我们是否实现了动态解析的方法,系统内部都会执行retry对方法再次进行查找,那么如果我们实现了动态解析方法,此时就会顺利查找到方法,进而返回imp对方法进行调用。如果我们没有实现动态解析方法。就会进行消息转发。
动态解析流程图:
动态解析方法流程图消息转发阶段
从源码中可以看出,如果我们经过消息发送阶段找不到,然后到消息动态解析,都还没有找到消息实现者的话,就会信息消息转发阶段,会调用_objc_msgForward_imcache函数,发现在C++源码中找不到_objc_msgForward_imcache函数,但是我们可以在汇编中找到__objc_msgForward_impcache函数中找到__objc_msgForward进而找到__objc_forward_handler
__objc_msgFoward_impcache __objc_forward_handler从这里可以看到这个方法仅仅只是一个错误的信息输出,因为消息转发机制是不开源的,但是还是有一个消息转发的过程,那应该这个消息转发有可能是返回了某种类型的对象,该对象实现相应的方法。通过代码来实现一下:
forwardingTargetForSelector从上述例子中可以看出,当本类没有实现方法,并且没有动态解析方法,就会调用forwardingTargetForSelector函数,进行消息转发,因此我们可以实现forwardingTargetForSelector函数,在其内部将消息转发给可以实现此方法的对象,如果forwardingTargetForSelector函数返回nil,或者没有实现的话,就会调用methodSignatureForSelector方法,用来返回一个方法签名,这个是消息机制中最后的机会了,如果methodSignatureForSelector方法返回正确的方法签名就会调用forwardInvocation方法,forwardInvocation方法内提供一个NSInvocation类型的参数,NSInvocation封装了一个方法的调用,包括方法的调用者,方法名,以及方法的参数,因此我们可以在forwardInvocation函数内修改方法调用对象就可以了,最后如果methodSignatureForSelector返回的为nil,就会来到doseNotRecognizeSelector:方法内部,程序crash提示无法识别选择器unrecognized selector sent to instance;
验证一下:将上面的代码更改一下,
消息转发最后机制流程图,接上面的消息转发机制:
消息转发阶段详解NSInvocation
methodSignatureForSelector 方法中返回的方法签名,在forwardInvocation中被包装成NSInvocation对象,NSInvocation提供了获取和修改方法名,参数,返回值等方法,简单来说,在forwardInvoation函数中我们可以做绝对的修改;
举例说明一下,现在我们为上面的driving方法添加返回值和参数,并在forwardInvocation方法中修改方法的返回值及参数。
nsinvocation从上述打印结果可以看出forwardInvocation方法中可以对方法的参数及返回值进行修改。
类方法的消息转发
类方法消息转发同对象方法一样,同样需要经过消息发送,动态方法解析之后才会进行消息转发机制。我们知道类方法是存储在元类对象中的,元类对象本来也是一种特殊的类对象。需要注意的是,类方法的消息接受者变为类对象。
当类对象进行消息转发时,对调用相应的+号的forwardingTargetForSelector、methodSignatureForSelector、forwardInvocation方法,需要注意的是+号方法仅仅没有提示,而不是系统不会对类方法进行消息转发。
总结消息处理机制
OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)。方法调用过程中也就是objc_msgSend底层实现分为三个阶段:消息发送、动态方法解析、消息转发,而这三个具体的实现和流程上面都说得很清楚了哈,有漏或者错误的地方,阅读者可以提出留言哈
网友评论