[toc]
一、基本概念
Runtime
是一套比较底层的纯C语言API,包含了很多底层的C语言API。在我们平时编写的OC代码中,程序运行时,其实最终都是转成了Runtime
的C语言代码。Runtime
是开源的,你可以去下载Runtime的源码。
实例方法被调用的过程分析
实例方法被调用的时候,会通过其持有的isa指针找到对应的类,然后在其中的class_data_bits_t中查找对应的方法。
*执行NSArray array = [[NSArray alloc] init];的流程:
- 1、[NSArray alloc]先被执行,由于NSArray没有+alloc方法,所以去分类NSObject中查找
- 2、检查NSArray是否能响应alloc方法,发现响应后,检查NSArray类,开辟NSArray所需的内存空间,然后把isa指向NSArray。同时+alloc方法被添加到cache列表中。
- 3、接着执行-init方法,如果NSArray不响应,则继续去父类NSObject中查找。找到后同时加入到cache列表中。
- 4、以后再使用[[NSArray alloc] init]初始化数组,直接从cache中获取方法执行。
realizeClass方法的主要作用是对类进行第一次初始化(分配可读写数据空间、返回真正的类结构) 类在内存中的位置是编译期确定的,只要代码不改变,类在内存中的位置就会不变
- ObjC 类中的属性、方法还有遵循的协议等信息都保存在
class_rw_t
中 - 当前类在编译期就已经确定的属性、方法以及遵循的协议都保存在
class_ro_t
中 - 类的方法、属性以及协议在编译期间存放到了“错误”的位置,直到
realizeClass
执行之后,才放到了class_rw_t
指向的只读区域class_ro_t
,这样我们即可以在运行时为class_rw_t
添加方法,也不会影响类的只读结构。 - 在
class_ro_t
中的属性在运行期间就不能改变了,再添加方法时,会修改class_rw_t
中的 methods 列表,而不是 class_ro_t 中的 baseMethods
一、相关术语
1、id:表示Objective-C的任意对象类型
struct objc_object { Class isa;} *id;复制代码
2、isa:实例的一个属性,用来指向实例所属的类
typedef struct objc_class *Class;struct objc_class { Class isa OBJC_ISA_AVAILABILITY; }复制代码
- 实例方法调用时,通过对象的 isa 在类中获取方法的实现
- 类方法调用时,通过类的 isa 在元类中获取方法的实现
3、SEL:SEL又叫选择器,是表示一个方法的selector的指针
typedef struct objc_selector *SEL;复制代码
4、Method:方法(方法名+方法类型+方法实现)
typedef struct objc_method *Method;struct objc_method { SEL method_name OBJC2_UNAVAILABLE; // 方法名 char *method_types OBJC2_UNAVAILABLE; // 方法类型,主要存储着方法的参数类型和返回值类型 IMP method_imp OBJC2_UNAVAILABLE; // 方法的实现,函数指针}复制代码
class_copyMethodList(Class cls, unsigned int *outCount)
可以使用这个方法获取某个类的成员方法列表。
5、IMP:函数指针,指向方法的实现,由编译器生成,决定代码最终在何处执行。
typedef id (*IMP)(id, SEL, ...);复制代码
6、Ivar:实例变量
typedef struct objc_ivar *Ivar;struct objc_ivar { char *ivar_name OBJC2_UNAVAILABLE; char *ivar_type OBJC2_UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE;#endif}复制代码
class_copyIvarList(Class cls, unsigned int *outCount)
可以使用这个方法获取某个类的成员变量列表。
// ivar 的修饰信息存放在了 Class 的 Ivar Layout 中struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize;#ifdef __LP64__ uint32_t reserved;#endif const uint8_t * ivarLayout; // <- 记录了哪些是 strong 的 ivar const char * name; const method_list_t * baseMethods; const protocol_list_t * baseProtocols; const ivar_list_t * ivars; const uint8_t * weakIvarLayout; // <- 记录了哪些是 weak 的 ivar const property_list_t *baseProperties;};复制代码
7、objc_property_t:实例属性 = Ivar + setter + getter
typedef struct objc_property *objc_property_t;复制代码
class_copyPropertyList(Class cls, unsigned int *outCount)
可以使用这个方法获取某个类的属性列表。
8、objc_category
typedef struct objc_category *Category;typedef struct objc_category { const char *name; // 类的名字 classref_t cls; // 类 struct method_list_t *instanceMethods; // category中所有给类添加的实例方法的列表 struct method_list_t *classMethods; // category中所有添加的类方法的列表 struct protocol_list_t *protocols; // category实现的所有协议的列表 struct property_list_t *instanceProperties; // category中添加的所有属性};复制代码
9、Cache:缓存提高查找效率
typedef struct objc_cache *Cachestruct objc_cache { unsigned int mask OBJC2_UNAVAILABLE; unsigned int occupied OBJC2_UNAVAILABLE; Method buckets[1] OBJC2_UNAVAILABLE;};复制代码
每调用一次方法后,不会直接在isa指向的类的方法列表(methodLists)中遍历查找能够响应消息的方法,因为这样效率太低。它会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从isa指向的类的方法列表(methodLists)中查找方法。提高效率。
10、metaClass(元类):类对象所属的类,类对象的isa指针指向元类
11、根元类:所有的元类的基类,根元类的isa指针指向自己
二、所有实例、类以及元类(meta class)都继承自一个基类,关系如下图所示:
上图中:superclass指针代表继承关系,isa指针代表实例所属的类。 类也是一个对象,它是另外一个类的实例,这个就是“元类”,元类里面保存了类方法的列表,类里面保存了实例方法的列表。实例对象的isa指向类,类对象的isa指向元类,元类对象的isa指针指向一个“根元类”(root metaclass)。所有子类的元类都继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。 注意:- 1、
Class
是一个指向objc_class
结构体的指针,而id
是一个指向objc_object
结构体的指针,其中的isa
是一个指向objc_class
结构体的指针。其中的id就是我们所说的对象,Class
就是我们所说的类。 - 2、
isa
指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用isKindOfClass
:方法来确定实例对象的类。因为KVO
的实现机制就是将被观察对象的isa指针指向一个中间类而不是真实的类。
三、类对象在runtime中的数据结构
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; // 类的版本信息,默认为0 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;复制代码
- 1、isa: 结构体的首个变量也是isa指针,这说明Class本身也是Objective-C中的对象。
- 2、super_class: 结构体里还有个变量是super_class,它定义了本类的超类。类对象所属类型(isa指针所指向的类型)是另外一个类,叫做“元类”。
- 3、name:类名称。
- 4、version:类的版本信息。
- 5、info:运行期使用的标志位,比如0x1(CLS_CLASS)表示该类为普通class,0x2(CLS_META)表示该类为 metaclass。
- 6、instance_size:实例大小,即内存所占空间。
- 7、ivars: 成员变量列表,类的成员变量都在ivars里面。
- 8、methodLists: 方法列表,根据标志位的不同可能指向不同,比如可能指向实例方法列表,或者指向类方法列表。类的实例方法都在methodLists里,类方法在元类的methodLists里面。methodLists是一个指针的指针,通过修改该指针指向指针的值,就可以动态的为某一个类添加成员方法。这也就是Category实现的原理,同时也说明了Category只可以为对象添加成员方法,不能添加成员变量。
- 9、cache: 方法缓存列表,objc_msgSend(下文详解)每调用一次方法后,就会把该方法缓存到cache列表中,下次调用的时候,会优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。提高效率。
- 10、protocols:类需要遵守的协议。
四、运行时创建类,只需要三步:
- 1、为类和类的元类分配内存空间
objc_allocateClassPair
- 2、为类添加方法和成员属性
class_addMethod
、class_addIvar
、class_addProperty
- 3、注册新创建的类
objc_registerClassPair
注意:运行时只能添加属性,不能添加成员变量,否则会打乱类的内存结构。
参考博客
二、动态特性
[TOC]
@(runtime)[runTime, 温故而知新]
转自:
1.什么是动态特性?
程序可以访问,检测和修改它本身状态或行为的能力。用我自己的理解,这里的状态和行为,理解成变量,属性和方法,会更加形象一点。
2.与动态特性相关的概念,selector,IMP,Class
Class: 从语法形式上看,和UIButton,NSString一样,是一种类型。
Class被定义为一个指向objc_class的结构体指针。
它是指向对象的类结构体的指针,该类结构体含有一个指向其父类类结构的指针,访类方法的链表,该类方法的缓存以及其他必要信息。见下图
除了静态方法来创建对象,还可以使用string来创建,NSClassFromString。
SEL:定义成一个指向objc_selector指针
运行时,会在方法链表中根据SEL查找具体的实现方法IMP。为什么不用函数指针直接调用,而加了一层SEL?我的理解,首先Object-C的类不能直接应用函数指针,这样只能做一个@selector语法来取(本人在OC中写过状态机,用函数指针形式写action,但一直报错,只能用selector代替);其次,SEL还可以配合动态方法来使用,例如NSSelectorFromString,performSelector,动态添加方法,并执行。
IMP:就是定义一个函数指针的形式
它包含一个接受消息的对象(self指针),调用方法SEL,以及若干参数,并返回一个id。
3. 举例子,如何将JSON直接映射成对象,如何将对象直接映射成DB(coreData原理)
3.1定义该类的属性,方法,生成对象。
用动态方法,获得该对象的属性/变量列表(class_copyPropertyList
/class_copyIvarList
),遍历获得每个属性的名称(property_getName),然后将JSON转换Dic,用key-value(setvalueForkey,valueForKey)方法,对对象进行赋值,取值操作。
此种方法,抽象出了公用的setter方法(用dictionary給对象赋值),但是缺点是,类型要事先定义。无法动态生成类型。这种例子,网上很多,而且不明白为什么例子中都把property name和attribute值打印出来,至于怎么用,半个字都没提?
(上面是最长见的使用方式,有人问我能否不事先定义类型,然后利用JSON来创建类型呢?这个还把我问住了)后来查阅OC runtime guide,发现有动态添加变量的方法(class_addIvar
),于是思路由此打开:
3.2、首先定义一个空的类
(没有属性,变量,方法),只有一个类名,然后运行时,給该类添加变量(当时没有查到可以动态添加属性的方法,后来发现有,但是要到iOS4.3以后才行),随后用給变量赋值。但是结果让人失望,无法动态添加变量。原因是class_addIvar只能在动态创建类型的时候,添加变量,也就是“class_addIvar"This function may only be called after objc_allocateClassPair and beforeobjc_registerClassPair.Adding an instance variable to an existing class is notsupported”,而事先定义类是静态创建的类,故无法在runtime时添加变量
于是,只能放弃事先定义类的方式,转而利用在动态创建类时(objc_allocateClassPair
),添加变量 。然后用給变量赋值和取值的方式(object_setInstanceVariable
,object_getIvar
,注意,无法用key-value的方式操作,这种方法只有静态定义属性后才行),但这种方式,就只能用纯C的方式封装,赋值,取值都要传进obj参数,比较繁琐,没有面向对象那么方便。
结论:3.2中的结论,如果编译前定义类,那么无法用runtime添加变量,这种方法行不通;只有在runtime时,在objc_allocateClassPair
和objc_registerClassPair
之间用class_addIvar
添加变量
3.3、后来查到有动态添加property的方法
(class_addProperty
),在4.3之后。于是想到一种动态创建类型,并且可以用OC语法的方式访问变量。
首先,动态创建类型,添加变量(这个很重要,因为当我们访问property时,实际上是要对变量操作,如果没有添加变量,那么就是null),注册类型,然后往里动态添加属性,随后就可以象OC一样方便访问属性了 (因为静态类中属性会默认有一个和它同名的变量,对属性操作,实际上是对该变量操作)。
但实际上对该属性赋值后,取值却是null。因为只有在编译前定义的属性才会默认一个变量,property实际上只是提供了setter和getter的方法,至于你要把值存贮在哪里,需要自己设定,所以还需要在class_addProperty
方法后,添加property的setter,getter,并在其中确定需要把值保存到哪里,从哪里取值。
3.4、使用动态创建类,对象,以及ORM的优点,缺点
这个例子有如下几个特点:1.可以动态生成类型 2.可以用OC的方式访问属性。纯粹的“动态”。
当然也有美中不足的地方,首先动态创建对象的类型都是id类型(因为是动态创建,事先没有定义具体类型),视觉上不直观。其次编译过程中,会报warning,因为property是动态添加的,不是编译之前确定的,所以编译器不知道setter,getter方法哪里来的。(当然可以用performSelector来调用就没有warning问题,但是调用方式太繁琐)
但是不影响使用。
结果
结论:3.3的方法比3.2,3.1的方法牛逼,直接动态创建类型和对象,但是牺牲的是code的可读性和可维护性,研究的意义大于实用意义。
注意:这里需要大家研究的是,如何通过JSON的值,确定动态添加的变量和property的类型,我的思路是,可以容易区分NSString和NSNumber,但是如果确定int,long,float, long long等类型?应该可以通过值的大小范围来确定,例如int -256~255
3.5、如何将对象映射进DB中,其实原理是一样的,可以运行时,获得类名,属性名,属性类型,值,然后用sqlite3的接口创建表,列,值,类型等等。其实Coredata也是运用了这个动态的原理来实现的。
三、 动态添加属性
@property 和 Ivar 的区别:@property = Ivar + setter + getter
第一种:通过runtime动态关联对象
相关函数
objc_setAssociatedObject
、objc_getAssociatedObject
、objc_removeAssociatedObjects
,下面的代码通过给UIButton添加一个分类的方式关联两个属性clickInterval
、clickTime
,来实现按钮的防连点操作。
// .h文件#import@interface UIButton (FixMultiClick)@property (nonatomic, assign) NSTimeInterval clickInterval;@end复制代码
// .m文件#import "UIButton+FixMultiClick.h"#import#import @interface UIButton ()@property (nonatomic, assign) NSTimeInterval clickTime;@end@implementation UIButton (FixMultiClick)-(NSTimeInterval)clickTime { return [objc_getAssociatedObject(self, _cmd) doubleValue];}-(void)setClickTime:(NSTimeInterval)clickTime { objc_setAssociatedObject(self, @selector(clickTime), @(clickTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);}-(NSTimeInterval)clickInterval { return [objc_getAssociatedObject(self, _cmd) doubleValue];}-(void)setClickInterval:(NSTimeInterval)clickInterval { objc_setAssociatedObject(self, @selector(clickInterval), @(clickInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);}+(void)load { [UIButton aspect_hookSelector:@selector(sendAction:to:forEvent:) withOptions:AspectPositionInstead usingBlock:^(id info){ UIButton *obj = info.instance; if(obj.clickInterval <= 0){ [info.originalInvocation invoke]; } else{ if ([NSDate date].timeIntervalSince1970 - obj.clickTime < obj.clickInterval) { return; } obj.clickTime = [NSDate date].timeIntervalSince1970; [info.originalInvocation invoke]; } } error:nil];}@end复制代码
优点:
可以快速为一个已有的class
添加一个动态属性或者block块
缺点:
不能遍历所有的关联对象列表,不能移除指定的关联对象,只能通过objc_removeAssociatedObjects
一次移除所有的关联对象。
第二种:通过runtime动态创建类的时候添加Ivar
相关函数objc_alloctateClassPair
、class_addIvar
、objc_registerClassPaire
// 一:为Class分配内存空间Class myClass = objc_allocateClassPair([NSObject class], "myClass", 0);// 二:添加方法class_addMethod(myClass, @selector(method), (IMP)myMethod, "v@:");// 三:注册Classobjc_registerClassPair(myClass);// 创建对象调用方法id obj = [[myClass alloc] init];[obj performSelector:@selector(method)];复制代码
优点:
动态添加Ivar我们能够通过遍历Ivar得到我们所添加的属性
缺点:
必须通过class_allocatePair动态创建一个class,才能调用class_addIvar创建Ivar,最后通过class_registClassPair注册class。不能为已存在的类添加Ivar,否则会涉及到OC中类的成员变量的偏移量问题,如果在类注册之后class_adddIvar的话会破坏原来类成员变量的正确偏移量,这样的话会导致你访问的成员变量并不是你想访问的成员变量(用KVC赋值和取值直接报错, 用getIvar的话取值为null),如图:
第三种:通过runtime动态添加property
相关函数class_addProperty
、class_addMethod
、objc_getAssociatedObject
、objc_getAssociatedObject
仅仅添加属性是没什么用的,因为还需要添加属性对应的实例变量。虽然runtime提供了class_addIvar方法来给类添加实例变量,但是注意,该方法只能在创建新的类的时候才能使用;对于已经存在的类,是不允许添加实例变量的。鉴于上述原因,所以可以采用动态添加关联对象来存储属性对应的实例变量。实现策略如下:
- 1、由于我们肯定会在interface 中提供生的property(由于没有合成实现与ivar,在此称为生的),所以这样对于在外部访问时和普通property相同。
- 2、由于缺乏的是实现以及可以存取的数据量,这里我们可以直接实现这些set与get。
- 3、set与get的实现可以通过 associatedObject 进行对对象的存取操作。
#import "RuntimeTest.h"#import@interface RuntimeTest(){ NSString* _address;}@end@implementation RuntimeTest+(void)load { [self runtimeTest];}void myMethod(id self, SEL _cmd) { NSLog(@"self = %@", self); NSLog(@"self.name = %@", [self valueForKey:NSStringFromSelector(@selector(name))]); NSLog(@"self.addres = %@", [self valueForKey:NSStringFromSelector(@selector(addres))]);}NSString *nameGetter(id self, SEL _cmd) { NSString* result = objc_getAssociatedObject(self, _cmd); return result;}void nameSetter(id self, SEL _cmd, NSString *value) { NSString *propertyStr = NSStringFromSelector(_cmd); // 去掉 set NSString *realProperty = [propertyStr substringFromIndex:3]; // 去掉 : realProperty = [realProperty substringToIndex:realProperty.length - 1]; // 首字母小写 realProperty = [realProperty lowercaseString]; // 关联对象 objc_setAssociatedObject(self, NSSelectorFromString(realProperty), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);} + (void) runtimeTest { // 1、Class分配内存空间 Class myClass = objc_allocateClassPair([NSObject class], "myClass", 0); // 2.1、添加方法 class_addMethod(myClass, @selector(method), (IMP)myMethod, "v@:"); // 2.2、添加变量(ivar) class_addIvar(myClass, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*)); // 三:注册Class objc_registerClassPair(myClass); // 2.3、添加属性(property)(可以在类的注册完成之后) NSString* propertyName = @"addres"; objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type objc_property_attribute_t ownership0 = { "C", "" }; // C = copy objc_property_attribute_t ownership = { "N", "" }; // N = nonatomic objc_property_attribute_t backingivar = { "V", [[NSString stringWithFormat:@"_%@", propertyName] UTF8String] }; //variable name objc_property_attribute_t attrs[] = { type, ownership0, ownership, backingivar }; if (class_addProperty(myClass, [propertyName UTF8String], attrs, sizeof(attrs)/sizeof(objc_property_attribute_t))) { //添加get和set方法 NSString *setFunc = [NSString stringWithFormat:@"set%@:",[propertyName capitalizedString]]; class_addMethod(myClass, NSSelectorFromString(propertyName), (IMP)nameGetter, "@@:"); class_addMethod(myClass, NSSelectorFromString(setFunc), (IMP)nameSetter, "v@:@"); } // 创建对象调用方法 id obj = [[myClass alloc] init]; [obj setValue:@"xiaoMing" forKey:NSStringFromSelector(@selector(name))]; [obj setValue:@"宇宙1" forKey:NSStringFromSelector(@selector(addres))]; NSLog(@"addres1 = : %@", [obj valueForKey:NSStringFromSelector(@selector(addres))]); [obj setValue:@"宇宙2" forKey:@"addres"]; NSLog(@"addres1 = : %@", [obj valueForKey:@"addres"]); [obj performSelector:@selector(method)];}@end复制代码
优点:
能都在已有的类中添加property,并且能能够遍历到动态添加的属性。这种操作由于提供了生的property,所以在第三方的json转model库遍历property时可以直接遍历到,由于手动实现了set和get方法,所以在遍历后的KVC赋值时也能起到作用,保证了和普通成员变量操作的一致性。
缺点:
比较麻烦class_addProperty只是声明了get和set方法(缺少实现和Ivar),get和set方法需要自己实现,值也需要自己存储(可以使用关联对象或者存储到已存在的ivar上)。
第四种:通过setValue:forUndefinedKey:动态添加键值
这种方法类似于property,需要重写setValue:forUndefinedKey
和valueForUndefinedKey:
,存值方式也一样,需要借助一个其他对象。由于这种方式没有借助于runtime,所以也比较容易理解。
参考资料:
四、 KVC分析
Key-Value Coding(KVC)实现分析
[obj setValue:@"张三" forKey:@"name"];// =======================================// 就会被编译器处理成:// =======================================SEL sel = sel_get_uid ("setValue:forKey:");IMP method = objc_msg_lookup (obj->isa, sel);method(obj, sel, @"张三", @"name");复制代码
KVC运用了isa_swizzling(类型混合指针机制)技术,来实现其内部查找定位的。
- (void)setValue:(id)value forKey:(NSString *)key;复制代码
- ① 首先搜索 setter 方法,有就直接赋值。
- ② 如果上面的 setter 方法没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
-
- 返回 NO,则执行setValue:forUNdefinedKey:
-
- 返回 YES,则按_key,_isKey,key,isKey的顺序搜索成员名。
- ③ 还没有找到的话,就调用setValue:forUndefinedKey。这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。
- (id)valueForKey:(NSString *)key;复制代码
- ① 首先查找 getter 方法,找到直接调用。如果是 bool、int、float 等基本数据类型,会做 NSNumber 的转换。
- ② 如果没查到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
-
- 返回 NO,则执行valueForUNdefinedKey:
-
- 返回 YES,则按_key,_isKey,key,isKey的顺序搜索成员名。
- ③ 还没有找到的话,调用valueForUndefinedKey:
KVC 主要方法
设置值
// value的值为OC对象,如果是基本数据类型要包装成NSNumber- (void)setValue:(id)value forKey:(NSString *)key;// keyPath键路径,类型为xx.xx- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;// 它的默认实现是抛出异常,可以重写这个函数做错误处理。- (void)setValue:(id)value forUndefinedKey:(NSString *)key;复制代码
获取值
- (id)valueForKey:(NSString *)key;- (id)valueForKeyPath:(NSString *)keyPath;// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常- (id)valueForUndefinedKey:(NSString *)key;复制代码
NSKeyValueCoding 类别中还有其他的一些方法
// 允许直接访问实例变量,默认返回YES。如果某个类重写了这个方法,且返回NO,则KVC不可以访问该类。+ (BOOL)accessInstanceVariablesDirectly;// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;// 如果你在setValue方法时面给Value传nil,则会调用这个方法- (void)setNilValueForKey:(NSString *)key;// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;// KVC提供属性值确认的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。- (BOOL)validateValue:(id)ioValue forKey:(NSString *)inKey error:(NSError)outError;复制代码
实用技巧
//JSON数据://{// "username": "lxz",// "age": 25,// "id": 100//}@interface User : NSObject@property (nonatomic, copy) NSString *name;@property (nonatomic, assign) NSString age;@property (nonatomic, assign) NSInteger userId;@end@implementation User- (void)setValue:(id)value forUndefinedKey:(NSString *)key { if ([key isEqualToString:@"id"]) { self.userId = [value integerValue]; }}@end复制代码
赋值时会遇到一些问题,例如服务器会返回一个id字段,但是对于客户端来说id是系统保留字段,可以重写setValue:forUndefinedKey:方法并在内部处理id参数的赋值。
转换时需要服务器数据和类定义匹配,字段数量和字段名都应该匹配。如果User比服务器数据多,则服务器没传的字段为空。如果服务端传递的数据User中没有定义,则会导致崩溃。
在KVC进行属性赋值时,内部会对基础数据类型做处理,不需要手动做NSNumber的转换。需要注意的是,NSArray和NSDictionary等集合对象,value都不能是nil,否则会导致Crash。
异常处理
- (void)setNilValueForKey:(NSString *)key { if ([key isEqualToString:@"name"]) { [self setValue:@"" forKey:@”age”]; } else { [super setNilValueForKey:key]; }}复制代码
当通过KVC给某个非对象的属性赋值为nil时,此时KVC会调用属性所属对象的setNilValueForKey:方法,并抛出NSInvalidArgumentException的异常,并使应用程序Crash。
我们可以通过重写下面方法,在发生这种异常时进行处理。例如给name赋值为nil的时候,就可以重写setNilValueForKey:方法并表示name是空的。
应用场景:
1、访问修改私有变量(替换系统自带的导航栏,tabBar,替换UIPageControl的image等等)
[pageControler setValue:[UIImage imageNamed:@"line"] forKeyPath:@"pageImage"];[pageControler setValue:[UIImage imageNamed:@"current"] forKeyPath:@"currentPageImage"];复制代码
2、valueForKeyPath的使用更加广泛,功能也更加强大
1、对数组求和、平均值、最大值、最小值。
NSArray *array = @[@1, @3, @5, @7, @9,@11, @13];NSInteger sumPath = [[array valueForKeyPath:@"@sum.floatValue"] integerValue];NSInteger avgPath = [[array valueForKeyPath:@"@avg.floatValue"] integerValue];NSInteger maxPath = [[array valueForKeyPath:@"@max.floatValue"] integerValue];NSInteger minPath = [[array valueForKeyPath:@"@min.floatValue"] integerValue];NSLog(@"sum = %ld, avg = %ld, max = %ld, min = %ld",(long)sumPath, (long)avgPath, (long)maxPath, (long)minPath);// 上述例子经验证是可取的,但下面的写法不可取(将引起崩溃)NSInteger sum = [[array valueForKey:@"@sum.floatValue"] integerValue];NSInteger avg = [[array valueForKey:@"@avg.floatValue"] integerValue];NSInteger max = [[array valueForKey:@"@max.floatValue"] integerValue];NSInteger min = [[array valueForKey:@"@min.floatValue"] integerValue];NSLog(@"sum = %ld, avg = %ld, max = %ld, min = %ld",(long)sum, (long)avg, (long)max, (long)min);复制代码
2、删除数组中重复的数据
NSArray *array = @[@1, @3, @5, @7, @9, @11, @13, @7, @9,@11];NSLog(@"deleteKeyPath = %@",[array valueForKeyPath:@"@distinctUnionOfObjects.self"]);// 下述写法不可取,会引起崩溃NSLog(@"deleteKey = %@",[array valueForKey:@"@distinctUnionOfObjects.self"]);复制代码
3、深层次取出字典中的属性
NSDictionary *dic = @{@"dic1":@{@"dic2":@{@"name":@"zhangsanfeng",@"info":@{@"age":@"13"}}}};NSLog(@"KeyPath = %@",[dic valueForKeyPath:@"dic1.dic2.info.age"]); // 可以深层次的取到子层级属性NSLog(@"Key = %@",[dic valueForKey:@"dic1.dic2.info.age"]); // 无法深层次取到子层级属性复制代码
参考资料:
五、IMP
method_t的结构
struct method_t { SEL name; // 方法名(一个类里面可能有多个name相同的method,比如分类中重写的方法) const char *types; // 存储着方法的参数类型和返回值类型的描述字串 IMP imp; // 方法的函数指针(方法实现,相同的name可能对应不同的实现)};复制代码
IMP与SEL的区别?
- IMP 它是一个指向方法实现的指针,每一个方法都一个对应的IMP指针
- SEL 是方法的名字,不同的方法可能名字(SEL)相同,实现(IMP)不同。比如 category中重写了类方法,则可能出现不同的method对应相同的名字(SEL),但是实现(IMP)不同
怎么获取IMP
1、根据method获取IMP(唯一)
// Method method = class_getInstanceMethod([self class], sel);Method method = someMethod;IMP imp = method_getImplementation(method);复制代码
2、根据SEL获取IMP(可能不是想要的mehtod的IMP)
// 第一种:methodForSelector(SEL) (内部是用 class_getMethodImplementation 实现)SEL sel = @selector(myFunc);IMP imp = [self methodForSelector:sel];// 第二种:class_getMethodImplementation(Class, SEL)SEL sel = @selector(myFunc);IMP imp = class_getMethodImplementation(self, sel);复制代码
执行一个selector的几种方法
SEL sel = @selector(someFunc:);复制代码
1、使用objc_msgSend(接受者+选择器+参数)
#if !OBJC_OLD_DISPATCH_PROTOTYPESOBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )#elseOBJC_EXPORT id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)#endif复制代码
2、使用performSelector
(尽量不使用)
performSelector系列方法在内存管理上容易有缺失,它无法确定将要执行的选择子是什么,因而ARC编译器也无法插入适当的内存管理方法,这是一个大坑,使用GCD则不存在这个问题。
// 没有参数- (id)performSelector:(SEL)aSelector;// 传递一个参数- (id)performSelector:(SEL)aSelector withObject:(id)object;// 传递两个参数- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;复制代码
3、直接调用IMP(相当于C语言函数指针)
// 不同的返回值使用不同的宏,否则会报EXC_BAD_ACCESS错误typedef id (*_IMP) (id, SEL, ...);typedef int (*_INT_IMP) (id, SEL, ...);typedef bool (*_BOOL_IMP) (id, SEL, ...);typedef void (*_VOID_IMP) (id, SEL, ...);Method mthod = class_getInstanceMethod([Obj class], sel);_IMP imp = (_IMP)method_getImplementation(mthod);imp(Obj, sel, 参数列表)复制代码
4、通过NSInvocation调用
NSMethodSignature * methodSignature = [[myObj class] instanceMethodSignatureForSelector:@selector(myFunc)];NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:methodSignature];[invocation setTarget:myObj];[invocation setSelector:@selector(myFunc)];NSString *a=@"111";int b=2;[invocation setArgument:&a atIndex:2];[invocation setArgument:&b atIndex:3];[invocation retainArguments];[invocation invoke];复制代码
objc_msgSend的方法实现的伪代码
id objc_msgSend(id self, SEL op, ...) { if (!self) return nil; // 关键代码(a) IMP imp = class_getMethodImplementation(self->isa, SEL op); imp(self, op, ...); // 调用这个函数,伪代码...}// 查找IMPIMP class_getMethodImplementation(Class cls, SEL sel) { if (!cls || !sel) return nil; IMP imp = lookUpImpOrNil(cls, sel); if (!imp) { ... // 执行动态绑定 } IMP imp = lookUpImpOrNil(cls, sel); if (!imp) return _objc_msgForward; // 这个是用于消息转发的 return imp;}// 遍历继承链,查找IMPIMP lookUpImpOrNil(Class cls, SEL sel) { if (!cls->initialize()) { _class_initialize(cls); } Class curClass = cls; IMP imp = nil; do { // 先查缓存,缓存没有时重建,仍旧没有则向父类查询 if (!curClass) break; if (!curClass->cache) fill_cache(cls, curClass); imp = cache_getImp(curClass, sel); if (imp) break; } while (curClass = curClass->superclass); // 关键代码(b) return imp;}复制代码
IMP实战
一:如果分类中重写了类的方法,找到原有方法,并且执行获取结果
/** 如果分类中重写了类的方法,找到原有方法,并且执行获取结果 @param aString 需要比较的NSString @return YES or NO */-(BOOL)excuteoRiginalIsEqualToString:(NSString*)aString { unsigned int count; Method originalMethod = {0}; // 获取类的所有方法列表,根据SEL匹配,可能找到多个method,最后一个即原有method Method *methods = class_copyMethodList([self class], &count); for (int i = 0; i < count; i++) { const char* funcName = sel_getName(method_getName(methods[i])); if ( 0 == strcmp(funcName, "isEqualToString:") ) { // category中的方法在方法列表中的下标小,最后一个为原来的方法 originalMethod = methods[i]; } } _BOOL_IMP imp = (_BOOL_IMP)method_getImplementation(originalMethod); BOOL res = NO; if (imp) { res = imp(self, method_getName(originalMethod), aString); } free(methods); return res;}复制代码
二:当每个Controller执行完ViewDidLoad以后就在控制台把自己的名字打印出来,方便去做调试或者了解项目结构
#import "UIViewController+viewDidLoad.h"#import@implementation UIViewController (viewDidLoad)+ (void)load{ //保证交换方法只执行一次 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ //获取原始方法 Method viewDidLoad = class_getInstanceMethod(self, @selector(viewDidLoad)); //获取方法实现 _VIMP viewDidLoad_IMP = (_VIMP)method_getImplementation(viewDidLoad); //重新设置方法实现 method_setImplementation(viewDidLoad,imp_implementationWithBlock(^(id target,SEL action){ viewDidLoad_IMP(target,@selector(viewDidLoad)); //自定义代码 NSLog(@"%@ did load",target); })); });}复制代码
参考博客
六、KVO分析
一次简单的KVO操作
@interface Man : NSObject@property (nonatomic, assign) NSInteger p_mustacheLength;// 直接修改成员变量- (void)set_P_mustacheLength:(NSInteger)p_mustacheLength;// 手动触发- (void)set_P_mustacheLength_manual:(NSInteger)p_mustacheLength;@end@implementation Man// 直接修改成员变量- (void)set_P_mustacheLength:(NSInteger)p_mustacheLength { _p_mustacheLength = p_mustacheLength;}// 手动触发- (void)set_P_mustacheLength_manual:(NSInteger)p_mustacheLength { [self willChangeValueForKey:@"p_mustacheLength"]; _p_mustacheLength = p_mustacheLength; [self didChangeValueForKey:@"p_mustacheLength"];}// 重写set方法- (void)setP_mustacheLength:(NSInteger)p_mustacheLength { _p_mustacheLength = p_mustacheLength;}// 是否自动对属性p_mustacheLength触发KVO+(BOOL)automaticallyNotifiesObserversOfP_mustacheLength { // 默认返回YES return YES;}@end@interface KVOVC : UIViewController@end// 同一个属性观察了多次,用来区分是哪一次观察操作// const*:不能改变内容// const:不能改变地址char const* const context_p_man_p_mustacheLength_1 = "context_p_man_p_mustacheLength_1";char const* const context_p_man_p_mustacheLength_2 = "context_p_man_p_mustacheLength_2";@interface KVOVC ()@property (nonatomic, strong) Man* p_man;@end@implementation KVOVC- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context { NSLog(@"-----------------------------------------"); NSLog(@"keyPath = %@", keyPath); NSLog(@"object = %@", object); NSLog(@"change = %@", change); NSLog(@"context = %s", context);}- (void)viewDidLoad { [super viewDidLoad]; self.p_man = [[Man alloc] init]; [self.p_man addObserver:self forKeyPath:NSStringFromSelector(@selector(p_mustacheLength)) options:NSKeyValueObservingOptionNew context:(void*)context_p_man_p_mustacheLength_1]; [self.p_man addObserver:self forKeyPath:NSStringFromSelector(@selector(p_mustacheLength)) options:NSKeyValueObservingOptionNew context:(void*)context_p_man_p_mustacheLength_2]; // (触发)set方法可以触发(无论是否重写Man的p_mustacheLengthset方法。因为此时p_man的isa = NSKVONotifying_Man而不是Man 查看链接) self.p_man.p_mustacheLength = 10; // (触发)kvc可以触发(kvc首先查找调用的也是set方法 查看链接) [self.p_man setValue:@20 forKey:NSStringFromSelector(@selector(p_mustacheLength))]; // (不能触发)直接修改成员变量不能触发(没有走set方法) [self.p_man set_P_mustacheLength:30]; // (触发)手动触发 [self.p_man set_P_mustacheLength_manual:40];}- (void)dealloc { NSLog(@"%@-%s-%d", NSStringFromClass([self class]), __func__, __LINE__); [self.p_man removeObserver:self forKeyPath:NSStringFromSelector(@selector(p_mustacheLength)) context:(void*)context_p_man_p_mustacheLength_1]; [self.p_man removeObserver:self forKeyPath:NSStringFromSelector(@selector(p_mustacheLength)) context:(void*)context_p_man_p_mustacheLength_2];}@end复制代码
官方KVO文档:
自动键值观察是使用isa- swizzle
技术实现的。顾名思义,isa
指针指向维护分派表的对象的类。这个分派表本质上包含指向类实现的方法以及其他数据的指针。当观察者为一个对象的属性注册时,被观察对象的isa
指针被修改,指向一个中间类而不是真正的类。因此,isa
指针的值不一定反映实例的实际类。 您永远不应该依赖isa
指针来确定类的继承关系。相反,您应该使用类方法来确定对象实例的类。
KVO原理
一:在self.p_man添加KVO之前,查看其继承关系。结果:isa = Man,superClass = NSObject二:在self.p_man添加KVO之后,查看其继承关系。结果:isa = NSKVONotifying_Man,superClass = NSObject
官方文档中提及做多的关键字就是isa
,Objective-C的消息机制就是通过isa查找方法的。其实在添加KVO之后,isa
已经替换成了NSKVONotifying_Man
。因此调用属性的set方法的时候,根据isa找到的方法其实是NSKVONotifying_Man
中的set方法。
KVO是基于runtime机制实现的,当某个实例的属性第一次被观察的时候,系统会在运行时期动态的创建一个该类的子类(类名=NSKVONotifying_XXX
)并将isa指针指向新创建的子类。在这个派生类中重写所有被观察属性的set方法,在成员变量被改变前调用NSObject的willChangeValueForKey:
,被改变后调用didChangeValueForKey:
。从而导致observeValueForKey:ofObject:change:context
被调用。
KVO的这套实现机制中苹果还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类
-(void) addObserver: forKeyPath: options: context: 这个部分就是观察者的注册了。通过以下类图可以很方便得看到,所有的类的KVO观察都是通过infoTable管理的。以被观察对象实例作key,GSKVOInfo对象为value的形式保存在infoTable表里,每个被观察者实例会对应多个keypath,每个keypath会对应多个observer对象。顺带提一下,关于Notification的实现也类似,也是全局表维护通知的注册监听者和通知名。 GSKVOInfo的结构可以看出来,一个keyPath可以对应有多个观察者。其中观察对象的实例和option打包成GSKVOObservation对象保存在一起。
如何手动触发一个的KVO
// 手动触发- (void)set_P_mustacheLength_manual:(NSInteger)p_mustacheLength { [self willChangeValueForKey:@"p_mustacheLength"]; _p_mustacheLength = p_mustacheLength; [self didChangeValueForKey:@"p_mustacheLength"];}复制代码
如果要禁止KVO对某个属性自动触发,返回NO就可以
// 是否自动对属性p_mustacheLength触发KVO+(BOOL)automaticallyNotifiesObserversOfP_mustacheLength { // 默认返回YES return YES;}复制代码
KVO容易掉进去的坑
- 1、没有removeObserver (使用或者不要忘记)
- 2、同一个对象的同一个属性被重复removeObserver了多次(使用
context
来removeObserver) - 3、keyPath严重依赖于string(
@selector
弥补)
KVOController实现探索
自己实现一个KVO:
参考博客
七、消息转发
为啥可以对nil对象发送消息?
NilTest
宏,判断被发送消息的对象是否为nil
的。如果为nil
,那就直接返回nil
。
参考资料:
八、weak的原理
weak
不论是用作property
修饰符还是用来修饰一个变量的声明其作用是一样的,就是不增加新对象的引用计数,被释放时也不会减少新对象的引用计数,同时在新对象被销毁时,weak修饰的属性或变量均会被设置为nil,这样可以防止野指针错误,本文要讲解的也正是这个特性,runtime如何将weak修饰的变量的对象在销毁时自动置为nil。
那么
runtime
是如何实现在weak
修饰的变量的对象在被销毁时自动置为nil的呢?一个普遍的解释是:
runtime
对注册的类会进行布局,对于weak
修饰的对象会放入一个hash
表中。用weak
指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc
,假如weak
指向的对象内存地址是a,那么就会以a为键在这个weak
表中搜索,找到所有以a为键的weak
对象,从而设置为nil