一. Method, SEL, IMP的关系与使用
1. Method的含义:
     typedef struct objc_method *Method;
     typedef struct objc_ method {
          SEL method_name;
          char *method_types;
          IMP method_imp;
     };
一个方法 Method,其包含一个方法选标 SEL – 表示该方法的名称,一个types – 表示该方法参数的类型,一个 IMP - 指向该方法的具体实现的函数指针

2. SEL的含义:表示方法签名,typedef struct objc_selector *SEL;
不同的类可以拥有相同的 selector,这个没有问题,因为不同类的实例对象performSelector相同的 selector 时,会在各自的消息选标(selector)/实现地址(address) 方法链表中根据 selector 去查找具体的方法实现IMP, 然后用这个方法实现去执行具体的实现代码。这是一个动态绑定的过程,在编译的时候,我们不知道最终会执行哪一些代码,只有在执行的时候,通过selector去查询,我们才能确定具体的执行代码。

3. IMP的含义:typedef id (*IMP)(id, SEL, ...);
IMP 是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self 指针), 调用方法的选标 SEL (方法名),以及不定个数的方法参数,并返回一个id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在C语言里面一样使用这个函数指针。
NSObject 类中的methodForSelector:方法就是这样一个获取指向方法实现IMP 的指针,methodForSelector:返回的指针和赋值的变量类型必须完全一致,包括方法的参数类型和返回值类型。


二. 消息调用
1.消息函数obj_msgSend
编译器会将消息转换为对消息函数 objc_msgSend的调用,该函数有两个主要的参数:消息接收者id 和消息对应的方法选标 SEL, 同时接收消息中的任意参数:
id objc_msgSend(id theReceiver, SEL theSelector, ...)
这个函数做了动态绑定所需要的一切工作:
1,它首先找到 SEL 对应的方法实现 IMP。因为不同的类对同一方法可能会有不同的实现,所以找到的方法实现依赖于消息接收者的类型。
2, 然后将消息接收者对象(指向消息接收者对象的指针)以及方法中指定的参数传递给方法实现 IMP。
3, 最后,将方法实现的返回值作为该函数的返回值返回。

2. IMP查找过程
1,首先去该类的方法 cache 中查找,如果找到了就返回它;
2,如果没有找到,就去该类的方法列表中查找。如果在该类的方法列表中找到了,则将 IMP 返回,并将它加入cache中缓存起来。根据最近使用原则,这个方法再次调用的可能性很大,缓存起来可以节省下次调用再次查找的开销。
3,如果在该类的方法列表中没找到对应的 IMP,在通过该类结构中的 super_class指针在其父类结构的方法列表中去查找,直到在某个父类的方法列表中找到对应的IMP,返回它,并加入cache中。
4,如果在自身以及所有父类的方法列表中都没有找到对应的 IMP,则进入下文中要讲的消息转发流程。


三. 消息动态解析与转发




1. 在对象类的 dispatch table 中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;

2. 如果没有找到,NSObject的方法 +resolveInstanceMethod: 或者 +resolveClassMethod: 会被调用;使用这两个方法,提供了一个可以动态提供方法实现的机会。在+resolveInstanceMethod: 或者 +resolveClassMethod: 里面可以通过class_addMethod来动态设置IMP进行消息的动态解析。IMP可以通过imp_implementationWithBlock方法或者直接定义对应IMP C方法来生成。

使用class_addMethod方法时,需要注意最后一个参数,例如下面调用中"v@:"是type encodeing,必须与IMP的类型一致才行,这里可以官方guide中type encoding中的内容,https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
class_addMethod([self class], sel, (IMP)fly2IMP, "v@:");

还有一点需要注意的是,这里使用respondsToSelector: 方法进行对应的消息检测会返回YES,respondToSelector: 方法会先调用+resolveInstanceMethod: 或者 +resolveClassMethod:。

3. 如果上面resolve 方法返回 NO,就会进行消息重定向, 会发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象;如果没有相应的转发对象,就返回nil;有的话,返回对应的对象即可。但是不要返回self,这样会导致死循环调用。

4. 如果没有新的目标对象返回,会进行消息转发, 会发送 -methodSignatureForSelector:和 -forwardInvocation: 消息,这里必须实现methodSignatureForSelector方法,否则会抛出异常。在-forwardInvocation:方法中,可以使用得到对应的NSInvocation,然后设置invocation的SEL和target,最后使用invocation的 -invoke 消息来手动转发消息,并不再调用[super forwardInvocation:anInvocation]。

需要注意的是,尽管转发很像继承,但是NSObject类不会将两者混淆。像respondsToSelector: 和 isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。这个时候,可以通过重写respondsToSelector: 方法来hack一下。所以通过-forwardingTargetForSelector: 进行重定向或者通过 -forwardInvocation: 进行转发之后的实现,respondsToSelector: 进行消息检测的时候会返回NO。

5. 如果上面方法也未处理,NSObject会发送 -doesNotRecognizeSelector: 抛出异常。

四. Method Swizzling
method_exchangeImplementations
method_setImplementation
class_replaceMethod
class_addMethod
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐