消息发送机制
在OC中的方式调用是通过Runtime实现,实际上是通过对对象进行发送消息,也就是objc_msgSend()进行消息发送。
[object test]; // 源代码
objc_msgSend(object, @selector(test));//转换之后的调用
Selector
如果单独打印@selector(testMethod),会发现每一个类的打印的是同一个地址。
这是因为SEL并不按照类分别存储,所有的SEL都会存放在Runtime的表中,相同名字的会被认为是同一个。
找方法流程
- 先判断接受消息的对象是否为空,是则返回nil。
- 获取到接受消息的对象。
- 从对象缓存的方法中获取。
- 如果没有找到,则从method list中查找。
- 如果还没找到则从父类中找。重复3-5的步骤,直到找到NSObject。
- 如果还没找到,可以通过动态方法解析。
resolveInstanceMethod和resolveClassMethod。 (主要用于新增方法便于响应) - 如果动态方法解析也没有相应,则进入动态信息转发阶段,如果不做处理则会crash。(可以修改方法以及接受方法的对象)
缓存
-
缓存的意义以及使用 如果没有缓存,会通过从下到上的查找方式,调用链会非常的长,调用一个方法成千上万次,带来的时间消耗会非常明显,除非一个方法只调用一次,不然通过缓存可以节约很多时间。
-
存储方式 方式列表method_list是通过数组存储,缓存是通过散列表的方式,这样可以使查找效率非常快,为O(1)。所有缓存都存在metaclass上,所以每个类都只有一份方法缓存,而不是每一个类的object都保存一份。
-
调用父类的方法会缓存到子类中么? 从父类取到的方法,也会存在类本身的方法缓存里。而当用一个父类对象去调用那个方法的时候,也会在父类的metaclass里缓存一份。
-
缓存的大小限制
/* 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];
}
Category
主要功能
- 为已经存在的类添加方法
- 可以把类的实现分开在几个不同的文件里面。 这样做有几个显而易见的好处 a)可以减少单个文件的体积 b)可以把不同的功能组织到不同的category里 c)可以由多个开发者共同完成一个类 d)可以按需加载想要的category
- 声明私有方法
注意事项
调用方式
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_setAssociatedObject与objc_getAssociatedObject完成。