performSelector和forwardInvocation之消息的派发和转发

本文详细解析Objective-C中的performSelector和forwardInvocation的用法,包括它们的区别、同步与异步执行、参数传递及动态方法调用。同时讨论了尾部调用优化、消息转发机制及其具体流程。

第一部分:performSelector

一、performSelector调用和直接调用区别

下面两段代码都在主线程中运行,我们在看别人代码时会发现有时会直接调用,有时会利用performSelector调用,今天看到有人在问这个问题,我便做一下总结,
[delegate imageDownloader:self didFinishWithImage:image];
[delegate performSelector:@selector(imageDownloader:didFinishWithImage:)withObject:self withObject:image];
1、performSelector是运行时系统负责去找方法的,在编译时候不做任何校验;如果直接调用编译是会自动校验。如果imageDownloader:didFinishWithImage:image:不存在,那么直接调用 在编译时候就能够发现(借助Xcode可以写完就发现),但是使用performSelector的话一定是在运行时候才能发现(此时程序崩溃);Cocoa支持在运行时向某个类添加方法,即方法编译时不存在,但是运行时候存在,这时候必然需要使用performSelector去调用。所以有时候如果使用了performSelector,为了程序的健壮性,会使用检查方法- (BOOL)respondsToSelector:(SEL)aSelector;
2、直接调用方法时候,一定要在头文件中声明该方法的使用,也要将头文件import进来。而使用performSelector时候,可以不用import头文件包含方法的对象,直接用performSelector调用即可。
二、常用的performSelector简单分析
1.
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
这三个方法,均为同步执行,与线程无关,主线程和子线程中均可调用成功。等同于直接调用该方法。在需要动态的去调用方法的时候去使用。
例如:[self performSelector:@selector(test2)];与[self test2];执行效果上完全相同。
2.
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
这两个方法为异步执行,即使delay传参为0,仍为异步执行。只能在主线程中执行,在子线程中不会调到aSelector方法。可用于当点击UI中一个按钮会触发一个消耗系统性能的事件,在事件执行期间按钮会一直处于高亮状态,此时可以调用该方法去异步的处理该事件,就能避免上面的问题。
在方法未到执行时间之前,取消方法为:
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
注意:调用该方法之前或在该方法所在的viewController生命周期结束的时候去调用取消函数,以确保不会引起内存泄露。

拓展:

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;

这个方法是单线程的,也就是说只有当前调用次方法的函数执行完毕后,selector方法才会被调用。

比如:

- (void)changeText:(NSString *)string
{
    label.text = string;
    NSLog(@"changeText:(NSString *)string");
}

- (void)changePopoverSize
{   
    [self performSelector:@selector(changeText:) withObject:@"Happy aha" afterDelay:1];

    NSLog(@"changePopoverSize#####end");
    sleep(5);
    NSLog(@"changePopoverSize-----end");
}

执行结果(注意时间):

2015-08-17 17:14:06.697 awrbv[1973:f803] changePopoverSize#####end

2015-08-17 17:14:11.698 awrbv[1973:f803] changePopoverSize-----end

2015-08-17 17:14:11.701 awrbv[1973:f803] changeText:(NSString *)string

3.
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
这两个方法,在主线程和子线程中均可执行,均会调用主线程的aSelector方法;
如果设置wait为YES:等待当前线程执行完以后,主线程才会执行aSelector方法;
设置为NO:不等待当前线程执行完,就在主线程上执行aSelector方法。
如果,当前线程就是主线程,那么aSelector方法会马上执行。
注意:apple不允许程序员在主线程以外的线程中对ui进行操作,此时我们必须调用performSelectorOnMainThread函数在主线程中完成UI的更新。

拓展:

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;

这个方法是单线程的,也就是说只有当前调用次方法的函数执行完毕后,selector方法才会被调用。

比如:

- (void)changeText:(NSString *)string
{
    label.text = string;
    NSLog(@"changeText:(NSString *)string");
}

- (void)changePopoverSize
{   
    [self performSelector:@selector(changeText:) withObject:@"Happy aha" afterDelay:1];

    NSLog(@"changePopoverSize#####end");
    sleep(5);
    NSLog(@"changePopoverSize-----end");
}

执行结果(注意时间):

2015-08-17 17:14:06.697 awrbv[1973:f803] changePopoverSize#####end

2015-08-17 17:14:11.698 awrbv[1973:f803] changePopoverSize-----end

2015-08-17 17:14:11.701 awrbv[1973:f803] changeText:(NSString *)string

如果要想多线程的话,可以是使用

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
或者
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;

代码如下:

- (void)changeText:(NSString *)string
{
    label.text = string;
    NSLog(@"changeText:(NSString *)string");
}

- (void)changePopoverSize
{
    [self performSelectorOnMainThread:@selector(changeText:) withObject:@"Happy aha111" waitUntilDone:YES];
    NSLog(@"changePopoverSize#####end");
    sleep(5);
    NSLog(@"changePopoverSize-----end");
}
执行结果如下:

2015-08-17 17:19:29.618 awrbv[2024:f803] changeText:(NSString *)string

2015-08-17 17:19:29.619 awrbv[2024:f803] changePopoverSize#####end

2015-08-17 17:19:34.620 awrbv[2024:f803] changePopoverSize-----end

可以看出,如果waitUntilDone:YES那么等changeText执行完毕后再往下执行

如果waitUntilDone:NO的话,结果如下:

2015-08-17 17:21:12.135 awrbv[2049:f803] changePopoverSize#####end

2015-08-17 17:21:17.137 awrbv[2049:f803] changePopoverSize-----end

2015-08-17 17:21:17.139 awrbv[2049:f803] changeText:(NSString *)string

4.
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
调用指定线程中的某个方法。分析效果同3。
5.
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
开启子线程在后台运行
三、示例
首先建立一个简单的函数
- (void) fooNoInputs {
    NSLog(@"Does nothing");
}
然后调用它[self performSelector:@selector(fooNoInputs)];
第二个试验看看如何在消息中传递参数
我们建立一个有input参数的函数
- (void) fooOneIput:(NSString*) first {
   NSLog(@"Logs %@", first);
}
然后调用它[self performSelector:@selector(fooOneInput:) withObject:@"first"];
第三个试验更多的参数
- (void) fooFirstInput:(NSString*) first secondInput:(NSString*) second {
   NSLog(@"Logs %@ then %@", first, second);
}
然后调用它
[self performSelector:@selector(fooFirstInput:secondInput:) withObject:@"first" withObject:@"second"];

第四个试验如何建立动态的函数,然后调用他们?我们需要建立一个selector

SEL myTestSelector = @selector(myTest:);
并且我们调用的函数在另外一个Class内
- (void)abcWithAAA: (NSNumber *)number {
int primaryKey = [number intValue];
NSLog("%i", primaryKey);
}
MethodForSelectors * mfs = [[MethodForSelectors alloc]init];
NSArray *Arrays = [NSArray arrayWithObjects:@"AAA", @"BBB", nil];
for ( NSString *array in Arrays ){
SEL customSelector = NSSelectorFromString([NSStringstringWithFormat:@"abcWith%@:", array]);
mfs = [[MethodForSelectors alloc] performSelector:customSelector withObject:0];
}
1.如果使用了ARC会产生“performSelector may cause a leak because its selector is unknown”警告
2.这种方式当传入一个不符合约定的消息时会继续运行并不报错。例如应该传入2个参数,但只传入1个参数。或传入了3个参数,第三个参数不会被初始化。
还有一种调用其他Class Function的方法是,但是不能有参数,我们这里假设没有参数,那么就可以这样[mfs customSelector]; 
完整的代码:
@implementation ClassForSelectors
- (void) fooNoInputs {
NSLog(@"Does nothing");
}
- (void) fooOneIput:(NSString*) first {
NSLog(@"Logs %@", first);
}
- (void) fooFirstInput:(NSString*) first secondInput:(NSString*) second {
NSLog(@"Logs %@ then %@", first, second);
}
- (NSArray *)abcWithAAA: (NSNumber *)number {
int primaryKey = [number intValue];
NSLog("%i", primaryKey);
}
- (void) performMethodsViaSelectors {
[self performSelector:@selector(fooNoInputs)];
[self performSelector:@selector(fooOneInput:) withObject:@"first"];
[self performSelector:@selector(fooFirstInput:secondInput:) withObject:@"first" withObject:@"second"];
}
- (void) performDynamicMethodsViaSelectors {
MethodForSelectors * mfs = [MethodForSelectors alloc];
NSArray *Arrays = [NSArray arrayWithObjects:@"AAA", @"BBB", nil];
for ( NSString *array in Arrays ){
SEL customSelector = NSSelectorFromString([NSStringstringWithFormat:@"abcWith%@:", array]);
mfs = [[MethodForSelectors alloc] performSelector:customSelector withObject:0];
}
}
@end
@implementation MethodForSelectors
- (void)abcWithAAA: (NSNumber *)number {
NSLog("%i", number);
}
@end

第二部分:forwardInvocation

一,消息发送
在Objective-C中,使用对象进行方法调用是一个消息发送的过程(Objective-C采用“动态绑定机制”,所以所要调用的方法直到运行期才能确定)。例如:

id returnValue = [someObject messageName:parameter];
其中,someObject是消息的“接收者”,messageName为“选择子”,“选择子”与参数合起来叫做消息。
当编译器看到消息后,会将其转为一条标准的C语言函数调用objc_msgSend,其原型如下:
void objc_msgSend (id self, SEL cmd, ...)
这是一个参数可变的函数,第一个参数是接受者,第二个参数是选择子,后续参数就是消息中的那些参数。objc_msgSend会搜索接收者类中的“方法列表”(选择子的名称是查表时所用的“键”)。如果能找到与选择子名称相符的方法,就跳转至其实现代码。如果找不到,就沿着起继承体系向上查找。如果最终还是没有找到相符的方法,那就执行“消息转发”。

二,尾部调用优化

一般情况下,在方法内部调用另外一个方法,就会把方法的内部变量、返回的地址等信息压入栈中。以便另外一个方法调用结束后,能够正确返回并处理。所以,如果A方法调用B方法,B方法调用C方法,C方法调用D方法⋯⋯,就需要往栈中压入许多信息。这样就可能发生栈溢出的情况。
为了在一定程度上避免这种情况发生,当方法是在最后一步执行另外一个方法的调用时,编译器会执行尾部调用优化。即不保留外层方法的信息(因为返回地址、内部变量等信息都不会再用到了),直接用内层方法的调用记录,取代外层方法的调用记录。
三,消息转发
当对象收到无法解读的消息时,就会启动“消息转发”机制。开发者可以在此过程中,通过函数对象处理未知的消息。

消息转发处理流程

上图显示了消息转发的具体流程,接收者在每一步中均有机会处理消息。步骤越往后处理消息的代价越大。

首先,会调用+ (BOOL)resolveInstanceMethod:(SEL)sel。若方法返回YES,则表示可以处理该消息。在这个过程,可以动态地给消息增加方法。代码如下图所示:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSString *selStr = NSStringFromSelector(sel);
    if ([selStr isEqualToString:@"name"]) {
        class_addMethod(self, sel, (IMP)nameGetter, "@@:");
        return YES;
    }
    if ([selStr isEqualToString:@"setName:"]) {
        class_addMethod(self, sel, (IMP)nameSetter, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
 }

void nameSetter(id self, SEL cmd, id value){
    NSString *fullName = value;
    NSArray *nameArray = [fullName componentsSeparatedByString:@" "];
    PersonModel *model = (PersonModel *)self;
    model.firstName = nameArray[0];
    model.lastName  = nameArray[1];
}
id nameGetter(id self, SEL cmd){ 
    PersonModel *model = (PersonModel *)self;
    NSMutableString *name = [[NSMutableString alloc] init];
    if (nil != model.firstName) {
        [name appendString:model.firstName];
        [name appendString:@" "];
    }
    if (nil != model.lastName) {
        [name appendString:model.lastName];
    }
    return name;
}
若方法返回NO,则进行消息转发的第二步,查找是否有其它的接收者。对应的处理函数是:- (id)forwardingTargetForSelector:(SEL)aSelector。可以通过该函数返回一个可以处理该消息的对象。
- (id)forwardingTargetForSelector:(SEL)aSelector{  
    NSString *selStr = NSStringFromSelector(aSelector);
    if ([selStr isEqualToString:@"companyName"]) {
        return self.companyModel;
    }else{
        return [super forwardingTargetForSelector:aSelector];
    }
}
若第二步返回nil,则进入消息转发的第三步。调用- (void)forwardInvocation:(NSInvocation *)anInvocation。这个方法实现得很简单。只需要改变调用目标,使消息在新目标上得以调用即可。不过,如果采用这种方式,实现的效果与第二步的消息转发是一致的。所以比较有用的实现方式是:先以某种方式改变消息内容,比如追加另外一个参数,或者改换选择子,等等。例如:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{                          
    NSMethodSignature *sig = nil;
    NSString *selStr = NSStringFromSelector(aSelector);
    if ([selStr isEqualToString:@"deptName"]) {
        //此处返回的sig是方法forwardInvocation的参数anInvocation中的methodSignature
        sig = [self.companyModel methodSignatureForSelector:@selector(deptName:)];
    }else{
        sig = [super methodSignatureForSelector:aSelector];
    }
    return sig;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSString *selStr = NSStringFromSelector(anInvocation.selector);
    if ([selStr isEqualToString:@"deptName"]) {
        [anInvocation setTarget:self.companyModel];
        [anInvocation setSelector:@selector(deptName:)];
        BOOL hasCompanyName = YES;
        //第一个和第一个参数是target和sel
        [anInvocation setArgument:&hasCompanyName atIndex:2];
        [anInvocation retainArguments];
        [anInvocation invoke];
    }else{
        [super forwardInvocation:anInvocation];
    }
}
如果直到NSObject,继承体系中的其它类都无法处理这个消息转发,就会由NSObject调用该方法,并在该方法中调用doesNotRecognizeSelector,以抛出异常。

注意:

1.-forwardInvocation:在一个对象无法识别消息之前调用,再需要重载-methodSignatureForSelector:,因为在调用-forwardInvocation:之前是要把消息打包成NSIvocation对象的,所以需要-methodSignatureForSelector:重载,如果不能在此方法中不能不为空的NSMethodSignature对象,程序依然会崩溃

Demo:http://download.youkuaiyun.com/download/lxl_815520/9502065

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值