KVO概述
键值观察Key-Value-Observer就是观察者模式。
- 观察者模式的定义:一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。
当需要检测其他类的属性值变化,但又不想被观察的类知道,有点像FBI监视嫌疑人,这个时候就可以使用KVO了。
KVO同KVC一样都依赖于Runtime的动态机制
KVO实现步骤
注册
实现方法
移除
KVO的实现分析 使用观察者模式需要被观察者的配合,当被观察者的状态发生变化的时候通过事先定义好的接口(协议)通知观察者。在KVO的使用中我们并不需要向被观察者添加额外的代码,就能在被观察的属性变化的时候得到通知,这个功能是如何实现的呢?同KVC一样依赖于强大的Runtime机制。
系统实现KVO有以下几个步骤:
当类A的对象第一次被观察的时候,系统会在运行期动态创建类A的派生类。我们称为B。
在派生类B中重写类A的setter方法,B类在被重写的setter方法中实现通知机制。
类B重写会 class方法,将自己伪装成类A。类B还会重写dealloc方法释放资源。 系统将所有指向类A对象的isa指针指向类B的对象。
KVO同KVC一样,通过 isa-swizzling 技术来实现。当观察者被注册为一个对象的属性的观察对象的isa指针被修改,指向一个中间类,而不是在真实的类。其结果是,isa指针的值并不一定反映实例的实际类。 所以不能依靠isa指针来确定对象是否是一个类的成员。应该使用class方法来确定对象实例的类
KVO,建议不要过多的使用,因为他的模式是一种轮询的方式,比如,一直在敲门,直到打开门为止,还有一种采用轮询的方式的是多线程中的自旋锁。
KVO内部实现原理
*KVO 键值监听。它提供一种机制,当指定的对象的属性被修改后,则对象就会收到通知。简单说就是每次指定的被观察者的属性被修改后,KVO就会自动通知相应的观察者了。
利用它可以很容易实现视图组件和数据模型的分离。
KVO是基于runtime机制实现的
在ObjC中要实现KVO则必须实现NSKeyValueObServing协议,不过幸运的是NSObject已经实现了该协议,因此所有的ObjC对象都可以使用KVO。
当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制。 setter方法会负责在调用原setter方法之前这之后,通知所有对象观察属性的更改情况。
键值观察通知依赖于NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。
如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法
补充:KVO的这套实现机制中苹果还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类
苹果官方文档:
大概意思就是说利用了isa_swizzling技术,大家都知道swizzling是一种OC级别的Hook技术,所以isa_swizzling就是一种isa Hook技术,在一个支持KVO的对象被添加了观察者,系统会为其生成一个子类,重写了setXXX方法(XXX为被监听的属性名),并将该实例的Isa指针指向了新的这个子类(class),这样对被观察者进行属性赋值的时候调用的是重写后的setXXX方法,而setXXX方法内部添加了通知机制;
自旋锁
互斥锁
多个线程共享同一个资源时,会造成线程不安全解决方式 互斥锁
主要是防止多线程抢资源造成的数据不安全
原理 :每一个对象(NSObject)内部都有一个锁(变量)当有线程要进入synchronized到代码块的时候先检查对象的锁是打开或者是关闭默认是打开,当线程会进入代码的内部,进行上锁
如果锁是是关闭的,再有线程执行代码的时候就要先等待,直到上个线程结束后就先等待,直到直到锁打开就才可以执行
@synchronized(self) {// 需要锁定的代码} 注意self要是全局对象
atomic :自旋锁(单写多写) 效率低 (轮讯)
1.使用id作为方法返回值的问题:
1>在接收方法的返回值的时候可以使用任何类型来接收,编译都不报错,但是运行时可能出错。
2.instancetype需要注意的点
1>instancetype在类型表示上,与id意思一样,都表示任何对象类型
2>instancetype只能用作返回值类型,不能向id一样声明变量、用作参数等
3>使用instancetype,编译器会检测instancetype的真实类型,如果类型不匹配,编译时会有个警告(Incompatible pointer types returning 'Person *' from a function with result type 'BFButton ’ 不相容的指针类型“人”从函数返回结果类型“BFButton *”)。(instancetype出现在哪个类型中就表示对应的类型)
/* runtime 简称运行时。是一套纯C语言的API。就是系统在运行的时候的一些机制,其中最主要的是消息机制。 C语言在编译的时候就决定调用哪个函数,编译完后直接顺序执行。 OC 的函数调用成为消息发送。属于动态调用过程。在编译的时候不能决定真正的调用哪个函数。只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
** 消息发送: 我们写 OC 代码,它在运行的时候也是转换成了runtime方式运行的。任何方法调用本质:就是发送一个消息(用runtime发送消息,OC 底层实现通过runtime实现)。
3、消息机制原理:(消息传递机制)OC如何实现动态调用的? runtime 会把我们的方法调用 转化为 消息发送,也就是objc_msgSend 方法,并且把方法的调用者和方法选择器,当做参数传递过去。 在这个时候方法的调用者会通过isa指针来找到方法所属的类,SEL通过方法名字查找到对应的函数指针(IMP),找到了函数指针就找到了实现的方法(在 cache 或者 methodLists 中查找),找到了就跳转到响应的方法去执行。 如果在类中没有找到该方法,就会通过super_class 往更上一级去查找,找到了就执行,没找到继续通过super_class 向上查找。
类中的methodLists 中存储的是对象方法,类方法存储在MetaClass(元类)中,Class会通过isa指针找到所属的元类。
typedef struct objc_method *Method; struct objc_method { SEL _Nonnull method_name OBJC2_UNAVAILABLE; char * _Nullable method_types OBJC2_UNAVAILABLE; IMP _Nonnull method_imp OBJC2_UNAVAILABLE; } objc_method *Method 存储了方法名,方法类型和方法实现。 方法名类型 SEL 方法类型 method_types 是个 char 指针,存储方法的参数类型和返回值类型 method_imp 指向了方法的实现,本质是一个函数指针
在编译阶段会根据方法的名字生成一个唯一的(整型)标识符。SEL的作用就是作为IMP的Key,进行存储的。 IMP(method_imp)就是指向实现函数的指针。通过SEL获取到IMP后,我们就找到了实现函数的入口
黑魔法:Method Swizzling本质上就是对IMP和SEL进行交换
****************** OC 的动态特性: ****************** 1、动态类型 即运行时再决定对象的类型,在日常中非常常见,简单的就说id类型。id类型即通用的对象类,任何对象都可以被id指针所指。其类型需要等到运行时才能决定,在编译阶段id就是通用类型。 2、动态绑定: 基于动态类型,在某个实例对象被确定后,其类型便被确定了。该对象的属性和响应的消息也被完全确定,则就是动态绑定。 动态绑定的作用: 即在实例所属的类确定后,将某些属性和响应的方法绑定到实例上。这里所属的属性和方法当然包括了原来没有在类中实现的,而是在运行时才需要新加入的实现。 3、动态加载: 根据需求加载所需要的资源,Retina设备上加载@2x的图片,而老一些的普通品你过目加载原图。
黑魔法:方法交换 分类中添加属性
/ / OC是编程语言,但是OC不是Cocoa,Cocoa Touch 是官方框架。 OC = C+编译器+Runtime。 程序执行过程:预处理->编译->链接->运行。
Objective-C 是运行时的语言,就是说它会尽可能的把编译和链接时要执行的逻辑延迟到运行时。 OC之所以从C变成了面向对象的C,拥有动态特性,都是由于运行时系统的存在。 Objective-C动态运行库会自动注册我们代码中定义的所有的类。我们也可以在运行时创建类定义并使用objc_addClass函数来注册它们。
/ / 2、runtime 机制: runtime,是一套纯C(C语言编写的)的api。而oc就是运行时机制,也就是在运行时候的一些机制,其中最主要的是 消息机制。 我们写oc代码,它在运行的时候也就是转换成了 runtime方式运行的。任何方法调用本质:就是发送一个消息
1、 C 语言,函数的调用在编译的时候就决定调用哪个函数。 OC是基于C的,它为C添加了面向对象的特性。它讲很多静态语言在编译和链接时期做的事,放到了runtime运行时来处理,可以说runtime是我们OC幕后工作者。(也就是说,编译后的文件不全是机器指令,还有一部分中间代码,在运行的时候,通过runtime再把需要转换的中间代码再编译成机器指令)这使得OC有很大的灵活性。
动态的确认类型、动态的确定消息传递的对象、动态的给对象添加方法、消息。
@selector()这是一个SEL方法选择器。SEL主要作用是快速的通过方法名字查找到对应的函数指针,然后根据函数指针调用其函数。OC在编译时,会依据每一个方法的名字生成一个唯一的整型标识符,这个标识符就是SEL。 SEL的作用是作为IMP的Key。进行存储的。因为存储的是一个字符串。 IMP是指向实现函数的指针,通过SEL取得IMP后,我们就获得 最终要找的实现函数的入口。 Method: 这个结构体相当于在SEL和IMP之间作了一个绑定。这样有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。(在运行时才将SEL和IMP绑定, 动态配置方法)( objc_method_list 就是用来存储当前类的方法链表,objc_method存储了类的某个方法的信息。 objc_cache 方法缓存
一个对象的isa指针指向什么?有什么作用? 每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,,根据这个指针就能知道将来调用哪个类的方法。
所有的objc_object对象结构体都有一个isa指针,这个isa指向它所属的类,在运行时就靠这个指针来检测这个对象是否响应一个selector。
5、runtime 常见作用: 1、创建一个新类(KVO) 2、动态添加方法 3、动态添加属性 4、交换方法(黑魔法) 5、获取类的属性列表、方法列表 6、实现NSCoding的自动归档和解档 7、实现字典和模型的自动转换
4、消息机制调用流程(消息机制) 怎么去调用 eat方法,对象方法:(保存到类对象的方法列表) 类方法: (保存到元类中(Meta Class)方法列表) 1、oc在向一个对象发送消息时,runtime库根据对象的isa指针找到该对象对应的类或其父类中查找方法。。 2、注册方法编号(这里用方法编号的好处,可以快速查找)。 3、根据方法编号查找对应方法。 4、找到只是最终函数实现地址,根据地址方法调用对应函数。
typedef struct objc_class *Class; struct objc_class { Class isa; // 指向metaclass Class super_class ; // 指向其父类 const char *name ; // 类名 long version ; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取 long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法; long instance_size ; // 该类的实例变量大小(包括从父类继承下来的实例变量); struct objc_ivar_list *ivars; // 用于存储每个成员变量的地址 struct objc_method_list **methodLists ; // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法; struct objc_cache *cache; // 指向最近使用的方法的指针,用于提升效率; struct objc_protocol_list *protocols; // 存储该类遵守的协议 } 我们可以看到,对于一个Class类中,存在很多东西,下面我来一一解释一下: Class isa:指向metaclass,也就是静态的Class。一般一个Obj对象中的isa会指向普通的Class,这个Class中存储普通成员变量和对象方法(“-”开头的方法),普通Class中的isa指针指向静态Class,静态Class中存储static类型成员变量和类方法(“+”开头的方 法)。 Class super_class:指向父类,如果这个类是根类,则为NULL。
6、metaClass (元类) metaclass是Class对象的类,同样也是个对象。每个类都必须有一个唯一的metaclass。任何基类的metaclass都是他们自己(他们的isa指针指向自己)。也就是说metaclass的isa指针指向自己(是自己的一个实例)。
类的实例对象的isa指针指向该类;该类的isa指针指向metaclass。也就是说,成员方法在class method-list中,类方法记录在meta-class中。
示例在class中的查找位置:先在class的cache(缓存)中查找,没有找打在去methodLists中去查找。
7、在Objective-C中,对象的类是isa指针决定的。isa指针指向对象所属的类。 OC中类也是一个对象。这意味着你可以发送消息给一个类。这就意味着类结构必须以一个isa指针开始。从而可以个obj_object在二进制层面兼容。然后这个结构体的下一个字段必须是一个指向超类的指针(对于基类则为nil)。为了调用类里面的方法,类的isa指针必须指向包含这些类方法的类结构体。这就引出了元类的定义:元类是类对象的类。 元类中存储了类的类方法,每个类都必须有独一无二的元类,因为每个类都有是独一无二的类方法。
可以先理解一下实例对象的isa指针:实例对象的isa指是指向类的。类中存储了实例的方法。
8、元类的类是什么? 元类,就向之前的类一样,它也是一个对象。你也可以调用他的方法。自然的,这就以为这它必须也有一个类。 所有的元类都使用跟元类(集成体系中处于顶端的类的元类)作为他们的类。这就爱以为这所有的object的子类(大多数类)的元类都是以NSObject的元类作为他们的类。根据这个原则,所有的元类使用跟元类作为他们的类,跟元类则就是他自己。也就是跟元类的isa指针指向自己。
动态添加属性:
+load 方法按照父类到子类到分类的顺序加载。
*/
首先,我们利用runtime在添加监听之前和之后分别打印一下类对象
NSLog(@"%@", object_getClass(self.person));
[self.person addObserver:self forKeyPath:@"name" options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
NSLog(@"%@", object_getClass(self.person));
2018-05-23 15:07:46.572409+0800 FFKVO[13725:4936096] Person
2018-05-23 15:07:46.573037+0800 FFKVO[13725:4936096] NSKVONotifying_Person
我们发现添加监控之后,实例对象的类对象的发生了变化,系统为我们添加了一个NSKVONotifying_Person的类。因为属性赋值是通过setter方法实现的,所以很明显的是系统动态的生成的NSKVONotifying_Person类重写了setter方法。我们手动创建 NSKVONotifying_Person 类,运行时会在添加KVO处崩溃。
在系统自动创建重写的的setter方法内部做了什么呢??同样在添加监听方法之前,利用runtime打印下方法的实现,截图如下: