我们在工作中经常会用到@H_419_2@ 概述 @H_419_2@KVO
,但是系统原生的KVO
并不好用,很容易导致Crash
。而且编写代码时,需要编写大量KVO
相关的代码,由于不支持block
的形式,代码会写的很分散。 @H_419_2@本篇文章对KVO
的实现原理进行了详细的分析,并且简单的实现了一个KVO
,来当做技术交流。由于系统提供的KVO
存在很多问题,在文章的最下面给出了解决方案。
KVO
全称keyvalueObserving
,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO
的实现机制,所以对属性才会发生作用,一般继承自NSObject
的对象都默认支持KVO
。 @H_419_2@KVO
和NSNotificationCenter
都是iOS
中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO
是一对一的,而一对多的。KVO
对被监听对象无侵入性,不需要修改其内部代码即可实现监听。 @H_419_2@KVO
可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC
的mutableArrayValueForKey:
等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO
监听的方法。集合对象包含NSArray
和NSSet
。 基础使用 @H_419_2@使用KVO
分为三个步骤: 通过addobserver:forKeyPath:options:context:
方法注册观察者,观察者可以接收keyPath
属性的变化事件。 在观察者中实现observeValueForKeyPath:ofObject:change:context:
方法,当keyPath
属性发生改变后,KVO
会回调这个方法来通知观察者。 当观察者不需要监听时,可以调用removeObserver:forKeyPath:
方法将KVO
移除。需要注意的是,调用removeObserver
需要在观察者消失之前,否则会导致Crash
。 注册方法 @H_419_2@在注册观察者时,可以传入options
参数,参数是一个枚举类型。如果传入NSkeyvalueObservingOptionNew
和NSkeyvalueObservingOptionold
表示接收新值和旧值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSkeyvalueObservingOptionInitial
枚举。 @H_419_2@还可以通过方法context
传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式。 @H_419_2@在调用addobserver
方法后,KVO
并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash
。 监听方法 @H_419_2@观察者需要实现observeValueForKeyPath:ofObject:change:context:
方法,当KVO
事件到来时会调用这个方法,如果没有实现会导致Crash
。change
字典中存放KVO
属性相关的值,根据options
时传入的枚举来返回。枚举会对应相应key
来从字典中取出值,例如有NSkeyvalueChangeoldKey
字段,存储改变之前的旧值。 @H_419_2@change
中还有NSkeyvalueChangeKindKey
字段,和NSkeyvalueChangeoldKey
是平级的关系,来提供本次更改的信息,对应NSkeyvalueChange
枚举类型的value
。例如被观察属性发生改变时,字段为NSkeyvalueChangeSetting
。 @H_419_2@如果被观察对象是集合对象,在NSkeyvalueChangeKindKey
字段中会包含NSkeyvalueChangeInsertion
、NSkeyvalueChangeRemoval
、NSkeyvalueChangeReplacement
的信息,表示集合对象的 *** 作方式。 兼容的调用方式 @H_419_2@调用KVO
属性对象时,不仅可以通过点语法和set
语法进行调用,KVO
兼容很多种调用方式。 // 直接调用set方法,或者通过属性的点语法间接调用[account setname:@"Savings"]; // 使用KVC的setValue:forKey:方法[account setValue:@"Savings" forKey:@"name"]; // 使用KVC的setValue:forKeyPath:方法[document setValue:@"Savings" forKeyPath:@"account.name"];// 通过mutableArrayValueForKey:方法获取到代理对象,并使用代理对象进行 *** 作Transaction *newTransaction = <#Create a new transaction for the account#>;NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];[transactions addobject:newTransaction];实际应用 @H_419_2@
KVO
主要用来做键值观察 *** 作,想要一个值发生改变后通知另一个对象,则用KVO
实现最为合适。斯坦福大学的iOS
教程中有一个很经典的案例,通过KVO
在Model
和Controller
之间进行通信。 @H_419_2@ 注意点 @H_419_2@KVO
的addobserver
和removeObserver
需要是成对的,如果重复remove
则会导致NSRangeException
类型的Crash
,如果忘记remove
则会在观察者释放后再次接收到KVO
回调时Crash
。 @H_419_2@苹果官方推荐的方式是,在init
的时候进行addobserver
,在dealloc
时removeObserver
,这样可以保证add
和remove
是成对出现的,是一种比较理想的使用方式。 手动调用KVO @H_419_2@KVO
在属性发生改变时的调用是自动的,如果想要手动控制这个调用时机,或想自己实现KVO
属性的调用,则可以通过KVO
提供的方法进行调用。 - (voID)setBalance:(double)theBalance { if (theBalance != _balance) { [self willChangeValueForKey:@"balance"]; _balance = theBalance; [self dIDChangeValueForKey:@"balance"]; }}@H_419_2@可以看到调用
KVO
主要依靠两个方法,在属性发生改变之前调用willChangeValueForKey:
方法,在发生改变之后调用dIDChangeValueForKey:
方法。 @H_419_2@如果想控制当前对象的自动调用过程,也就是由上面两个方法发起的KVO
调用,则可以重写下面方法。方法返回YES
则表示可以调用,如果返回NO
则表示不可以调用。 + (BOol)automaticallyNotifIEsObserversForKey:(Nsstring *)theKey { BOol automatic = NO; if ([theKey isEqualToString:@"balance"]) { automatic = NO; } else { automatic = [super automaticallyNotifIEsObserversForKey:theKey]; } return automatic;}实现原理 @H_419_2@
KVO
是通过isa-swizzling
技术实现的(这句话是整个KVO
实现的重点)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa
指向中间类。并且将class
方法重写,返回原类的Class
。所以苹果建议在开发中不应该依赖isa
指针,而是通过class
实例方法来获取对象类型。 测试代码 @H_419_2@为了测试KVO
的实现方式,我们加入下面的测试代码。首先创建一个KVOObject
类,并在里面加入两个属性,然后重写description
方法,并在内部打印一些关键参数。 @interface KVOObject : NSObject@property (nonatomic,copy ) Nsstring *name;@property (nonatomic,assign) NSInteger age;@end@implementation KVOObject- (Nsstring *)description { NSLog(@"object address : %p \n",self); IMP nameIMP = class_getmethodImplementation(object_getClass(self),@selector(setname:)); IMP ageIMP = class_getmethodImplementation(object_getClass(self),@selector(setAge:)); NSLog(@"object setname: IMP %p object setAge: IMP %p \n",nameIMP,ageIMP); Class objectMethodClass = [self class]; Class objectRuntimeClass = object_getClass(self); Class superClass = class_getSuperclass(objectRuntimeClass); NSLog(@"objectMethodClass : %@,ObjectRuntimeClass : %@,superClass : %@ \n",objectMethodClass,objectRuntimeClass,superClass); NSLog(@"object method List \n"); unsigned int count; Method *methodList = class_copyMethodList(objectRuntimeClass,&count); for (NSInteger i = 0; i < count; i++) { Method method = methodList[i]; Nsstring *methodname = NsstringFromSelector(method_getname(method)); NSLog(@"method name = %@\n",methodname); } return @"";}@H_419_2@在另一个类中分别创建两个
KVOObject
对象,其中一个对象被观察者通过KVO
的方式监听,另一个对象则始终没有被监听。在KVO
前后分别打印两个对象的关键信息,看KVO
前后有什么变化。 @property (nonatomic,strong) KVOObject *object1;@property (nonatomic,strong) KVOObject *object2;self.object1 = [[KVOObject alloc] init];self.object2 = [[KVOObject alloc] init];[self.object1 description];[self.object2 description];[self.object1 addobserver:self forKeyPath:@"name" options:NSkeyvalueObservingOptionNew | NSkeyvalueObservingOptionold context:nil];[self.object1 addobserver:self forKeyPath:@"age" options:NSkeyvalueObservingOptionNew | NSkeyvalueObservingOptionold context:nil];[self.object1 description];[self.object2 description];self.object1.name = @"lxz";self.object1.age = 20;@H_419_2@下面是
KVO
前后打印的关键信息,我们在下面做详细分析。 // 第一次object address : 0x604000239340object setname: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0objectMethodClass : KVOObject,ObjectRuntimeClass : KVOObject,superClass : NSObjectobject method Listmethod name = .cxx_destructmethod name = descriptionmethod name = namemethod name = setname:method name = setAge:method name = ageobject address : 0x604000237920object setname: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0objectMethodClass : KVOObject,superClass : NSObjectobject method Listmethod name = .cxx_destructmethod name = descriptionmethod name = namemethod name = setname:method name = setAge:method name = age// 第二次object address : 0x604000239340object setname: IMP 0x10ea8defe object setAge: IMP 0x10ea94106objectMethodClass : KVOObject,ObjectRuntimeClass : NSKVONotifying_KVOObject,superClass : KVOObjectobject method Listmethod name = setAge:method name = setname:method name = classmethod name = deallocmethod name = _isKVOAobject address : 0x604000237920object setname: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0objectMethodClass : KVOObject,superClass : NSObjectobject method Listmethod name = .cxx_destructmethod name = descriptionmethod name = namemethod name = setname:method name = setAge:method name = age@H_419_2@我们发现对象被
KVO
后,其真正类型变为了NSKVONotifying_KVOObject
类,已经不是之前的类了。KVO
会在运行时动态创建一个新类,将对象的isa
指向新创建的类,新类是原类的子类,命名规则是NSKVONotifying_xxx
的格式。KVO
为了使其更像之前的类,还会将对象的class
实例方法重写,使其更像原类。 @H_419_2@在上面的代码中还发现了_isKVOA
方法,这个方法可以当做使用了KVO
的一个标记,系统可能也是这么用的。如果我们想判断当前类是否是KVO
动态生成的类,就可以从方法列表中搜索这个方法。 重写setter方法 @H_419_2@ @H_419_2@KVO
会重写keyPath
对应属性的setter
方法,没有被KVO
的属性则不会重写其setter
方法。在重写的setter
方法中,修改值之前会调用willChangeValueForKey:
方法,修改值之后会调用dIDChangeValueForKey:
方法,这两个方法最终都会被调用到observeValueForKeyPath:ofObject:change:context:
方法中。 object_getClass @H_419_2@为什么上面调用runtime
的object_getClass
函数,就可以获取到真正的类呢? @H_419_2@调用object_getClass
函数后其返回的是一个Class
类型,Class
是objc_class
定义的一个typedef
别名,通过objc_class
就可以获取到对象的isa
指针指向的Class
,也就是对象的类对象。 @H_419_2@由此可以推测,object_getClass
函数内部返回的是对象的isa
指针。 typedef struct objc_class *Class;struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABIliTY;#if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE; const char * _Nonnull name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_List * _Nullable ivars OBJC2_UNAVAILABLE; struct objc_method_List * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; struct objc_protocol_List * _Nullable protocols OBJC2_UNAVAILABLE;#endif}缺点 @H_419_2@苹果提供的
KVO
自身存在很多问题,首要问题在于,KVO
如果使用不当很容易崩溃。例如重复add
和remove
导致的Crash
,Observer
被释放导致的崩溃,keyPath
传错导致的崩溃等。 @H_419_2@在调用KVO
时需要传入一个keyPath
,由于keyPath
是字符串的形式,所以其对应的属性发生改变后,字符串没有改变容易导致Crash
。我们可以利用系统的反射机制将keyPath
反射出来,这样编译器可以在@selector()
中进行合法性检查。 NsstringFromSelector(@selector(isFinished))@H_419_2@
KVO
是一种事件绑定机制的实现,在keyPath
对应的值发生改变后会回调对应的方法。这种数据绑定机制,在对象关系很复杂的情况下,很容易导致不好排查的BUG
。例如keyPath
对应的属性被调用的关系很复杂,就不太建议对这个属性进行KVO
,可以想一下RAC
的信号脑补一下。 自己实现KVO @H_419_2@除了上面的缺点,KVO
还不支持block
语法,需要单独重写父类方法,这样加上add
和remove
方法就会导致代码很分散。所以,我通过runtime
简单的实现了一个KVO
,源码放在我的Github
上,叫做EasyKVO。 self.object1 = [[KVOObject alloc] init];[self.object1 lxz_addobserver:self originalSelector:@selector(name) callback:^(ID observedobject,Nsstring *observedKey,ID oldValue,ID newValue) { // callback}];self.object1.name = @"lxz";[self.object1 lxz_removeObserver:self originalSelector:@selector(name)];@H_419_2@调用代码很简单,直接通过
lxz_addobserver:originalSelector:callback:
方法就可以添加KVO
的监听,可以通过callback
的block
接收属性发生改变后的回调,而且方法的keyPath
接收的是一个SEL
类型参数,所以可以通过@selector()
传入参数时进行方法合法性检查,如果是未实现的方法直接就会报警告。 @H_419_2@通过lxz_removeObserver:originalSelector:
方法传入观察者和keyPath
,当观察者所有keyPath
都移除后则从KVO
中移除观察者对象。 @H_419_2@如果重复addobserver
和removeObserver
也没事,内部有判断逻辑。EasyKVO
内部通过weak
对观察者做引用,并不会影响观察者的生命周期,并且在观察者释放后不会导致Crash
。一次add
方法调用对应一个block
,如果观察者监听多个keyPath
属性,不需要在block
回调中判断keyPath
。 注意 @H_419_2@需要注意的是,EasyKVO
只是做技术交流,不建议在项目中使用。因为KVO
实现需要考虑很多情况,继承关系、多个观察者等很多问题。 KVOController @H_419_2@想在项目中安全便捷的使用KVO
的话,推荐Facebook
的一个KVO
开源第三方框架-KVOController。KVOController
本质上是对系统KVO
的封装,具有原生KVO
所有的功能,而且规避了原生KVO
的很多问题,兼容block
和action
两种回调方式。 源码分析 @H_419_2@从源码来看还是比较简单的,主要分为NSObject
的category
和FBKVOController
两部分。 @H_419_2@ @H_419_2@在category
中提供了KVOController
和KVOControllerNonRetaining
两个属性,顾名思义第一个会对observer
产生强引用,第二个则不会。其内部代码就是创建FBKVOController
对象的代码,并将创建出来的对象赋值给category
的属性,直接通过这个category
就可以懒加载创建FBKVOController
对象。 - (FBKVOController *)KVOControllerNonRetaining{ ID controller = objc_getAssociatedobject(self,NSObjectKVOControllerNonRetainingKey); if (nil == controller) { controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO]; self.KVOControllerNonRetaining = controller; } return controller;}FBKVOController部分 @H_419_2@在
FBKVOController
中分为三部分,_FBKVOInfo
是一个私有类,这个类的功能很简单,就是以结构化的形式保存FBKVOController
所需的各个对象,类似于模型类的功能。 @H_419_2@还有一个私有类_FBKVOSharedController
,这是FBKVOController
框架实现的关键。从命名上可以看出其是一个单例,所有通过FBKVOController
实现的KVO
,观察者都是它。每次通过FBKVOController
添加一个KVO
时,_FBKVOSharedController
都会将自己设为观察者,并在其内部实现observeValueForKeyPath:ofObject:change:context:
方法,将接收到的消息通过block
或action
进行转发。 @H_419_2@其功能很简单,通过observe:info:
方法添加KVO
监听,并用一个NSHashtable
保存_FBKVOInfo
信息。通过unobserve:info:
方法移除监听,并从NSHashtable
中将对应的_FBKVOInfo
移除。这两个方法内部都会调用系统的KVO
方法。 @H_419_2@在外界使用时需要用FBKVOController
类,其内部实现了初始化以及添加和移除监听的 *** 作。在调用添加监听方法后,其内部会创建一个_FBKVOInfo
对象,并通过一个NSMaptable
对象进行持有,然后会调用_FBKVOSharedController
来进行注册监听。 @H_419_2@使用FBKVOController
的话,不需要手动调用removeObserver
方法,在被监听对象消失的时候,会在dealloc
中调用remove
方法。如果因为业务需求,可以手动调用remove
方法,重复调用remove
方法不会有问题。 - (voID)_observe:(ID)object info:(_FBKVOInfo *)info{ NSMutableSet *infos = [_objectInfosMap objectForKey:object]; _FBKVOInfo *existingInfo = [infos member:info]; if (nil != existingInfo) { return; } if (nil == infos) { infos = [NSMutableSet set]; [_objectInfosMap setobject:infos forKey:object]; } [infos addobject:info]; [[_FBKVOSharedController sharedController] observe:object info:info];}@H_419_2@因为
FBKVOController
的实现很简单,所以这里就很简单的讲讲,具体实现可以去Github下载源码仔细分析一下。 总结 以上是内存溢出为你收集整理的KVO原理分析及使用进阶全部内容,希望文章能够帮你解决KVO原理分析及使用进阶所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)