zzz Objective-C的消息传递机制

各种语言都有些传递函数的方法: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点要注意的:

  1. 因为方法调用有self(调用对象)和_cmd(选择器)这2个隐含参数,因此设置参数时,索引应该从2开始。
  2. 因为参数是对象,所以必须传递指针,即&object。
  3. 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, "[email protected]:ii");
NSLog(@"%d", [test sum:1 and:2]);
时间: 2024-10-10 22:10:14

zzz Objective-C的消息传递机制的相关文章

消息传递机制

每个应用或多或少都由一些需要相互传递消息的对象结合起来以完成任务.在这篇文章里,我们将介绍所有可用的消息传递机制,并通过例子来介绍怎样在苹果的框架里使用.我们还会选择一些最佳范例来介绍什么时候该用什么机制. 虽然这一期的主题是关于 Foundation 框架的,但是我们会超出 Foundation 的消息传递机制 (KVO 和 通知) 来讲一讲 delegation,block 和 target-action 几种机制. 当然,有些情况下该使用什么机制没有唯一的答案,所以应该按照自己的喜好去试试

android触碰消息传递机制

前阵子要的工作是给桌面(Launcher启动器,其实也是一个activity)添加一个触摸特效(一个View),而这个特效是每次触碰都会有,不管你在桌面上做什么操作都会显示特效!之前一直摸索着不知道如何入手,后来慢慢的实验之后才知道有个android触碰消息传递机制.自己摸索的确很慢,要是早点知道这个机制那将会事半功倍. 用户的每次触碰(onClick,onLongClick,onScroll,etc.)都是由一个ACTION_DOWN+n个ACTION_MOVE+1个ACTION_UP组成的,

Handler、Looper消息传递机制

一.Handler消息传递机制初步认识: (一).引入: 子线程没有办法对UI界面上的内容进行操作,如果操作,将抛出异常:CalledFromWrongThreadException 为了实现子线程中操作UI界面,Android中引入了Handler消息传递机制,目的是打破对主线程的依赖性. 什么是Handler? handler通俗一点讲就是用来在各个线程之间发送数据的处理对象.在任何线程中,只要获得了另一个线程的handler,则可以通过  handler.sendMessage(messa

Android异步消息传递机制源码分析&amp;&amp;相关知识常被问的面试题

1.Android异步消息传递机制有以下两个方式:(异步消息传递来解决线程通信问题) handler 和 AsyncTask 2.handler官方解释的用途: 1).定时任务:通过handler.postDelay(Runnable r, time)来在指定时间执行msg. 2).线程间通信:在执行较为耗时操作的时候,在子线程中执行耗时任务,然后handler(主线程的)把执行的结果通过sendmessage的方式发送给UI线程去执行用于更新UI. 3.handler源码分析 一.在Activ

Android笔记二十五.Android事件Handler消息传递机制

因为Android平台不同意Activity新启动的线程訪问该Activity里的界面控件.这样就会导致新启动的线程无法动态改变界面控件的属性值.但在实际Android应用开发中,尤其是涉及动画的游戏开发中,须要让新启动的线程周期性地改变界面控件的属性值,这就须要借助Handler的消息传递机制实现. 一.Handler类简单介绍 1.功能 Handler类主要有两个作用 (1)在新启动的线程中发送消息; (2)在主线程中获取消息.处理消息.即当须要界面发生变化的时候.在子线程中调用Handle

解析Android的 消息传递机制Handler

1. 什么是Handler: Handler 网络释义"操纵者,管理者的"意思,在Android里面用于管理多线程对UI的操作: 2. 为什么会出现Handler: 在Android的设计机制里面,只允许主线程(一个程序第一次启动时所移动的线程,因为此线程主要是完成对UI相关事件的处理,所以也称UI线程) 对UI进行修改等操作,这是一种规则的简化,之所以这样简化是因为Android的UI操作时线程不安全的,为了避免多个线程同时操作UI造成线程安全 问题,才出现了这个简化的规则. 由此以

Android基础入门教程——3.3 Handler消息传递机制浅析

Android基础入门教程--3.3 Handler消息传递机制浅析 标签(空格分隔): Android基础入门教程 本节引言 前两节中我们对Android中的两种事件处理机制进行了学习,关于响应的事件响应就这两种:本节给大家讲解的 是Activity中UI组件中的信息传递Handler,相信很多朋友都知道,Android为了线程安全,并不允许我们在UI线程外操作UI:很多时候我们做界面刷新都需要通过Handler来通知UI组件更新!除了用Handler完成界面更新外,还可以使用runOnUiT

iOS开发——OC篇&amp;消息传递机制(KVO/NOtification/Block/代理/Target-Action)

iOS开发中消息传递机制(KVO/NOtification/Block/代理/Target-Action) 今晚看到了一篇好的文章,所以就搬过来了,方便自己以后学习 虽然这一期的主题是关于Foundation Framework的,不过本文中还介绍了一些超出Foundation Framework(KVO和Notification)范围的一些消息传递机制,另外还介绍了delegation,block和target- action. 大多数情况下,消息传递该使用什么机制,是很明确的了,当然了,在某

我理解的Hanlder--android消息传递机制

每一个学习Android的同学都会觉得Handler是一个神奇的东西,我也一样,开始我以为我懂了Handler的机制,后来发现自己是一知半解,昨天想想,我能否自己实现一个Handler,让子线程与ActivityUI线程通信,如果能够自己实现一个Handler,那必然是对Handler的消息传递机制理解渗透了. 一.引入 Android的UI是单线程控制的,实际上,成功的UI框架都是基于单线程的,多线程的UI框架往往因为解决并发和死锁的艰难而胎死腹中.只有UI线程能控制界面控件,但我们总是希望子

Chrome 消息传递机制

Chrome插件开发入门(二)——消息传递机制 Blog | Qiushi Chen 2014-03-31 9538 阅读 Chrome 插件 由于插件的js运行环境有区别,所以消息传递机制是一个重要内容.阅读了很多博文,大家已经说得很清楚了,直接转一篇@姬小光 的博文,总结的挺好.后面附一个自己写过的demo,基本就对消息传递能够熟悉了. 在开发 Chrome 扩展时经常需要在页面之间进行通讯,比如 background 与 content script 之间,background 与 pop