在对象上调用方法是OC中经常使用的功能。用OC术语来说这叫做:“传递消息”(pass a message)。消息有“名称”(name)或者“选择子”(selector),可以接收参数,而且可能还有返回值。
由于OC是C的超集,所以最好理解C语言的函数调用方式。C语言使用“静态绑定”,就是说在编译期就能决定运行时所应调用的函数。以下列代码为例:
#import <stdio.h> void printHello(){ printf("Hello world\n"); } void printGoodBye(){ printf("Goodbye world\n"); } void doTheThing(int type){ if(type == 0){ printfHello(); }else{ printGoodbye(); } return 0; }
编译器在编译代码的时候就已经知道程序中有printHello与printGoodBye这两个函数了,于是直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的。若将刚才那段代码写成下面这样,会如何呢?
#import <stdio.h> void printHello(){ printf("Hello world\n"); } void printGoodBye(){ printf("Goodbye world\n"); } void doTheThing(int type){ void (*fun)(); if(type == 0){ fun = printfHello(); }else{ fun = printGoodbye(); } fun(); return 0; }
这时就得使用“动态绑定”(dynamic binding),因为所要用的函数知道运行时才能确定。编译器在这个环境下生成的指令与刚才哪个例子不同,在第一个例子中if和else语句中都有函数调用指令。而第二个例子中,只有一个函数调用指令,不过待调用的地址无法硬编码在指令之中,而是要运行期读出来。
在OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通C语言函数,然而对象收到消息后,究竟该掉哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得OC称为一门真正的动态语言。
给对象发消息可以这样写:
id returnValue = [someObject messageName:parameter];
本例中,someObject叫做“接收者”,messageName叫做“选择子”。选择子与参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制的核心函数,叫做objc_msgSend,其原型如下:
void objc_msgSend(id self ,SEL cmd,...);
这是个参数个数可变长的函数,经过转化,刚才的函数被转化为这样:
id returnValue = objc_msgSend(someObject,@selector(messageName),parameter);
objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜索其“方法列表”(list of methods)如果能找到与选择子名称相符的方法,就跳至其实现代码。若找不到,那就沿着继承体系继续向上查找,等找到名称相符的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”
这么说来,想调用一个方法似乎需要很多步骤。所幸objc_msgSend会将匹配结果缓存在“快速映射表”里面。实际上,消息派发(message dispatch)并非应用程序的瓶颈所在。
前面讲的这部分内容只描述了部分消息的调用过程,其他“边界情况”(edge case)则需要交由OC运行环境中的另一些函数来处理:
● objc_msgSend_stret 待发的消息要返回结构体
● objc_msgSend_fpret 消息返回的是浮点数
● objc_msgSend_super 要给超类发消息。
<return_type> class_selector(id self,SEL _cmd,...)
每个类里都有一张表格(参考下面第14条关于isa的描述),其中的指针都会指向这个函数,而选择子的名称则是查表时所用的“键”。objc_msgSend等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。请注意,原型的样子和objc_msgSend很像。这不是巧合,而是利用“尾调用优化”(tail-call optimization)技术,令“跳至方法实现”这一操作变得更简单。
在实际编写OC时,无须担心这些问题,开发者应该了解其底层工作原理。代码究竟是如何执行的,而且能理解为何在调试的时候,栈信息中总是出现objc_msgSend
【本节要点】
● 消息由接收者、选择子及参数构成。给某对象“发送消息”也就相当于在该对象上“调用方法”
● 发给某对象的去全部消息都要由“动态消息派发系统”(dynamic message dispatch system)来处理。该系统会查出对应的方法,并执行其代码
-[__NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87 *** Terminating app due to uncaught exception 'NSInvalidArgumentException',reason:'-[__NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87'
id autoDictionaryGetter(id self,self _cmd); void autoDictionarySetter(id self, SEL _cmd,id value); +(BOOL)resolveInstanceMethod:(SEL)selector{ NSString *selectorString = NSStringFromSelector(selector); if(/*selector is from a @dynamic property*/) { if([selectorString hasPrefix:@"set"]){ class_addMethod(self.selector,(IMP)autoDictionarySetter,"v@:@"); }else{ class_addMethod(self.selector,(IMP)autoDictionaryGetter,"v@:@"); } return YES; } return [super resolveInstanceMethod:selector]; }首先将选择子化为字符串,然后检测其是否表示设置方法。若前缀为set,则表示“设置方法”,否则就是“获取方法”。
-(id) forwardingTargetForSelector:(SEL)selector
如果转发算法已经来到这一步,那么唯一能做的就是启动完整的消息转发机制了。首先创建NSInvacaton对象,把与尚未处理的那条消息有关的全部细节都封于其中,此对象包含选择子、目标(target)及参数。在触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派给目标对象。
此步骤会调用下列方法转发消息:
-(void) forwardInvocation:(NSInvocation*)invocation
这个方法可以实现得很简单:只需要改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是该换选择子,等等。
实现此方法时,若发现某调用操作不应本类处理,则需要调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,次异常表示选择子最终未能得到处理。
下图描述了消息转发的各个步骤:
接收者在每一步均有机会处理消息。步骤越往后,消息处理的代价就越大。
id(*IMP)(id,SEL,..)
OC运行期系统提供的几个方法都能够用来操作这张表。开发者可以向其新增选择子,也可以改变某选择子对应的方法实现,还可以交换选择子所映射到的指针。经过几次操作之后,类的方法表就会标称如图这个样子
在新的映射表中,多了一个名为newSelector的选择子,capitalizedString的实现也变了,而lowercaseString与uppercaseString的实现则互换了。上述修改均无需编写子类,只要修改了“方法表”的布局,就会反映到程序中所有的NSString实例之上。这下大家见识到此特性的强大之处了吧
下面看一下如何互换两个方法实现。想交换方法实现,可用下列函数:
void method_exchangeImplementations(Method m1,Method m2)
此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:
Method class_getInstanceMethod(Class aClass,SEL aSelector)
具体操作如下:
Method originalMethod = class_getInstanceMethod([NSStringclass],@selector(lowercaseString)); Method swappedMethod = class_getInstanceMethod([NSStringclass],@selector(uppercaseString)); method_exchangeImplementations(originalMethod,swappedMethod);
实际应用,可以为那些黑盒方法增加日志技术功能。比如NSString 的lowercaseString接口,想在lowercaseString中添加log,该如何做呢
@interface NSString (EOCMyAddtions) -(NSString*)eoc_myLowercaseString; @end @implementation NSString(EOCMyAdditions) -(NSString*) eoc_myLowercaseString{ NSString *lowercase = [self eoc_mylowercaseString]; NSLog(@"%@ =>%@",self,lowercase); return lowercase; } @end这段代码看上去好像会陷入递归调用的死循环,不过大家要记住,此方法是准备和lowercaseString方法互换的。所以,在运行期,eoc_myLowercaseString选择子实际上对应于原有的lowercaseString方法实现。最后,通过下列代码叫魂这两个方法实现:
Method originalMethod = class_getInstanceMethod([NSStringclass],@selector(lowercaseString)); Method swappedMethod = class_getInstanceMethod([NSStringclass],@selector(roc_myLowercaseString)); method_exchangeImplementations(originalMethod,swappedMethod);
执行完上诉代码之后,只要在NSString实例上调用lowercaseString方法,就会输出log了。
【本节要点】● 在运行期间,可以向类中新增或者替换选择子所对应的方法实现。
● 使用另一份实现来替换缘由的方法实现,这道工序叫:“方法调配”,开发者常用此技术向原有实现中添加新功能。
● 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。
OC实际上是一门及其动态的语言。第11条讲解了运行期系统如何查找并调用某方法的实现代码,第12条则讲述了消息转发的原理:如果类无法立即响应某个选择子,那么就会启动消息转发流程。然而,消息的接收者究竟是何物?是对象本身么?运行期系统如何知道某个对象的类型呢?对象类型并非在编译器就绑定好了,而是要在运行期查找。而且还有个特殊类型id,它能够指代任意的OC对象类型。
我们先讲一些基础知识,看看OC对象本质是什么。每个OC对象都是指向某块内存数据的指针。所以在声明变量时,类型后面都要跟一个星号(*)
NSString *pointerVariable = @"Some thing";
编过C语言程序的人都知道什么意思。该变量“指向”(point to)NSString实例。所有OC对象都是如此,如果想把OC对象声明在栈上,编译器会报错:
Sting stackVariable = @"Some thing"; //error: interface type cannot be statically allocated
对于通用id类型,由于其本身已经是指针了,所以我们能够这样写:
id genericTypeString = @"Some thing";
描述OC对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也在定义这里:
typedef struct objc_object{ Class isa; } *id;
由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为is a指针。例如,刚才的例子中所有的对象is a NSString,所以其“is a”指针就指向NSString。Class对象也定义在运行期程序库的头文件中:
typedef struct objc_class *Class; struct objc_class{ Class isa; Class super_class; const char* name; long version; long instance_size; struct objc_ivar_list *ivars; struct objc_method_list **methodLists; struct objc_cache *cache; struct objc_protocol_list *protocols };此结构图存放类的“元数据”(metadata),例如类的实例实现了几个方法,具备多少实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为OC对象。结构体里还有个变量叫做super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做“元类”(metaclass),用来表述类对象本身所具备的元数据。“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。
假设有个名为SomeClass的子类从NSObject中继承而来,则其继承体系如下图所示
super_class指针确立了继承关系,而isa指针描述了实例所属的类。通过这张布局关系图即可执行“类型信息查询”。我们可以查出对象是否能响应某个选择子,是否遵从某项协议,并且能看出此对象位于“类继承体系”的那一部分。每个类仅有一个”类对象”,而每个“类对象”仅有一个与之相关的“元类”。
下图可以证明:多个实例中的isa就是同一个:
可以用类型信息查询方法来检视类继承体系。“isMemberOfClass:”能够判断出对象是否为某个特定类的实例。而“isKindOfClass:”则能够判断出对象是否为某类或其派生的实例。
NSMutableDictionary *dict = [NSMutableDictionary new]; [dict isMemberOfClass:[NSDictionary class]];//no [dict isMemberOfClass:[NSMutableDictionary class]];//yes [dict isKindOfClass:[NSDictionary class]];//yes [dict isKindOfClass:[NSArray class]];//no由于OC使用“动态类型系统”(dynamic typing),所以用于查询对象所属类的类型信息查询非常有用。从collecting中获取对象时,通常是id类型,那就可以使用查询类型信息方法了。
【本节要点】
● 每个实例都有一个指向Class对象的指针,用以表明其类型。从这些Class对象则构成了类的继承体系。
● 如果对象类型无法在编译期确定,那么就应该使用类型查询方法来探知
● 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。