博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Runtime学习笔记整理
阅读量:5778 次
发布时间:2019-06-18

本文共 29398 字,大约阅读时间需要 97 分钟。

[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_addMethodclass_addIvarclass_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_allocateClassPairobjc_registerClassPair之间用class_addIvar添加变量

3.3、后来查到有动态添加property的方法

(class_addProperty),在4.3之后。于是想到一种动态创建类型,并且可以用OC语法的方式访问变量。

首先,动态创建类型,添加变量(这个很重要,因为当我们访问property时,实际上是要对变量操作,如果没有添加变量,那么就是null),注册类型,然后往里动态添加属性,随后就可以象OC一样方便访问属性了 (因为静态类中属性会默认有一个和它同名的变量,对属性操作,实际上是对该变量操作)。

但实际上对该属性赋值后,取值却是null。因为只有在编译前定义的属性才会默认一个变量,property实际上只是提供了setter和getter的方法,至于你要把值存贮在哪里,需要自己设定,所以还需要在class_addProperty方法后,添加property的setter,getter,并在其中确定需要把值保存到哪里,从哪里取值。

getter

setter

这样我们就能用ClassA objA; [objAsetxxx:xxx]; [objA xxx]的方法来访问属性了(本人写了一个简单的实现,但暂时无法上传github,稍后会上传,请各位上传)

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_setAssociatedObjectobjc_getAssociatedObjectobjc_removeAssociatedObjects,下面的代码通过给UIButton添加一个分类的方式关联两个属性clickIntervalclickTime,来实现按钮的防连点操作。


// .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_alloctateClassPairclass_addIvarobjc_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_addPropertyclass_addMethodobjc_getAssociatedObjectobjc_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:forUndefinedKeyvalueForUndefinedKey:,存值方式也一样,需要借助一个其他对象。由于这种方式没有借助于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

转载地址:http://jsuyx.baihongyu.com/

你可能感兴趣的文章
LINUX下防恶意扫描软件PortSentry
查看>>
由数据库对sql的执行说JDBC的Statement和PreparedStatement
查看>>
springmvc+swagger2
查看>>
软件评测-信息安全-应用安全-资源控制-用户登录限制(上)
查看>>
我的友情链接
查看>>
Java Web Application 自架构 一 注解化配置
查看>>
如何 debug Proxy.pac文件
查看>>
Python 学习笔记 - 面向对象(特殊成员)
查看>>
Kubernetes 1.11 手动安装并启用ipvs
查看>>
Puppet 配置管理工具安装
查看>>
Bug多,也别乱来,别被Bug主导了开发
查看>>
sed 替换基础使用
查看>>
高性能的MySQL(5)创建高性能的索引一B-Tree索引
查看>>
oracle备份与恢复--rman
查看>>
图片变形的抗锯齿处理方法
查看>>
Effective C++ Item 32 确保你的 public 继承模子里出来 is-a 关联
查看>>
phpstorm安装laravel-ide-helper实现自动完成、代码提示和跟踪
查看>>
python udp编程实例
查看>>
TortoiseSVN中图标的含义
查看>>
js原生继承之——构造函数式继承实例
查看>>