Objective-C具有相当多的动态特性,有动态类型(Dynamic typing),动态绑定(Dynamic binding)和动态加载(Dynamic loading)。
而在这之后,OC在底层也提供了相当丰富的运行时的特性,
比如枚举类属性方法(class_copyIvarList)、获取方法实现等。
This document has the following chapters:
int main(int argc, const char * argv[]){ /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; NSLog((NSString *)&__NSConstantStringImpl__var_folders_rs_xpndkrtn3gbc99jjvrdxxlj00000gn_T_main_599312_mi_0); Person *p = ((id (*)(id, SEL))(void *)objc_msgSend)((id)((id (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")); ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)p, sel_registerName("setAge:"), 16); NSLog((NSString *)&__NSConstantStringImpl__var_folders_rs_xpndkrtn3gbc99jjvrdxxlj00000gn_T_main_599312_mi_1,((int (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("age"))); } return 0;}
// 第 2 句
Person *p = [[Personalloc]init];
p.age = 10;//相当于[p setAge:10];
去掉 强转 后的cpp代码:运行时代码:发消息
objc_msgSend(p, @selector(setAge:),16);
objc_msgSend(p, @selector(setName:),@"nana");
#import
#import
objc_msgSend(self,NSSelectorFromString(setXxx:),dict[xxx]);
例如:
objc_msgSend(self,NSSelectorFromString(setName:),@"nana");
NSObject+Ivar.m// cmd_01_runtime Created by beyond on 14-9-29.// Copyright (c) 2014年 com.beyond. All rights reserved.//#import "NSObject+Ivar.h"// 字典转模型,下面是通过运行时实现#import #import @implementation NSObject (Ivar)// 使用字典,进行填充 成员属性- (void)populateWithDict:(NSDictionary *)dict{ Class cls = [self class]; unsigned int count; // Ivar 全称实例变量,就是类的成员属性 // 通过 运行时代码,获得,类内部的成员属性的个数 放到count内存地址 // 返回结果 是一个指针,其所指向的内存空间为 首个Ivar的内存地址,类似于数组名 Ivar * ivarArr = class_copyIvarList(cls, &count); // 遍历数组 (数组名 存放的就是数组首地址,就是一个指针,两者可互换) for (int i = 0 ; i < count; i++) { // 可以把 Ivar当成一个基本类型,如double,int Ivar ivar = ivarArr[i]; const char * name = ivar_getName(ivar); // 输出:_create_time NSLog(@"%s",name); // 1. c 字符,转成 OC 【_create_time】 NSMutableString *ocName = [NSMutableString stringWithUTF8String:name]; // 2. 删掉最前面的_ 【create_time】 [ocName deleteCharactersInRange:NSMakeRange(0, 1)]; // 2. 从字典取值时,用的key 【create_time】 NSString *key = [ocName copy]; id value = dict[key]; // 3. 自定义方法,将【create_time】转成【setCreate_name】 NSString *setter = [self ivarNameToMsgName:ocName]; // 4. 最后一步,使用运行时,填充数据 // 4.1 如果是NSNumber,转成int if ([value isKindOfClass:[NSNumber class]]) { int intValue = [value intValue]; // 如年龄 21 objc_msgSend(self, NSSelectorFromString(setter),intValue); } else { // 如果是...其他特殊情况,都要一一将对象类型,转成,基本类型 objc_msgSend(self, NSSelectorFromString(setter),value); } } }// 自定义方法,将【_create_time】转成【setCreate_name】- (NSString *)ivarNameToMsgName:(NSMutableString *)ocName{ // 1. 取出首字母,变成大写 NSString *capital = [ocName substringToIndex:1].uppercaseString; // 再将 首字母 由小写的,替换成大写的,此时,【Create_time】 [ocName replaceCharactersInRange:NSMakeRange(0, 1) withString:capital]; // 2. 拼接set,变成,【setCreate_time】 return [NSString stringWithFormat:@"set%@:", ocName];}@end
[selfpopulateWithDict:dict];
Objective-C具有相当多的动态特性,有动态类型(Dynamic typing),动态绑定(Dynamic binding)和动态加载(Dynamic loading)。
而在这之后,OC在底层也提供了相当丰富的运行时的特性,
比如枚举类属性方法(class_copyIvarList)、获取方法实现等。
id obj = 某实例对象;if ([obj isKindOfClass:某类]){ 某类 *classSpecifiedInstance = (某类 *)obj; // 现在强转后,已经是某类的实例对象了}
-isKindOfClass:是NSObject的方法,
用意确定某个NSObject对象是否是某个类的成员。
与之相似的为-isMemberOfClass:
可以用以确定某个对象是否是某个类或其子类的成员。
这两个方法为典型的introspection【内省】方法。
在确定对象为某类成员后,可以安全地进行强制转换。
动态绑定所做的,即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。
这里所指的属性和方法包括了原来没有在类中实现的,后来在运行时才需要的新加入的实现。
向一个NSObject对象发送-respondsToSelector:
或者-instancesRespondToSelector:等 来确定对象是否可以对某个SEL做出响应,
而在OC消息转发机制被触发之前,
对应的类的+resolveClassMethod:和+resolveInstanceMethod:将会被调用,
在此时有机会动态地向类或者实例添加新的方法,也即类的实现是可以动态绑定的。
void dynamicMethodIMP(id self, SEL _cmd){ // 代码 ....}
//该方法在OC消息转发 生效前被调用
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{ if (aSEL == @selector(resolveThisMethodDynamically))
{
//向[self class]中新加入一个方法:返回为void,SEL名字为aSEL,实现的具体内容为dynamicMethodIMP
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, “v@:”);
return YES;
}
return [super resolveInstanceMethod:aSel];
}
可以在任意需要的地方,通过调用class_addMethod或者method_setImplementation(前者添加实现,后者替换实现),来完成动态绑定的需求。
这类运行时特性大多由/usr/lib/libobjc.A.dylib这个动态库提供,
里面定义了对于类、实例成员、成员方法和消息发送的很多API,
包括获取类实例变量列表,替换类中的方法,为类成员添加变量,动态改变方法实现等
一个简单的例子,比如在开发Universal应用或者游戏时,如果使用IB构建了大量的自定义的UI,
那么在由iPhone版转向iPad版的过程中所面临的一个重要问题就是如何从不同的nib中加载界面?
在iOS5之前,所有的UIViewController在使用默认的界面加载时(init或者initWithNibName:bundle:),
都会走-loadNibNamed:owner:options:。
但是,由于无法拿到-loadNibNamed:owner:options的实现,所以无法对其重载。
因此,只能通过运行时,使用自己实现的新的类似-loadNibNamed:owner:options的方法将系统的原方法替换掉,
同时保证非iPad的设备还走原来的loadNibNamed:owner:options方法。
交换自己实现的loadPadNibNamed:owner:options和系统的loadNibNamed:owner:options,
之后所有的loadNibNamed:owner:options消息,
都将会发为loadPadNibNamed:owner:options,由自己的代码进行处理。
+(BOOL)swizze { Method 旧方法 = class_getInstanceMethod(self, @selector(loadNibNamed:owner:options:)); if (!oldMethod) { return NO; }Method 新方法 = class_getInstanceMethod(self, @selector(loadPadNibNamed:owner:options:));if (!newMethod) {return NO;}method_exchangeImplementations(oldMethod, newMethod);return YES;}
loadPadNibNamed:owner:options的实现如下,注意在其中的loadPadNibNamed:owner:options由于之前已经进行了交换,因此实际会发送为系统的loadNibNamed:owner:options。以此完成了对不同资源的加载。
-(NSArray *)loadPadNibNamed:(NSString *)name owner:(id)owner options:(NSDictionary *)options { NSString *newName = [name stringByReplacingOccurrencesOfString:@"@pad" withString:@""]; newName = [newName stringByAppendingFormat:@"@pad"];
//判断是否存在
NSFileManager *fm = [NSFileManager defaultManager];
NSString* filepath = [[NSBundle mainBundle] pathForResource:newName ofType:@”nib”];
//这里调用的loadPadNibNamed:owner:options:实际为为交换后的方法,即loadNibNamed:owner:options:
if ([fm fileExistsAtPath:filepath]) {
return [self loadPadNibNamed:newName owner:owner options:options];
} else {
return [self loadPadNibNamed:name owner:owner options:options];
}
}
1. 传统的面向过程的语言开发,例如c语言。
实现c语言编译器很简单,只要按照语法规则实现一个LALR语法分析器就可以了。
这里实现了编译器其中最最基础和原始的目标之一就是把一份代码里的函数名称,转化成一个相对内存地址,把调用这个函数的语句转换成一个jmp跳转指令。
在程序开始运行时候,调用语句可以正确跳转到对应的函数地址。 这样很好,也很直白,但是。。。太死板了。所有事情都是预先定义死的!
2. 希望灵活,于是需要开发面向对象的语言,例如c++。
c++在c的基础上增加了类的部分。
但这到底意味着什么呢?
在写它的编译器要如何考虑呢?
其实,就是让编译器多绕个弯,在严格的c编译器上增加一层类处理的机制,把一个函数限制在它处在的class环境里,每次请求一个函数调用,先找到它的对象, 其类型,返回值,参数等等,确定了这些后再jmp跳转到需要的函数。
这样很多程序增加了灵活性,
因此同样一个函数调用会根据请求参数和类的环境返回完全不同的结果。
增加类机制后,就模拟了现实世界的抽象模式,
不同的对象有不同的属性和方法。
同样的方法,不同的类有不同的行为!
这里大家就可以看到作为一个编译器开发者都做了哪些进一步的思考。但是。。。还是死板, 仍然叫c++是static language。
3. 希望更加灵活! 于是完全把上面哪个类的实现部分抽象出来,做成一套完整运行阶段的检测环境。
这次再写编译器甚至保留部分代码里的sytax名称,名称错误检测,runtime环境注册所有全局的类,函数,变量等等信息等等,
可以无限的为这个层增加必要的功能。
调用函数时候,会先从这个运行时环境里检测所以可能的参数再做jmp跳转,这就是runtime。
编译器开发起来比上面更加弯弯绕。
但是这个层极大增加了程序的灵活性。
例如当调用一个函数时候,前2种语言,很有可能一个jmp到了一个非法地址导致程序crash, 但是在这个层次里面,runtime就过滤掉了这些可能性。
这就是为什么dynamic langauge更加强壮。
因为编译器和runtime环境开发人员已经帮你处理了这些问题。
好了上面说着这么多,再返回来看objective-c.
现在你是不是能理解这样的语句了呢?
id obj=self;
if ([obj respondsToSelector:@selector(function1:)) {
}
if ([obj isKindOfClass:[NSArray class]] ) {
}
if ([obj conformsToProtocol:@protocol(myProtocol)]) {
}
if ([[obj class] isSubclassOfClass:[NSArray class]]) {
}
[obj someNonExistFunction];
看似很简单的语句,但是为了让语言实现这个能力,语言开发者要付出很多努力实现runtime环境。
这里运行时环境,处理了弱类型、函数存在检查工作。
runtime会检测注册列表里是否存在对应的函数,类型是否正确,
最后确定下来正确的函数地址,
再进行保存寄存器状态,压栈,函数调用等等实际的操作。
id knife=[Knife grateKnife];
NSArray *monsterList=[NSArray array];
[monsterListmakeObjectsPerformSelector:@selector(killMonster:) withObject:knife];
在c,c++年代去完成这个功能是非常麻烦的,但是动态语言却非常简单。
关于执行效率问题。
“静态语言执行效率要比动态语言高”,这句没错。
因为一部分cpu计算损耗在了runtime过程中。
而静态语言生成的机器指令更简洁。
正因为知道这个原因,所以开发语言的人付出很大一部分努力为了保持runtime小巧上。
所以objecitve-c是c的超集+一个小巧的runtime环境。
Class isa; /* metaclass */ Class super_class /* 父类的地址 */ const char *name /* 类名称 */ long version /* 版本 */ long info /* 类信息 */ long instance_size /* 实例大小 */ struct objc_ivar_list *ivars /* 实例参数列表*/ struct objc_method_list **methodLists /* 方法列表 */ struct objc_cache *cache /* 方法缓存 */ struct objc_protocol_list *protocols /* protocol链表*/
一 函数调用概述
Objective-C不支持多重继承(同Java和Smalltalk),而C++语言支持多重继承。
Objective-C是动态绑定,它的类库比C++要容易操作。
Objective-C在运行时可以允许根据字符串名字来访问方法和类,还可以动态连接和添加类。
C++ 跟从面向对象编程里的Simula 67(一种早期OO语言)学派,
而Objecive-C属于Smalltalk学派,Simula 67学派更安全,因为大部分错误可以在编译时查出。
在C++里,对象的静态类型决定你是否可以发送消息给它,
而对Objective-C来说,由动态类型来决定。
二 消息的产生
在Objective-c中消息一直到运行时才能绑定到对应的函数:
[reveiver message];
编译器在处理时会将上面的表达式处理称下面这种形式:
objc_msgSend(receiver,selector);
如果方法有多个参数的时候会处理成下面这种形式:
objc_msgSend(receiver,selector,arg1,arg2,…….);
现在知道了在Objective-c中函数调用都会被编译器进行预处理,调用obj_msgSend函数进行消息的发送。
三 类对象中与消息有关的结构
首先看一下与消息传递有关的几个数据结构和数据类型。
1. struct objc_method_list **methodLists
在类对象objc_class中有一个结构体 struct objc_method_list **methodLists ,它其实是方法的映射表。
struct objc_method_list { struct objc_method_list *obsolete int method_count struct objc_method method_list[1] }
在objc_method_list中包含一个objc_method结构体
struct objc_method { SEL method_name char *method_types IMP method_imp }
- (void) viewDidLoad { [super viewDidLoad]; //声明一个函数指针 void (*MyPrint)(id,SEL,NSString*); MyPrint = ( void (*)(id,SEL,NSString*) )[self methodForSelector:@selector(Print:)]; MyPrint(self,@selector(Print:),@"Hello World"); } - (void) Print:(NSString*) str { NSLog(@"%@",str); }
首先,编译器根据函数声明生成一个唯一函数ID,每个实例变量的第一个成员是isa,它指向类对象,在类对象中保存有该类所拥有的方法列表,通过生成的的函数ID可以找到其对应的函数地址,从而调用函数。如果在当前类对象的函数映射表中没有找到函数的话,就继续搜索其父类中(每个类对象的super_class 存储了父类的类对象地址),如果到达其根类还是没找到的话,会报运行时错误。 其过程如下图所示:
消息的成功传递依赖于两个重要的要素,通过实例中的 isa指针找到实例所属的类对象,然后通过类对象中的消息映射表找到所要调用的函数。
首先通过传递的selector参数找到消息映射表中的函数。然后调用函数,将实例的地址和参数传递给调用的这 个函数。最后返回返回值。
3.消息接收者对象(指向消息接收者对象的指针)以及方法中指定的参数传递给方法实现 IMP。
4.最后,将方法实现的返回值作为该函数的返回值返回。
注:编译器会自动插入调用消息函数objc_msgSend,无须在代码中显示调用该消息函数。
五 消息传递小结
Objective-c的运行时和MFC的RTTI挺像的。
下图是MFC的运行时机制,CRuntimeClass相当于Objectiv-c的object_clas类对象,在CRuntimeClass中有指向基类函数的指针,与MFC不同的是object_class没有类似与m_pNext_ClassMFC成员。
1.编译器会将消息转换为对消息函数 objc_msgSend的调用,该函数有两个主要的参数:消息接收者 receiver 和消息对应的方法ID,即SEL, 同时接收消息不定参数列表。
下面再做一下详细消息传递过程:
正因为有像CRuntimeClass和object_class这些类类型的存在才使得它们拥有了运行识别,动态创建,序列化等机制。
一 消息查找优化
发现如果每次函数调用都经历上面的过程(那函数调用的效率就会很低,尤其是当类的继承层次很多的时候,它需要一层层的查找其效率将会更低,为了加快查找调用的速度,Objective-c对消息查找做了优化。
从类对象知道它含有一个 struct objc_cache *cache成员,这个缓存就是为了提高查找的效率的。每个类都有自己的缓存,同时包括继承的方法和在该类中定义的方法。当在查找IMP 时:
1.首先去该类的方法 cache 中查找,如果找到了就返回它
2.如果没有找到,就去该类的方法列表中查找。如果在该类的方法列表中找到了,则将 IMP 返回,并将它加入cache中缓存起来。根据最近使用原则,这个方法再次调用的可能性很大,缓存起来可以节省下次调用再次查找的开销。
关于消息转发的作用,可以考虑如下情景:假设,需要设计一个能够响应negotiate消息的对象,并且能够包括其它类型的对象对消息的响应。 通过在negotiate方法的实现中将negotiate消息转发给其它的对象来很容易的达到这一目的。 更进一步,假设希望的对象和另外一个类的对象对negotiate的消息的响应完全一致。一种可能的方式就是让的类继承其它类的方法实现。 然而,有时候这种方式不可行,因为的类和其它类可能需要在不同的继承体系中响应negotiate消息。 虽然的类无法继承其它类的negotiate方法,但仍然可以提供一个方法实现,这个方法实现只是简单的将negotiate消息转发给其他类的对象,就好像从其它类那儿“借”来的现一样。如下所示:- negotiate { if ([someOtherObject respondsToSelector:@selector(negotiate)]) return [someOtherObject negotiate];
这种方式显得有欠灵活,特别是有很多消息都希望传递给其它对象时,就必须为每一种消息提供方法实现。此外,这种方式不能处理未知的消息。当写下代码时,所有需要转发的消息的集合都必须确定。然而,实际上,这个集合会随着运行时事件的发生,新方法或者新类的定义而变化。 forwardInvocation:消息给这个问题提供了一个更特别的,动态的解决方案:当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都从NSObject类中继承了forwardInvocation:方法。然而,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector:。通过实现自己的forwardInvocation:方法,可以在该方法实现中将消息转发给其它对象。 要转发消息给其它对象,forwardInvocation:方法所必须做的有: 1.决定将消息转发给谁,并且 2.将消息和原来的参数一块转发出去消息可以通过invokeWithTarget:方法来转发:- (void) forwardInvocation:(NSInvocation *)anInvocation{ if ([someOtherObject respondsToSelector:[anInvocation selector]]) [anInvocation invokeWithTarget:someOtherObject]; else [super forwardInvocation:anInvocation];}
转发消息后的返回值将返回给原来的消息发送者。可以返回任何类型的返回值,包括: id,结构体,浮点数等。 forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的"吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。
可以通过NSObject的一些方法获取运行时信息或动态执行一些消息:
/*Returns a Boolean value that indicates whether the receiving class is a subclass of, or identical to, a given class.*/
+ (BOOL)isSubclassOfClass:(Class)aClass;
/*Returns a Boolean value that indicates whether instances of the receiver are capable of responding to a given selector.*/
+ (BOOL)instancesRespondToSelector:(SEL)aSelector;