`
啸笑天
  • 浏览: 3465253 次
  • 性别: Icon_minigender_1
  • 来自: China
社区版块
存档分类
最新评论

NSObject中methodSignatureForSelector、forwardInvocation两个消息方法

 
阅读更多

 

在obj-c中我们可以向一个实例发送消息,相当于c/c++ java中的方法调用,只不过在这儿是说发送消息,实例收到消息后会进行一些处理。比如我们想调用一个方法,便向这个实例发送一个消息,实例收到消息后,如果能respondsToSelector,那么就会调用相应的方法。如果不能respond一般情况下会crash。今天要的,就是不让它crash。

 

首先说一下向一个实例发送一个消息后,系统是处理的流程:

1. 发送消息如:[self startwork] 

2. 系统会check是否能response这个消息

3. 如果能response则调用相应方法,不能则抛出异常

在第二步中,系统是如何check实例是否能response消息呢?如果实例本身就有相应的response,那么就会相应之,如果没有系统就会发出methodSignatureForSelector消息,寻问它这个消息是否有效?有效就返回对应的方法地址之类的,无效则返回nil。如果是nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了. 如果不是nil接着发送forwardInvocation消息。

所以我们在重写methodSignatureForSelector的时候就人工让其返回有效实例。

我们定义了这样一个类

@interface TargetProxy : NSProxy {  
    id realObject1;  
    id realObject2;  
}  
   
- (id)initWithTarget1:(id)t1 target2:(id)t2;  
   
@end 

 

实现:

@implementation TargetProxy  
   
- (id)initWithTarget1:(id)t1 target2:(id)t2 {  
    realObject1 = [t1 retain];  
    realObject2 = [t2 retain];  
    return self;  
}  
   
- (void)dealloc {  
    [realObject1 release];  
    [realObject2 release];  
    [super dealloc];  
}  
   
// The compiler knows the types at the call site but unfortunately doesn't  
// leave them around for us to use, so we must poke around and find the types  
// so that the invocation can be initialized from the stack frame.  
   
// Here, we ask the two real objects, realObject1 first, for their method  
// signatures, since we'll be forwarding the message to one or the other  
// of them in -forwardInvocation:.  If realObject1 returns a non-nil  
// method signature, we use that, so in effect it has priority.  
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {  
    NSMethodSignature *sig;  
    sig = [realObject1 methodSignatureForSelector:aSelector];  
    if (sig) return sig;  
    sig = [realObject2 methodSignatureForSelector:aSelector];  
    return sig;  
}  
   
// Invoke the invocation on whichever real object had a signature for it.  
- (void)forwardInvocation:(NSInvocation *)invocation {  
    id target = [realObject1 methodSignatureForSelector:[invocation selector]] ? realObject1 : realObject2;  
    [invocation invokeWithTarget:target];  
}  
   
// Override some of NSProxy's implementations to forward them...  
- (BOOL)respondsToSelector:(SEL)aSelector {  
    if ([realObject1 respondsToSelector:aSelector]) return YES;  
    if ([realObject2 respondsToSelector:aSelector]) return YES;  
    return NO;  
}  
   
@end  

 

 

现在我们还用这个类,注意向它发送的消息:

  id proxy = [[TargetProxy alloc] initWithTarget1:string target2:array];  
   
    // Note that we can't use appendFormat:, because vararg methods  
    // cannot be forwarded!  
    [proxy appendString:@"This "];  
    [proxy appendString:@"is "];  
    [proxy addObject:string];  
    [proxy appendString:@"a "];  
    [proxy appendString:@"test!"];  
   
    NSLog(@"count should be 1, it is: %d", [proxy count]);  
      
    if ([[proxy objectAtIndex:0] isEqualToString:@"This is a test!"]) {  
        NSLog(@"Appending successful.");  
    } else {  
        NSLog(@"Appending failed, got: '%@'", proxy);  
    }  

 

运行的结果是:

 

count should be 1, it is:  1

Appending successful.

 TargetProxy声明中是没有appendString与addObject消息的,在这儿却可以正常发送,不crash,原因就是发送消息的时候,如果原本类没有这个消息响应的时候,转向询问methodSignatureForSelector,接着在forwardInvocation将消息重定向。 

 

 

 

 

///////////

 

 

 

 

分享到:
评论
2 楼 啸笑天 2013-08-28  
http://www.cnblogs.com/bandy/archive/2012/03/28/2420809.html
1 楼 啸笑天 2013-08-28  
各种语言都有些传递函数的方法:C语言中可以使用函数指针,C++中有函数引用、仿函数和lambda,Objective-C里也有选择器(selector)和block。
不过由于iOS SDK中的大部分API都是selector的方式,所以本文就重点讲述selector了。

Objective-C和我接触过的其他面向对象的语言不同,它强调消息传递,而非方法调用。因此你可以对一个对象传递任何消息,而不需要在编译期声名这些消息的处理方法。
很显然,既然编译期并不能确定方法的地址,那么运行期就需要自行定位了。而Objective-C runtime就是通过“id objc_msgSend(id theReceiver, SEL theSelector, ...)”这个函数来调用方法的。其中theReceiver是调用对象,theSelector则是消息名,省略号就是C语言的不定参数了。
这里的消息名是SEL类型,它被定义为struct objc_selector *。不过文档中并没有透露objc_selector是什么东西,但提供了@selector指令来生成:

SEL selector = @selector(message);
@selector是在编译期计算的,所以并不是函数调用。更进一步的测试表明,它在Mac OS X 10.6和iOS下都是一个C风格的字符串(char*):

NSLog (@"%s", (char *)selector);
你会发现结果是“message”这个消息名。

下面就写个测试类:
复制代码

@interface Test : NSObject
@end

@implementation Test

- (NSString *)intToString:(NSInteger)number {
    return [NSString stringWithFormat:@"%d", number];
}

- (NSString *)doubleToString:(double *)number {
    return [NSString stringWithFormat:@"%f", *number];
}

- (NSString *)pointToString:(CGPoint)point {
    return [NSString stringWithFormat:@"{%f, %f}", point.x, point.y];
}

- (NSString *)intsToString:(NSInteger)number1 second:(NSInteger)number2 third:(NSInteger)number3 {
    return [NSString stringWithFormat:@"%d, %d, %d", number1, number2, number3];
}

- (NSString *)doublesToString:(double)number1 second:(double)number2 third:(double)number3 {
    return [NSString stringWithFormat:@"%f, %f, %f", number1, number2, number3];
}

- (NSString *)combineString:(NSString *)string1 withSecond:string2 withThird:string3 {
    return [NSString stringWithFormat:@"%@, %@, %@", string1, string2, string3];
}

@end
复制代码
再来测试下objc_msgSend:
复制代码

#import <objc/message.h>
//要使用objc_msgSend的话,就要引入这个头文件

Test *test = [[Test alloc] init];
CGPoint point = {123, 456};
NSLog(@"%@", objc_msgSend(test, @selector(pointToString:), point));
[test release];
复制代码
结果是“{123.000000, 456.000000}”。而且与之前猜想的一样,下面这样调用也是可以的:
NSLog(@"%@", objc_msgSend(test, (SEL)"pointToString:", point));
看到这里你应该发现了,这种实现方式只能确定消息名和参数数目,而参数类型和返回类型就给抹杀了。所以编译器只能在编译期警告你参数类型不对,而无法阻止你传递类型错误的参数。


接下来再看看NSObject协议提供的一些传递消息的方法:
- (id)performSelector:(SEL)aSelector
- (id)performSelector:(SEL)aSelector withObject:(id)anObject
- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject
也没有觉得很无语?为什么参数必须是对象?为什么最多只支持2个参数?

好在selector本身也不在乎参数类型,所以传个不是对象的玩意也行:
NSLog(@"%@", [test performSelector:@selector(intToString:) withObject:(id)123]);
可是double和struct就不能这样传递了,因为它们占的字节数和指针不一样。如果非要用performSelector的话,就只能修改参数类型为指针了:

复制代码
- (NSString *)doubleToString:(double *)number {
    return [NSString stringWithFormat:@"%f", *number];
}

double number = 123.456;
NSLog(@"%@", [test performSelector:@selector(doubleToString:) withObject:(id)(&number)]);
复制代码
参数类型算是搞定了,可是要支持多个参数,还得费番气力。理想状态下,我们应该可以实现这2个方法:
复制代码
@interface NSObject (extend)

- (id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects;
- (id)performSelector:(SEL)aSelector withParameters:(void *)firstParameter, ...;

@end
复制代码
先看看前者,NSArray要求所有的元素都必须是对象,并且不能为nil,所以适用的范围仍然有限。不过你可别小看它,因为你会发现根本没法用objc_msgSend来实现,因为你在写代码时没法预知参数个数。
这时候就轮到NSInvocation登场了:
复制代码
@implementation NSObject (extend)

- (id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects {
    NSMethodSignature *signature = [self methodSignatureForSelector:aSelector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    [invocation setTarget:self];
    [invocation setSelector:aSelector];
   
    NSUInteger i = 1;
    for (id object in objects) {
        [invocation setArgument:&object atIndex:++i];
    }
    [invocation invoke];
   
    if ([signature methodReturnLength]) {
        id data;
        [invocation getReturnValue:&data];
        return data;
    }
    return nil;
}

@end


NSLog(@"%@", [test performSelector:@selector(combineString:withSecond:withThird:) withObjects:[NSArray arrayWithObjects:@"1", @"2", @"3", nil]]);
复制代码
这里有3点要注意的:
因为方法调用有self(调用对象)和_cmd(选择器)这2个隐含参数,因此设置参数时,索引应该从2开始。
因为参数是对象,所以必须传递指针,即&object。
methodReturnLength为0时,表明返回类型是void,因此不需要获取返回值。返回值是对象的情况下,不需要我们来创建buffer。但如果是C风格的字符串、数组等类型,就需要自行malloc,并释放内存了。

再来实现第2个方法:
复制代码
- (id)performSelector:(SEL)aSelector withParameters:(void *)firstParameter, ... {
    NSMethodSignature *signature = [self methodSignatureForSelector:aSelector];
    NSUInteger length = [signature numberOfArguments];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    [invocation setTarget:self];
    [invocation setSelector:aSelector];
   
    [invocation setArgument:&firstParameter atIndex:2];
    va_list arg_ptr;
    va_start(arg_ptr, firstParameter);
    for (NSUInteger i = 3; i < length; ++i) {
        void *parameter = va_arg(arg_ptr, void *);
        [invocation setArgument:&parameter atIndex:i];
    }
    va_end(arg_ptr);
   
    [invocation invoke];
   
    if ([signature methodReturnLength]) {
        id data;
        [invocation getReturnValue:&data];
        return data;
    }
    return nil;
}

NSLog(@"%@", [test performSelector:@selector(combineString:withSecond:withThird:) withParameters:@"1", @"2", @"3"]);

NSInteger number1 = 1, number2 = 2, number3 = 3;
NSLog(@"%@", [test performSelector:@selector(intsToString:second:third:) withParameters:number1, number2, number3]);
复制代码
和前面的实现差不多,不过由于参数长度是未知的,所以用到了[signature numberOfArguments]。当然也可以把SEL转成字符串(可用NSStringFromSelector()),然后查找:的数量。
处理可变参数时用到了va_start、va_arg和va_end,熟悉C语言的一看就明白了。
不过由于不知道参数的类型,所以只能设为void *。而这个程序也报出了警告,说void *和NSInteger类型不兼容。而如果把参数换成double,那就直接报错了。遗憾的是我也不知道怎么判别一个void *指针究竟是指向C数据类型,还是指向一个Objective-C对象,所以最好是封装成Objective-C对象。如果只需要兼容C类型的话,倒是可以将setArgument的参数的&去掉,然后直接传指针进去:

复制代码
NSInteger number1 = 1, number2 = 2, number3 = 3;
NSLog(@"%@", [test performSelector:@selector(intsToString:second:third:) withParameters:&number1, &number2, &number3]);

double number4 = 1.0, number5 = 2.0, number6 = 3.0;   
NSLog(@"%@", [test performSelector:@selector(doublesToString:second:third:) withParameters:&number4, &number5, &number6]);
[test release];
复制代码
至于NSObject类添加的performSelector:withObject:afterDelay:等方法,也可以用这种方式来支持多个参数。

接下来再说说刚才略过的_cmd,它还可以用来实现递归调用。下面就以斐波那契数列为例:

复制代码
- (NSInteger)fibonacci:(NSInteger)n {
    if (n > 2) {
        return [self fibonacci:n - 1] + [self fibonacci:n - 2];
    }
    return n > 0 ? 1 : 0;
}
复制代码
改成用_cmd实现就变成了这样:

return (NSInteger)[self performSelector:_cmd withObject:(id)(n - 1)] + (NSInteger)[self performSelector:_cmd withObject:(id)(n - 2)];
或者直接用objc_msgSend:

return (NSInteger)objc_msgSend(self, _cmd, n - 1) + (NSInteger)objc_msgSend(self, _cmd, n - 2);
但是每次都通过objc_msgSend来调用显得很费劲,有没有办法直接进行方法调用呢?答案是有的,这就需要用到IMP了。IMP的定义为“id (*IMP) (id, SEL, …)”,也就是一个指向方法的函数指针。
NSObject提供methodForSelector:方法来获取IMP,因此只需稍作修改就行了:

复制代码
- (NSInteger)fibonacci:(NSInteger)n {
    static IMP func;
    if (!func) {
        func = [self methodForSelector:_cmd];
    }
   
    if (n > 2) {
        return (NSInteger)func(self, _cmd, n - 1) + (NSInteger)func(self, _cmd, n - 2);
    }
    return n > 0 ? 1 : 0;
}
复制代码
现在运行时间比刚才减少了1/4,还算不错。

顺便再展现一下Objective-C强大的动态性,给Test类添加一个sum:and:方法:

复制代码
NSInteger sum(id self, SEL _cmd, NSInteger number1, NSInteger number2) {
    return number1 + number2;
}

class_addMethod([Test class], @selector(sum:and:), (IMP)sum, "i@:ii");
NSLog(@"%d", [test sum:1 and:2]);
复制代码
class_addMethod的最后那个参数是函数的返回值和参数类型,详细内容可以参考Type Encodings文档。

相关推荐

    NSObject源码.zip

    这两个特性是基于Objective-C的动态性,源码中会有对应的实现细节。 3. **初始化和释放**:`NSObject`定义了`init`和`dealloc`方法,是所有对象初始化和销毁的基本入口。通过阅读源码,我们可以看到如何正确地初始...

    NSObject扩展

    定义了一个`KeyboardProtocol`协议,其中包含两个必需的方法:`keyboardWillShow:` 和 `keyboardWillHide:`。这些方法在键盘将要显示和将要隐藏时调用,允许类接收这些通知并作出相应的反应。 ##### 1.2 ...

    探索NSObject的协议精髓:Objective-C中的基础方法解析

    - **`- (BOOL)isEqual:(id)object`**:比较两个对象是否相等。此方法允许对对象进行等价性检查。 - **`- (NSUInteger)hash`**:返回对象的哈希值。通常与 `-isEqual:` 配合使用,以提高效率。 - **`- (NSString *)...

    NSObject脑图

    NSObject脑图

    IOS消息转发

    `没有处理消息,最后会进入完整的消息转发流程,调用`- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector`获取方法签名,然后通过`NSInvocation`发送消息给`- (void)forwardInvocation:...

    runtime 消息发送和转发 动态创建类添加属性方法等

    6. **添加属性**:添加属性涉及到两个步骤:声明属性和实现存取方法。可以使用`class_addProperty()`添加属性,然后通过`class_getProperty()`获取属性信息。接着,为属性添加对应的getter和setter方法,可以使用`...

    NSObject+RunTimeUtilit

    4. **方法的交换(Method Swizzling)**:Runtime的另一个强大功能是方法交换,通过`method_exchangeImplementations`函数,可以替换两个方法的实现。这常用于AOP(面向切面编程),比如在不修改原代码的情况下添加...

    ios试题总结

    这一过程涉及到两个关键方法:`methodSignatureForSelector:` 和 `forwardInvocation:`。 - `methodSignatureForSelector:` 方法负责为即将转发的消息创建方法签名。如果没有匹配的方法签名,则尝试为消息创建一个...

    NSObject-Description-category:更快地使用描述方法

    而`description`方法是`NSObject`中的一个关键方法,用于返回一个对象的字符串表示,通常用于调试和日志记录。`NSObject+Description`这个类别(Category)是为了提供一种更快捷、更方便的方式来使用`description`...

    swift-PYTheme通过NSObject的分类实现使用简单的主题更换

    在PYTheme中,类别被用来扩展`NSObject`,使得所有的Swift对象都可以方便地与主题系统进行交互。 PYTheme的核心思想是将主题定义为一组颜色、图片等资源的集合,通过一个全局的主题管理器来控制这些资源的切换。当...

    iOS中NSObject的两种含义:类和协议详解

    协议中&lt;NSobject&gt;是什么意思? 子类继承了父类,子类会遵守父类遵守的协议吗? 会遵守NSObject协议,但是只在头文件中声明,编译器是不会自动生成实例变量的。需要自己处理getter和setter 方法 NS/CF/CG/CA/UI这些...

    iOS NSObject对象的本质、内存分配、ISA指针及superclass底层源码分析.pdf

    值得一提的是,如果我们计算NSObjct大小的话,会发现malloc_size方法返回的是16字节,原因如下:OC的底层代码中对给对象分配的最小内存空间做了限制,限制最小为16字节,所以NSObject对象虽然仅仅有一个ISA指针,...

    自己研究的runtime知识和一些方法调用,KVO的原理

    - `method_exchangeImplementations()`: 交换两个方法的实现。 - `object_setIvar()` 和 `object_getIvar()`: 设置和获取实例变量的值。 - `addMethod()` 和 `removeMethod()`: 添加或删除方法。 - `...

    InterView-NSObject的内存分析

    在NSObject类中,`isa`通常指向元类(meta-class),元类是类的类,负责存储类的方法列表。 其次,OC对象还包含实例变量。实例变量是对象内部存储数据的地方,这些数据可以是基本类型如int、float,也可以是其他...

    ios-扩展NSObject实现Json转模型,字典转模型.zip

    利用runtime对NSObject进行分类扩展,解决字典转Model的问题 原理:http://www.jianshu.com/p/71454166c397 github:https://github.com/cccgoodboy/CCModel 喜欢请给个star 谢谢!

    NSObject-Serialize:从 NSDictionary 到 NSObject 或从 NSObject 到 NSDictionary 的简单方法

    NSObject-序列化用法要运行示例项目, ... 要安装它,只需将以下行添加到您的 Podfile 中: pod "NSObject-Serialize"作者ipconfiger, 执照NSObject-Serialize 在 MIT 许可下可用。 有关详细信息,请参阅许可证文件。

    使用Delegate在两个ViewController间传值

    本篇将详细介绍如何在两个ViewController之间利用委托协议进行值传递,以及它与Android中`onActivityResult`方法的相似之处。 首先,我们需要理解什么是委托协议。在Objective-C或Swift中,委托是一种设计模式,...

    协议与委托 (Protocol and Delegate) 实例解析demo

    因为,NSObject是顶级父类,在NSObject中添加了该方法,也就是说通过继承关系,所有的类中都有该方法。 正式协议是通过protocol指定的一系列方法的声明,然后由实现该协议的类自己去实现这些方法。而非正式协议是...

Global site tag (gtag.js) - Google Analytics