runtime之消息转发
前言
在上一篇文章中我们初尝了runtime的黑魔法,可以在程序编译阶段就获取到成员变量的名字,特性以及动态的给对象增加属性等等,在接下来中我们进一步了解OC的消息发送机制。如果之前没接触过runtime的同学建议先看看:上一篇《runtime之玩转成员变量》
OC的消息发送机制是早有耳闻,鉴于自己一直觉得是很底层的东西需要花大量的时候去学习研究它所以一直都是蠢蠢欲动。同样不做过多铺垫,直接切入吧。当我们使用OC对象调用一个方法的时候,比如这样:[lisi sayHello]; 程序运行的时候会转化为runtime的代码objc_msgnSend(lisi,@selector(sayHello)),通过消息发送函数的字面意义我们可以知道是给lisi这个对象发送了sayHello这个消息。方法的调用其实就是给类发送一个消息,调用类方法也一样,类实际上也是一个对象,是元类的实例。runtime中类似这种消息发送的函数还有很多包括:
1 objc_property_t *class_copyProperty(Class cls,unsigned int *outcout) //获取所有的属性列表 2 Method *class_copyMethodList(Class cls,unsigned int *outCount) //获取所有方法的数组 3 Bool class_addMethod (Class cls,SEL name,IMP imp,const char *type) //添加方法
消息转发流程:
当我们创建一个实例变量并调用实例方法时候,即[receiver message],转换为运行时代码id objc_msgSend(id self,SEL op....),首先根据实例的isa指针到指定的类中的方法列表中进行查找相应的op,如果找到相应的op则调用,如果找不到的话则到相应的父类中查找,这样一直循环上去,一直到根类NSObject中如果还没有找到的话会按照优先级从高到低调用下面三个函数:
1 + resolveInstanceMethod:(SEL)sel // 对应实例方法没有获取到 + resolveClassMethod:(SEL)sel // 对应类方法没有获取到 2 - (id)forwardingTargetForSelector:(SEL)aSelector 3 - (void)forwardInvocation:(NSInvocation *)anInvocation
即某一个实例方法的本类及其父类都没有实现的时候会首先调用+ resolveInstanceMethod:(SEL)sel,如果该方法没有实现则调用- (id)forwardingTargetForSelector:(SEL)aSelector,如果第二个方法还没有实现的时候就调用第三个- (void)forwardInvocation:(NSInvocation *)anInvocation。若是这三个方法都没有实现的话则程序抛出异常。
注意:第三个方法(void)forwardInvocation:(NSInvocation *)anInvocation需要跟methodSignatureForSelector结合使用才能实现消息转发,methodSignatureForSelector的作用是为一个类已经实现的方法创建一个有效的签名。
消息转发的原理:
每个类都有一个包含SEL和对应的IMP的Method列表,也就是说一个Method包含着一个SEL和一个对应的IMP,而消息转发就是将原本的SEL和IMP的这种对应关系给分开,跟其他的Method重新组合。
下面通过一个Person类体验实现runtime的消息转发:
1,动态添加函数实现消息转发:
Person.h添加下面在这个方法并且在Person.m文件中不实现它:
- (void)goForWork;
在Person.m中实现消息转发:
1 +(BOOL)resolveInstanceMethod:(SEL)sel 2 { 3 NSString *selString = NSStringFromSelector(sel); 4 if ([selString isEqualToString:@"goForWork"]) { 5 /** 6 * 为类中没有实现的方法添加一个函数实现 7 * 8 * @param self 类名 9 * @param goForWork 没有实现的方法 10 * @IMP workFunc 添加的函数实现 11 * @ "v@:" TypeEncoding函数类型的类型编码 12 * @return 13 */ 14 class_addMethod(self, @selector(goForWork), (IMP)workFunc, "v@:"); 15 } 16 return [super resolveInstanceMethod:sel]; 17 } 18 19 void workFunc(id self,SEL sel) 20 { 21 NSLog(@"Person go for work"); 22 }
在外界调用Person实例的goForWalk 方法,可以看见打印台打印:
2,切换消息接受者实现消息转发:
将消息给其他对象也是消息转发的一种形式,一般是将消息转发给该对象中其他对象,这样子看起来也就感觉是该对象执行了该方法。我们在Person类中定义一个Dog类型的变量myDog。同样的,在Person定义一个walk方法并且不实现,Dog类同样定义这样的一个方法并在implement中实现。
Person.m中重写forwardingTargetForSelector:转换消息接收者:
-(id)forwardingTargetForSelector:(SEL)aSelector { NSString *selString = NSStringFromSelector(aSelector); if ([selString isEqualToString:@"walk"]) { self.myDog = [Dog new]; return self.myDog; } return [super forwardingTargetForSelector:aSelector]; }
外界调用后可以在打印台中:
转换消息对象方式二:
methodSignatureForSelector:和forwardInvocation:结合实现消息转发。
1 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 2 { 3 NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector]; 4 if (!methodSignature) { 5 methodSignature = [Dog instanceMethodSignatureForSelector:aSelector]; 6 } 7 return methodSignature; 8 } 9 10 - (void)forwardInvocation:(NSInvocation *)anInvocation 11 { 12 if ([Dog instancesRespondToSelector:anInvocation.selector]) { 13 //消息调用 14 [anInvocation invokeWithTarget:self.myDog]; 15 16 } 17 }
通过这种方式同样能够在打印台打印出相同结果。
runtime之方法交换实现:
当我学习到runtime这个移魂大法的时候不禁惊叹runtime大法好,简直是黑魔法,同时心里产生一点邪恶的心里😈
假设有两个方法A和B,正常情况下我发送A消息调用的是A的实现,发送B消息的时候调用的是B的实现。所谓的方法交换实现,就是我给对象发送了A消息调用的却是B的实现,给对象发送B消息调用的却是A的实现。难道这就是传说中的移花接木😂
runtime下提供了一系列的函数来助我们修炼移魂大法:
Method class_getInstanceMethod(Class cls, SEL name) //获取某一个实例方法 IMP class_getMethodImplementation(Class cls, SEL name) //获取某一个方法的实现 const char *method_getTypeEncoding(Method m) //获取一个方法类型的类型编码 IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) //用一个方法的实现来代替另一个方法的实现 void method_exchangeImplementations(Method m1, Method m2) //交换两个方法的实现
思路大概就跟我们动态添加方法实现的思路一样,当然你也可以霸王硬上弓,一上来二话不说直接就两个方法交换实现,但是如果此时有两个方法其中有一个没有实现呢???会发现什么??
Person.h中定义以下两个方法并在 Person.m文件中做以下实现
-(void)drink { NSLog(@"我在喝水"); } -(void)eat { NSLog(@"我在吃东西"); }
同时在初始化函数中我们对其这两个方式进行“移花接木”:
1 + (Person *)personWithName:(NSString *)name age:(NSNumber *)age gender:(NSString *)gender clan:(NSString *)clan 2 { 3 Person *p = [Person new]; 4 unsigned int outCount; 5 Ivar *IvarArray = class_copyIvarList([Person class], &outCount); 6 object_setIvar(p, IvarArray[0], clan); 7 object_setIvar(p, IvarArray[1], name); 8 object_setIvar(p, IvarArray[2], gender); 9 object_setIvar(p, IvarArray[3], age); 10 //以上为runtime下的成员变量操作,具体可看 上一篇 11 static dispatch_once_t onceToken; 12 dispatch_once(&onceToken, ^{ 13 Class selfClass = [self class]; 14 SEL aSel = @selector(drink); 15 Method aMethod = class_getInstanceMethod(selfClass, aSel); 16 SEL bSel = @selector(eat); 17 Method bMethod = class_getInstanceMethod(selfClass, bSel); 18 //依次先获取两个方法的SEL指针和runtime的Method 19 BOOL value = class_addMethod(selfClass, aSel, method_getImplementation(bMethod), method_getTypeEncoding(bMethod)); 20 //将B的实现添加到A身上 21 if (value) { 22 class_replaceMethod(selfClass, bSel, method_getImplementation(aMethod), method_getTypeEncoding(aMethod)); 23 }else{ 24 method_exchangeImplementations(bMethod, aMethod); 25 } 26 }); 27 return p; 28 }
在外界给person对象发送drink消息的时候打印台:
同样的当发送eat消息时候打印出来的是"我在吃东西"。一般情况下两个方法交换实现在实际需求中还是比较少见的,特别是我们自定义的方法中的时候容易出现不明甚至问题复杂的情况。想想要是有哪个心计boy在离职的话在程序埋了这样一个不会爆炸的炸弹的话。。。。。好腹黑
不过要是这种移花接木应用的好的话倒是在某些特定的场合能够省了不少事,比如你想更新版本发现素材比原来的素材名字上多了同样的前缀而已,这时候可以在为imageNamed添加一个方法实现为所有素材的名字加上一个同样的前缀。这样当程序调用imageWithNamed的时候就会调用你自定义的函数实现,轻松的更新素材的名字,而不用到程序中一个个查找手动添加前缀。又或者你刚接收公司项目需要整天查看项目的框架,一般情况下你或许会在所有的viewDidLoad添加上几句log来查看,这时候就能排上用场了。
初尝runtime,若是有什么表述不当的地方还请指出。后续将继续更新runtime的学习。