[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 *Cache
struct 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
注意:运行时只能添加属性,不能添加成员变量,否则会打乱类的内存结构。
参考博客
Objective-C中的Runtime
深入解析 ObjC 中方法的结构
二、动态特性
[TOC]
@(runtime)[runTime, 温故而知新]
转自:iOS动态特性初研究(利用JSON动态创建类型和对象)
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时添加变量objective-c-add-property-in-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@:");
// 三:注册Class
objc_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,所以也比较容易理解。
参考资料:
ios动态添加属性的几种方法
iOS-Runtime-实践篇
老生常谈category增加属性的几种操作
Objective-C 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等等)
KVC修改readonly的系统隐藏变量。将UIPageControl的圆形替换为长条形。
[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"]); // 无法深层次取到子层级属性
复制代码
参考资料:
KVC/KVO原理详解及编程指南
ios动态添加属性的几种方法
KVC, KVO实现原理剖析
KVC 与 KVO 使用姿势和原理解析
KVC原理剖析
KVC 中的 valueForKeyPath 高级用法
iOS 关于KVC的一些总结
五、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_PROTOTYPES
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
#else
OBJC_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;
复制代码
实现performSelector 传递多个参数
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, ...); // 调用这个函数,伪代码...
}
// 查找IMP
IMP 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;
}
// 遍历继承链,查找IMP
IMP 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);
}));
});
}
复制代码
参考博客
GCD实践(二)少用performSelector系列方法
刨根究底iOS—调戏Category
六、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文档:Key-Value Observing Implementation Details
自动键值观察是使用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 (使用KVOController或者不要忘记)
- 2、同一个对象的同一个属性被重复removeObserver了多次(使用
context
来removeObserver) - 3、keyPath严重依赖于string(
@selector
弥补)
KVOController实现探索
自己实现一个KVO:ImplementKVO
参考博客
KVO进阶——KVO实现探究
探究KVO的底层实现原理
从自己实现isa-swizzling到说一些Runtime的内容
iOS探索KVO实现原理,重写KVO
如何优雅地使用 KVO
七、消息转发
为啥可以对nil对象发送消息?
NilTest
宏,判断被发送消息的对象是否为nil
的。如果为nil
,那就直接返回nil
。
参考资料:
Objective-C 消息发送与转发机制原理
八、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