原帖:http://www.cnblogs.com/Mike-zh/p/4557014.htmlhtml
由于Objc是一门动态语言,因此它老是想办法把一些决定工做从编译链接推迟到运行时。也就是说只有编译器是不够的,还须要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。
Runtime其实有两个版本:“modern”和 “legacy”。咱们如今用的 Objective-C 2.0 采用的是现行(Modern)版的Runtime系统,只能运行在 iOS 和 OS X 10.5 以后的64位程序中。而OS X较老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你须要从新编译它的子类,而现行版就不须要。
Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而做出的努力。你能够在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。ios
ios的sdk中 usr/include/objc文件夹下面有这样几个文件
List.h
NSObjCRuntime.h
NSObject.h
Object.h
Protocol.h
a.txt
hashtable.h
hashtable2.h
message.h
module.map
objc-api.h
objc-auto.h
objc-class.h
objc-exception.h
objc-load.h
objc-runtime.h
objc-sync.h
objc.h
runtime.h
都是和运行时相关的头文件,其中主要使用的函数定义在message.h和runtime.h这两个文件中。 在message.h中主要包含了一些向对象发送消息的函数,这是OC对象方法调用的底层实现。 runtime.h是运行时最重要的文件,其中包含了对运行时进行操做的方法。 主要包括:api
/// An opaque type that represents a method in a class definition. 一个类型,表明着类定义中的一个方法 typedef struct objc_method *Method; /// An opaque type that represents an instance variable.表明实例(对象)的变量 typedef struct objc_ivar *Ivar; /// An opaque type that represents a category.表明一个分类 typedef struct objc_category *Category; /// An opaque type that represents an Objective-C declared property.表明OC声明的属性 typedef struct objc_property *objc_property_t; // Class表明一个类,它在objc.h中这样定义的 typedef struct objc_class *Class; struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE;
这些类型的定义,对一个类进行了彻底的分解,将类定义或者对象的每个部分都抽象为一个类型type,对操做一个类属性和方法很是方便。OBJC2_UNAVAILABLE标记的属性是Ojective-C 2.0不支持的,但实际上能够用响应的函数获取这些属性,例如:若是想要获取Class的name属性,能够按以下方法获取:数组
Class classPerson = Person.class; // printf("%s\n", classPerson->name); //用这种方法已经不能获取name了 由于OBJC2_UNAVAILABLE const char *cname = class_getName(classPerson); printf("%s", cname); // 输出:Person
对对象进行操做的方法通常以object_开头
对类进行操做的方法通常以class_开头
对类或对象的方法进行操做的方法通常以method_开头
对成员变量进行操做的方法通常以ivar_开头
对属性进行操做的方法通常以property_开头开头
对协议进行操做的方法通常以protocol_开头
根据以上的函数的前缀 能够大体了解到层级关系。对于以objc_开头的方法,则是runtime最终的管家,能够获取内存中类的加载信息,类的列表,关联对象和关联属性等操做。
例如:使用runtime对当前的应用中加载的类进行打印,别被吓一跳。框架
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { unsigned int count = 0; Class *classes = objc_copyClassList(&count); for (int i = 0; i < count; i++) { const char *cname = class_getName(classes[i]); printf("%s\n", cname); } }
在如下的代码中,都用到了Person类,Person类知识简单的定义了一个成员变量和两个属性函数
@interface Person : NSObject { @private float _height; } @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) int age; @end
对于获取成员变量的列表可使用class_copyIvarList函数,若是想要获取属性列表可使用class_copyPropertyList函数,使用的示例以下:布局
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { Class classPerson = NSClassFromString(@"Person"); // 与下面一句效果同样,能够不用导入头文件 // Class clazz = Person.class; unsigned int count = 0; Ivar *ivarList = class_copyIvarList(classPerson, &count); // 获取成员变量数组 for (int i = 0; i < count; i++) { const char *cname = ivar_getName(ivarList[i]); // 获取成员变量的名字 NSString *name = [NSString stringWithUTF8String:cname]; NSLog(@"%@", name); } NSLog(@"-------------------分割线------------------"); objc_property_t *propertyList = class_copyPropertyList(classPerson, &count); // 获取属性数组 for (int i = 0; i < count; i++) { const char *cname = property_getName(propertyList[i]); NSString *name = [NSString stringWithUTF8String:cname]; NSLog(@"%@", name); } }
以上代码的输出为:测试
2015-06-05 22:28:16.194 runtime终极[4192:195757] _height 2015-06-05 22:28:16.195 runtime终极[4192:195757] _age 2015-06-05 22:28:16.195 runtime终极[4192:195757] _name 2015-06-05 22:28:16.195 runtime终极[4192:195757] -------------------分割线------------------ 2015-06-05 22:28:16.195 runtime终极[4192:195757] name 2015-06-05 22:28:16.195 runtime终极[4192:195757] age
为何会有上面的输出结果,由于@property会作三份工做:
1.生成一个带下划线的成员变量
2.生成这个成员变量的get方法
3.生成这个成员变量的set方法
所以会输出三个成员变量_height、_age和_name。须要注意的是属性名是不带下划线的,和定义时的名字同样。所以能够说:ivarList能够获取到@property关键字定义的属性 ,而propertyList不能够获取到成员变量。也就是:使用ivarList是能够将全部的成员变量和属性都获取的。
当属性是readonly的并且重写了getter时,这种状况仍是会碰见的,好比一个属性是计算型属性,须要依赖其余属性的值计算而来。此时生成的带下划线的成员变量就不在了, 经过ivarList不能获取该属性了。所以当有这种值的时候,不管使用ivarList仍是使用propertyList都没法获取所有的属性或变量。
在进行下一个话题以前:先须要弄清楚另外一个问题:对于一个readonly的属性,究竟是didSet+set好,仍是重写getter好?
大部分的readonly的属性是计算型的,依旧是依赖于其余属性,所以可使用didSet+set,也就是在其余属性的set方法内,将本属性set。 可是didSet+set有时候彻底没有必要,不符合懒加载的规则,浪费了计算能力,用重写getter的方法好一些。 所以重写getter老是会好一点。
回归正题:在KVC时,想要获取所有的成员变量和属性, 怎么办呢?
首先要了解setValue: forKeyPath:方法的底层实现:以name属性为例
1.首先先去类的方法列表去寻找有木有setName:,若是有,就直接调用[person setName:value]
2.找找有没有带下划线的成员变量_name,若是有 _name = value;
3.找有没有成员变量name,若是有 name = value;
4.若是都没有找到,就直接报错。
所以对于readonly的又重写了getter的属性而言:若是对propertyList的属性一次使用kvc,就会报错,所以为保证代码正常,不能使用propertyList的属性进行kvc;
另外:这种属性原本就是计算型的了,为何还有为它赋值呢,所以对它进行kvc也不合情理。
当使用ivaList时,直接就没法获取到这种属性,所以是kvc的最佳方案。再者,使用propertyList没法获取成员变量(_height),没法对成员变量进行赋值。而使用ivaList是能够将该赋值的成员变量都获取的。
<span以上就是使用ivar仍是使用property进行kvc的论证。
话题外: 不少类 有些成员变量 既没有暴露给外部调用的getter又没有setter,只是用@private声明了一下:为何??
猜想是:是方法调用时使用的中间变量,由于是跟随对象产生,不适合使用静态static,又由于外部不会使用,因此不必给外部提供接口,可是可能有好几个方法都须要这个量,不适合作局部变量,因此就这样定义了。
对于这种状况,要想不对这种成员变量赋值,在KVC时又能够这样改进一下,经过ivarList获取,去掉propertyList中没有的成员变量,这样就过滤掉了上面的那种成员变量了。this
获取属性\成员列表一个重要的应用就是,一次取出模型中的属性\成员变量,根据它的名字获取字典中的key而后取出字典中这个key对应的value,使用setValue: forKeyPath:方法设置值。为何要这样,而再也不使用方法setValuesForKeysWithDictionary:。由于在setValuesForKeysWithDictionary:方法内部会执行这样一个过程
遍历字典里面的全部key,一个一个取出来,遍历每一个key按照如下过程
1.取出key,
2.取出key的value,即dict[key],直接给模型的属性\成员变量赋值
3.怎么给模型的属性赋值,使用方法setValue:value forKeyPath:key进行赋值,这个方法的执行过程在前面已经提到。
所以,开发中常常遇到的字典中的key比模型中多时,会出现的 this class is not key-value compliant for ‘xxx’这个bug就很好解释了,一般是由于字典中的key,比模型中的属性\成员变量多。那么当模型中的属性比字典中多时,使用setValuesForKeysWithDictionary:会不不会有bug呢?经测试:当多出来的属性是对象数据类型时,为null,当属性是基本数据类型时,会有一个系统默认值(如int为0)。
所以使用逐一为属性赋值的方法进行KVC:编码
Class clazz = Person.class; unsigned int count = 0; Person *person = [[Person alloc]init]; NSDictionary *dict = @{@"name":@"zhangsan",@"age":@19, @"height": @1.75}; Ivar *ivars = class_copyIvarList(clazz, &count); // NSLog(@"%tu", count); // 3 for (int i = 0; i < count; i++) { const char *cname = ivar_getName(ivars[i]); NSString *name = [NSString stringWithUTF8String:cname]; NSString *key = [name substringFromIndex:1]; // 去掉'_' [person setValue:dict[key] forKey:key]; } NSLog(@"%@", person); // 已经重写了description方法 输出是: <Person, 0x7ff15b80f230>{ name = zhangsan, height = 1.750000, age = 19}
使用这种方式进行kvc,即便字典中的key多的时候也不会有bug,可是新的问题出现了,若是模型中的属性比字典中的key多便会出现bug并且:若是多的是对象类型不会有bug,该属性的值为null,若是是基本数据类型就会出错could not set nil as the value for the key ‘xxx’。例如,将上面的字典改成:
NSDictionary *dict = @{@"age":@19, @"height": @1.75}; // 去掉了name NSString类型 修改以后输出为:
<Person, 0x7f996263fbd0>{ name = (null), height = 1.750000, age = 19} 若是将字典改成:
NSDictionary *dict = @{@"name":@"zhangsan",@"age":@19}; // 去掉了height float类型 程序直接崩溃。
如何解决上面的bug:能够在setValue:value forKeyPath:key方法调用以前进行以下处理:取出属性对应的类型,若是类型是基本数据类型,value替换为默认值(如int对应默认值为0)。
runtime提供的ivar_getTypeEncoding函数能够获取到属性的类型,返回值表明的含义以下:
height是float类型对应的TypeCode是"f"所以能够进行过滤一下,代码改动以下:
Class clazz = Person.class; unsigned int count = 0; Person *person = [[Person alloc]init]; NSDictionary *dict = @{@"name":@"zhangsan",@"age":@19, @"height": @1.75}; Ivar *ivars = class_copyIvarList(clazz, &count); for (int i = 0; i < count; i++) { const char *cname = ivar_getName(ivars[i]); NSString *name = [NSString stringWithUTF8String:cname]; NSString *key = [name substringFromIndex:1]; const char *coding = ivar_getTypeEncoding(ivars[i]); // 获取类型 NSString *strCode = [NSString stringWithUTF8String:coding]; id value = dict[key]; if ([strCode isEqualToString:@"f"]) {// 判断类型是不是float value = @(0.0); } [person setValue:value forKey:key]; } NSLog(@"%@", person); 这样就能够正常执行了,输出为: <Person, 0x7fc75d004a00>{ name = zhangsan, height = 0.000000, age = 19}
获取属性\成员列表另一个重要的应用就是进行归档和解档,其原理和上面的kvc基本上同样,这里只是展现一些代码:
- (void)encodeWithCoder:(NSCoder *)aCoder { unsigned int count = 0; Ivar *ivars = class_copyIvarList(self.class, &count); for (int i = 0; i < count; i++) { const char *cname = ivar_getName(ivars[i]); NSString *name = [NSString stringWithUTF8String:cname]; NSString *key = [name substringFromIndex:1]; id value = [self valueForKey:key]; // 取出key对应的value [aCoder encodeObject:value forKey:key]; // 编码 } } - (id)initWithCoder:(NSCoder *)aDecoder { if (self = [super init]) { unsigned int count = 0; Ivar *ivars = class_copyIvarList(self.class, &count); for (int i = 0; i < count; i++) { const char *cname = ivar_getName(ivars[i]); NSString *name = [NSString stringWithUTF8String:cname]; NSString *key = [name substringFromIndex:1]; id value = [aDecoder decodeObjectForKey:key]; // 解码 [self setValue:value forKey:key]; // 设置key对应的value } } return self; }
交换方法实现的需求场景:本身建立了一个功能性的方法,在项目中屡次被引用,当项目的需求发生改变时,要使用另外一种功能代替这个功能,要求是不改变旧的项目(也就是不改变原来方法的实现)。
能够在类的分类中,再写一个新的方法(是符合新的需求的),而后交换两个方法的实现。这样,在不改变项目的代码,而只是增长了新的代码 的状况下,就完成了项目的改进。
交换两个方法的实现通常写在类的load方法里面,由于load方法会在程序运行前加载一次,而initialize方法会在类或者子类在 第一次使用的时候调用,当有分类的时候会调用屡次。
// 程序一运行的时候调用 + (void)load { // 若是是类方法,使用的是class_getClassMethod,若是是对象方法使用的是class_getInstanceMethod Method methodOne = class_getInstanceMethod(self, @selector(methodOne:)); Method methodTwo = class_getInstanceMethod(self, @selector(methodTwo:)); // 交换两个方法的实现 method_exchangeImplementations(methodOne, methodTwo); }
注意的是
1.能够交换的两个方法的参数必须是匹配的,参数的类型一致。
2.若是在方法one的内部想要调用方法two,此时在方法one的内部应该用one调用,而其实是在调用two,不然会形成死循环。
例如:
// 交换前 - (NSString *) methodOne:(NSString *)str{ NSLog(@"%@", [self methodTwo:str]); return "suc"; } // 交换后 在方法的实现中,要注意将调用two的地方,换成本身的名字 - (NSString *) methodOne:(NSString *)str{ NSLog(@"%@", [self methodOne:str]); return "suc"; }
任何一个方法都有两个重要的属性:SEL是方法的编号 ,IMP是方法的实现,方法的调用过程实际上去根据SEL去寻找IMP。
在这个例子中,假设在交换以前SEL为methodOne:的方法指向着IMP1,SEL为methodTwo的方法指向着IMP2。
交换实现其实是在底层是交换了方法编号的指向,也就是让methodOne:指向了IMP2,methodTwo指向了IMP1。
应用场景
关联对象不是为类\对象添加属性或者成员变量(由于在设置关联后也没法经过ivarList或者propertyList取得),而是为类添加一个相关的对象,一般用于存储类信息,例如存储类的属性列表数组,为未来字典转模型的方便。 例如,将属性的名称存到数组中设置关联
const char* propertiesKey = "propertiesKey"; unsigned int count = 0; Ivar *ivars = class_copyIvarList([Person class], &count); NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:count]; for (unsigned int i = 0; i < count; ++i) { Ivar pty = ivars[i]; const char *cname = ivar_getName(ivars[i]); NSString *name = [NSString stringWithUTF8String:cname]; NSString *key = [name substringFromIndex:1]; // 去掉_ [arrayM addObject:key]; } free(ivars); objc_setAssociatedObject(self, propertiesKey, arrayM, OBJC_ASSOCIATION_COPY_NONATOMIC); NSLog(@"%@", arrayM); 输出是 ( age, height, name )objc_setAssociatedObject 方法的参数解释: 第一个参数id object, 当前对象 第二个参数const void *key, 关联的key,是c字符串 第三个参数id value, 被关联的对象 第四个参数objc_AssociationPolicy policy关联引用的规则,取值有如下几种: enum { OBJC_ASSOCIATION_ASSIGN = 0, OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, OBJC_ASSOCIATION_COPY_NONATOMIC = 3, OBJC_ASSOCIATION_RETAIN = 01401, OBJC_ASSOCIATION_COPY = 01403 }; 若是想要获取已经关联的对象,经过key取得便可 NSArray *pList = objc_getAssociatedObject(Person, propertiesKey); 能够将以上两种操做封装起来,为Person类增长一个properties类方法,封装上面的操做,用于方便获取类的属性列表。 const char* propertiesKey = "propertiesKey"; @implementation Person + (NSArray *)properties { NSArray *pList = objc_getAssociatedObject(self, propertiesKey); if (pList != nil) { return pList; } unsigned int count = 0; Ivar *ivars = class_copyIvarList([self class], &count); NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:count]; for (unsigned int i = 0; i < count; ++i) { Ivar pty = ivars[i]; const char *cname = ivar_getName(ivars[i]); NSString *name = [NSString stringWithUTF8String:cname]; NSString *key = [name substringFromIndex:1]; [arrayM addObject:key]; } free(ivars); objc_setAssociatedObject(self, propertiesKey, arrayM, OBJC_ASSOCIATION_COPY_NONATOMIC); return arrayM.copy; } @end
每一个类都有都有一下两个类方法(来自NSObject)
+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel
以上两个一个使用于类方法,一个适用于对象方法。在代码中调用没有实现的方法时,也就是sel标识的方法没有实现 都会现调用这两个方法中的一个(若是是类方法就调用第一个,若是是对象方法就调用第二个)拦截。 一般的作法是在resolve的内部指定sel对应的IMP,从而完成方法的动态建立和调用两个过程,也能够不指定IMP打印错误信息后直接返回。
假如在Person类中没有-sayHi这个方法,若是对象p使用[p performSelector:@selector(sayHi) withObject:nil];那么就会必须通过Person类的resolveInstanceMethod:(SEL)sel方法,在这里为-sayHi指定实现。
void abc(id self, SEL _cmd){ NSLog(@"%@说了hello", [self name]); } @implementation Person //动态添加方法:在resolve中添加相应的方法,注意是类方法仍是对象方法。 + (BOOL)resolveInstanceMethod:(SEL)sel { if ([NSStringFromSelector(sel) isEqualToString:@"sayHi"]) { class_addMethod(self, sel, abc, "v@:"); // 为sel指定实现为abc } return YES; } @end
对实现(abc)的前两个参数的说明
每一个方法的内部都默认包含两个参数,被称为隐式参数
id类型self(表明类或对象)和SEL类型的_cmd(方法编号)
class_addMethod函数参数的含义:
第一个参数Class cls, 类型
第二个参数SEL name, 被解析的方法
第三个参数 IMP imp, 指定的实现
第四个参数const char *types,方法的类型,具体参照类型的codeType那张图,可是要注意一点:Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).译为:由于函数必须至少有两个参数self和_cmd,第二个和第三个字符必须是“@:”。若是想要再增长参数,就能够从实现的第三个参数算起,看下面的例子就明白。
返回值:YES if the method was found and added to the receiver, otherwise NO.
为-sayHi方法的实现增长参数
调用时:
Person *p = [[Person alloc] init]; p.name = @"zhangsan"; p.age = 10; [p performSelector:@selector(sayHi:) withObject:@"world"]; // 增长了一个参数,多了冒号 对Person类中的代码作了修改 void abc(id self, SEL _cmd, NSString *content){ // 增长了一个参数content NSLog(@"%@说了hello%@", [self name], content); } @implementation Person //动态添加方法:在resolve中添加相应的方法,注意是类方法仍是对象方法。 + (BOOL)resolveInstanceMethod:(SEL)sel { if ([NSStringFromSelector(sel) isEqualToString:@"sayHi:"]) { class_addMethod(self, sel, abc, "v@:@"); // 增长了一个对象类型参数 增长了@ } return YES; } @end
输出为: zhangsan说了helloworld
动态建立一个类,为这个类添加成员变量和方法,并建立这个类型的对象:
#import "ViewController.h" #import <objc/runtime.h> #import <objc/message.h> #import "Person.h" static void printSchool(id self, SEL _cmd) { NSLog(@"个人学校是%@", [self valueForKey:@"schoolName"]); } @implementation ViewController - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { Class classStudent = objc_allocateClassPair(Person.class, "Student", 0); // 添加一个NSString的变量,第四个参数是对其方式,第五个参数是参数类型 if (class_addIvar(classStudent, "schoolName", sizeof(NSString *), 0, "@")) { NSLog(@"添加成员变量schoolName成功"); } // 为Student类添加方法 "v@:"这种写法见参数类型链接 if (class_addMethod(classStudent, @selector(printSchool), (IMP)printSchool, "v@:")) { NSLog(@"添加方法printSchool:成功"); } // 注册这个类到runtime系统中就可使用他了 objc_registerClassPair(classStudent); // 返回void // 使用建立的类 id student = [[classStudent alloc] init]; NSString *schoolName = @"清华大学"; // 给刚刚添加的变量赋值 // object_setInstanceVariable(student, "schoolName", (void *)&str);在ARC下不容许使用 [student setValue:schoolName forKey:@"schoolName"]; // 调用printSchool方法,也就是给student这个接受者发送printSchool:这个消息 // objc_msgSend(student, "printSchool"); // 我尝试用这种方法调用可是没有成功 [student performSelector:@selector(printSchool) withObject:nil]; // 动态调用未显式在类中声明的方法 } @end 输出的结果是: 添加成员变量schoolName成功 添加方法printSchool成功 个人学校是清华大学