消息发送机制的复习

272 阅读5分钟

消息发送机制

在OC中的方式调用是通过Runtime实现,实际上是通过对对象进行发送消息,也就是objc_msgSend()进行消息发送。

[object test]; // 源代码
objc_msgSend(object, @selector(test));//转换之后的调用

Selector

如果单独打印@selector(testMethod),会发现每一个类的打印的是同一个地址。 这是因为SEL并不按照类分别存储,所有的SEL都会存放在Runtime的表中,相同名字的会被认为是同一个。

找方法流程

  1. 先判断接受消息的对象是否为空,是则返回nil。
  2. 获取到接受消息的对象。
  3. 从对象缓存的方法中获取。
  4. 如果没有找到,则从method list中查找。
  5. 如果还没找到则从父类中找。重复3-5的步骤,直到找到NSObject。
  6. 如果还没找到,可以通过动态方法解析。resolveInstanceMethodresolveClassMethod。 (主要用于新增方法便于响应)
  7. 如果动态方法解析也没有相应,则进入动态信息转发阶段,如果不做处理则会crash。(可以修改方法以及接受方法的对象)

缓存

  1. 缓存的意义以及使用 如果没有缓存,会通过从下到上的查找方式,调用链会非常的长,调用一个方法成千上万次,带来的时间消耗会非常明显,除非一个方法只调用一次,不然通过缓存可以节约很多时间。

  2. 存储方式 方式列表method_list是通过数组存储,缓存是通过散列表的方式,这样可以使查找效率非常快,为O(1)。所有缓存都存在metaclass上,所以每个类都只有一份方法缓存,而不是每一个类的object都保存一份。

  3. 调用父类的方法会缓存到子类中么? 从父类取到的方法,也会存在类本身的方法缓存里。而当用一个父类对象去调用那个方法的时候,也会在父类的metaclass里缓存一份。

  4. 缓存的大小限制

/* 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,方法缓存的大小增速会慢一点,但是确实是没有上限的。

动态消息解析

当一个方法尚未实现,从子类遍历到NSObject都没有找到,这是会进入消息转发,在之前Runtime还会给一次机会动态添加方法。

void dynamicMethodIMP(id self, SEL _cmd) { 
	// implementation …. 
} 
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(resolveThisMethodDynamically)) { 
	class_addMethod([self class], sel, (IMP) dynamicMethodIMP, “v@:”); return YES; 
} 
return [super resolveInstanceMethod:sel]; 
} 

class_addMethod

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) { 
	if (!cls) return NO; 
   rwlock_writer_t lock(runtimeLock);
	return ! addMethod(cls, name, imp, types ?: “”, NO); 
} 

如果方法已经存在了,则不会做任何事情,直接返回方法。

如果方法不存在,则会创建一个method_list_t,添加到方法列表中。

消息转发

在消息转发之前,通过forwardingTargetForSelector将消息发给其他对象,可以重新走一遍找方法流程。

- (id)forwardingTargetForSelector:(SEL)aSelector {
	NSString *selectorName = NSStringFromSelector(aSelector); 
	if ([selectorName isEqualToString:@“selector”]) { 
		return object; 
	} 
	return [super forwardingTargetForSelector:aSelector]; 
} 

如果这里也不能处理,则只能走forwardInvocation,自定义转发逻辑。

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
         [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

详情用代码理解 ObjC 中的发送消息和消息转发 - 掘金

Category

主要功能

  1. 为已经存在的类添加方法
  2. 可以把类的实现分开在几个不同的文件里面。 这样做有几个显而易见的好处 a)可以减少单个文件的体积 b)可以把不同的功能组织到不同的category里 c)可以由多个开发者共同完成一个类 d)可以按需加载想要的category
  3. 声明私有方法

注意事项

调用方式

Category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的。 调用普通方法,从Compile Source中,查找最后一个编译的category的方法。 调用Load方法,则是从父类->子类->Category。 如果希望调用被覆盖的方法,可以通过遍历method_list方式找到之前同名的方法。

属性关联

Category中可以添加属性,但是不能添加实例变量。

+ (void)load
{
    NSLog(@"%@",@"load in Category1");
}

- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self,
                             “name”,
                             name,
                             OBJC_ASSOCIATION_COPY);
}

- (NSString*)name
{
    NSString *nameObject = objc_getAssociatedObject(self, “name”);
    return nameObject;
}

需要通过objc_setAssociatedObjectobjc_getAssociatedObject完成。