OC方法调用可以分为三大阶段:消息发送、消息动态解析、消息转发。
-
消息发送。OC 调用发送会被转变成 objc_msgSend() 函数的调用,这个调用过程是:
- 在消息接收者的类对象中的方法缓存中寻找是否有对应方法,为了提高搜索效率缓存使用的是散列表的数据结构。
- 如果在缓存中没有找到方法就会进入消息接收者的类对象的方法列表中寻找,如果能找到就会把方法缓存起来并且进行调用。
- 如果消息接收者的类对象的方法列表中也没有找到,就会沿着继承链(superClass)向上寻找,直到 NSObject,如果在这个过程中找到了方法就会缓存起来并调用。
- 通过上面的步骤如果仍然没有找到方法就会进入消息动态解析过程。
-
消息动态解析。通过重写
(BOOL)resolveInstanceMethod:(SEL)sel在里面给消息接收着的类对象在运行时添加需要的方法。当动态解析过程执行完成后会重新回到消息发送流程执行。
// 使用 resolveInstanceMethod 动态添加方法的实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod([self class], sel, (IMP)dynamicMethodImplementation, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 动态添加方法的实现
void dynamicMethodImplementation(id self, SEL _cmd) {
NSLog(@"Dynamic method has been called!");
}
- 消息转发。如果在动态解析阶段仍然不能处理这个方法,就会进入消息转发流程。转发过程中有两次机会处理消息。
1. 指定一个可以处理消息的对象。`(id)forwardingTargetForSelector:(SEL)aSelector`
1. 直接处理消息。给 `(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector` 方法返回一个**不为空**的方法签名,就可以在 `(void)forwardInvocation:(NSInvocation *)anInvocation` 方法中任意处理逻辑了
动态方法解析 - resolveInstanceMethod
对象在接收到未知的消息时,首先会调用所属类的类方法
+resolveInstanceMethod:(实例方法)或者
+resolveClassMethod:(类方法)。
在这个方法中,我们有机会为该未知消息新增一个“处理方法”,通过运行时class_addMethod函数动态添加到类里面就可以了。这种方案更多的是为了实现@dynamic属性。
举例:如果我们执行ClassA并不存在的foo方法,如果我们什么都不处理在运行时程序会崩溃
ClassA *a = [ClassA new];
[a performSelector:@selector(foo)];
如果想要用 resolveInstanceMethod来补救 ,该怎么做呢?
#include <objc/runtime.h>
void foo(id self, SEL _cmd) {
NSLog(@"resolveInstanceMethod add method foo ");
}
@implementation ClassA
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(foo)) {
class_addMethod([self class], sel, (IMP)foo, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
这里的return YES 或者 return NO,是告诉系统是否实现了这个方法,如果return YES,但是并没有增加方法,还是会报错,并且不会走到forward,因为系统默认你已经在这一步做了resolveInstanceMethod这个事情。
重定向 - forwardingTargetForSelector
@interface ClassB : NSObject
- (void)foo;
@end
@implementation ClassB
- (void)foo{
NSLog(@"ClassB foo run");
}
@end
A中需要实现forwardingTargetForSelector方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(foo)){
ClassB *b = [ClassB new];
return b;
}
return [super forwardingTargetForSelector:aSelector];
}
转发 - forwardInvocation
如果你的类没有实现forwardingTargetForSelector方法,系统会调用methodSignatureForSelector方法,如果这个方法返回一个函数的签名,则执行forwardInvocation方法,否则执行doesNotRecognizeSelector。
如果希望在这一步补救,如何做呢?
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [ClassB instanceMethodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL sel = invocation.selector;
ClassB *b = [ClassB new];
if([b respondsToSelector:sel]) {
[invocation invokeWithTarget:b];
}
else {
[self doesNotRecognizeSelector:sel];
}
}
消息缓存机制
- Runtime为每个类(不是每个类实例)缓存了一个方法列表,该方法列表采用hash表实现,hash表的优点是查找速度快,时间为O(1)。
- 父类方法的缓存只存在父类么,还是子类也会缓存父类的方法? 子类会缓存父类的方法。
- 类的方法缓存大小有没有限制? 在objc-cache.mm有一个变量_class_slow_grow定义如下
/* When _class_slow_grow is non-zero, any given cache is actually grown
* only on the odd-numbered times it becomes full; on the even-numbered
* times, it is simply emptied and re-used. When this flag is zero,
* caches are grown every time. */
static const int _class_slow_grow = 1;
注释中说明,当_class_slow_grow是非0值的时候,只有当方法缓存第奇数次满(使用的槽位超过3/4)的时候,方法缓存的大小才会增长(会清空缓存,否则hash值就不对了);当第偶数次满的时候,方法缓存会被清空并重新利用。 如果_class_slow_grow值为0,那么每一次方法缓存满的时候,其大小都会增长。 所以单就问题而言,答案是没有限制,虽然这个值被设置为1,方法缓存的大小增速会慢一点,但是确实是没有上限的。
-
为什么类的方法列表不直接做成散列表呢,做成list,还要单独缓存?
- 散列表是没有顺序的,Objective-C的方法列表是一个list,是有顺序的;Objective-C在查找方法的时候会顺着list依次寻找,并且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的顺序就没法保证。
- list的方法还保存了除了selector和imp之外其他很多属性
- 散列表是有空槽的,会浪费空间