本文字数:5154
预计阅读时间:15分钟

KVO原理分析

介绍

KVO全称KeyValueObserving,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO
KVONSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。KVO对被监听对象无侵入性,不需要手动修改其内部代码即可实现监听。
KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVCmutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArrayNSSet

使用

使用KVO分为三个步骤
  1. 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件回调。
  2. 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
  3. 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash

注册

在注册观察者时,可以传入options参数,参数是一个枚举类型。如果传入NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld表示接收新值和旧值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial枚举。
还可以通过方法context传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式。
在调用addObserver方法后,KVO并不会对观察者进行强引用。所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash

监听

观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crashchange字典中存放KVO属性相关的值,根据options时传入的枚举来返回。枚举会对应相应key来从字典中取出值,例如有NSKeyValueChangeOldKey字段,存储改变之前的旧值。
change中还有NSKeyValueChangeKindKey字段,和NSKeyValueChangeOldKey是平级的关系,来提供本次更改的信息,对应NSKeyValueChange枚举类型的value。例如被观察属性发生改变时,字段为NSKeyValueChangeSetting
如果被观察对象是集合对象,在NSKeyValueChangeKindKey字段中会包含NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement的信息,表示集合对象的操作方式。

其他触发方法

调用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];

实际应用

KVO主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用KVO实现最为合适。斯坦福大学的iOS教程中有一个很经典的案例,通过KVOModelController之间进行通信。

触发

主动触发

KVO在属性发生改变时的调用是自动的,如果想要手动控制这个调用时机,或想自己实现KVO属性的调用,则可以通过KVO提供的方法进行调用。
- (
void
)setBalance:(
double
)theBalance {

if
 (theBalance != _balance) {

        [
self
 willChangeValueForKey:
@"balance"
];

        _balance = theBalance;

        [
self
 didChangeValueForKey:
@"balance"
];

    }

}

可以看到调用KVO主要依靠两个方法,在属性发生改变之前调用willChangeValueForKey:方法,在发生改变之后调用didChangeValueForKey:方法。但是,如果不调用willChangeValueForKey,直接调用didChangeValueForKey是不生效的,二者有先后顺序并且需要成对出现。

禁用KVO

如果想禁止某个属性的KVO,例如关键信息不想被三方SDK通过KVO的方式获取,可以通过automaticallyNotifiesObserversForKey方法返回NO来禁止其他地方对这个属性进行KVO。方法返回YES则表示可以调用,如果返回NO则表示不可以调用。此方法是一个类方法,可以在方法内部判断keyPath,来选择这个属性是否允许被KVO
+ (
BOOL
)automaticallyNotifiesObserversForKey:(
NSString
 *)theKey {

BOOL
 automatic = 
NO
;

if
 ([theKey isEqualToString:
@"balance"
]) {

        automatic = 
NO
;

    }

else
 {

        automatic = [
super
 automaticallyNotifiesObserversForKey:theKey];

    }

return
 automatic;

}

KVC触发

KVCKVO有特殊兼容,当通过KVC调用非属性的实例变量时,KVC内部也会触发KVO的回调,并通过NSKeyValueDidChangeNSKeyValueWillChange向上回调。
下面忽略main函数向上的系统函数,只保留关键堆栈。这是通过调用属性setter方法的方式回调的KVO堆栈。
* thread #
1
, queue = 
'com.apple.main-thread'
, stop reason = breakpoint 
38.1
* frame #
0
0x0000000101bc3a15
 TestKVO`::-[ViewController observeValueForKeyPath:ofObject:change:context:](
self
=
0x00007f8419705890
, _cmd=
"observeValueForKeyPath:ofObject:change:context:"
, keyPath=
"object"
, object=
0x0000604000015b00
, change=
0x0000608000265540
, context=
0x0000000000000000
) at ViewController.mm:
84
frame #
1
0x000000010327e820
 Foundation`
NSKeyValueNotifyObserver
 + 
349
frame #
2
0x000000010327e0d7
 Foundation`
NSKeyValueDidChange
 + 
483
frame #
3
0x000000010335f22b
 Foundation`-[
NSObject
(
NSKeyValueObservingPrivate
) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:] + 
778
frame #
4
0x000000010324b1b4
 Foundation`-[
NSObject
(
NSKeyValueObservingPrivate
) _changeValueForKey:key:key:usingBlock:] + 
61
frame #
5
0x00000001032a7b79
 Foundation`_NSSetObjectValueAndNotify + 
255
frame #
6
0x0000000101bc3937
 TestKVO`::-[ViewController viewDidLoad](
self
=
0x00007f8419705890
, _cmd=
"viewDidLoad"
) at ViewController.mm:
70
这是通过KVC触发的向上回调,可以看到正常通过修改属性的方式触发KVO,和通过KVC触发的KVO还是有区别的。通过KVC的方式触发KVO,甚至都没有_NSSetObjectValueAndNotify的调用。
* thread #
1
, queue = 
'com.apple.main-thread'
, stop reason = breakpoint 
37.1
* frame #
0
0x0000000106be1a85
 TestKVO`::-[ViewController observeValueForKeyPath:ofObject:change:context:](
self
=
0x00007fe68ac07710
, _cmd=
"observeValueForKeyPath:ofObject:change:context:"
, keyPath=
"object"
, object=
0x0000600000010c80
, change=
0x000060c000262780
, context=
0x0000000000000000
) at ViewController.mm:
84
frame #
1
0x000000010886d820
 Foundation`
NSKeyValueNotifyObserver
 + 
349
frame #
2
0x000000010886d0d7
 Foundation`
NSKeyValueDidChange
 + 
483
frame #
3
0x000000010894d422
 Foundation`
NSKeyValueDidChangeWithPerThreadPendingNotifications
 + 
148
frame #
4
0x0000000108879b47
 Foundation`-[
NSObject
(
NSKeyValueCoding
) setValue:forKey:] + 
292
frame #
5
0x0000000106be19aa
 TestKVO`::-[ViewController viewDidLoad](
self
=
0x00007fe68ac07710
, _cmd=
"viewDidLoad"
) at ViewController.mm:
70

实现原理

核心逻辑

KVO是通过isa-swizzling技术实现的,这是整个KVO实现的重点。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。苹果重写class方法,就是为了屏蔽中间类的存在。
所以,苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型,来避免被KVO或者其他runtime方法影响。

_NSSetObjectValueAndNotify

随后会修改中间类对应的set方法,并且插入willChangeValueForkey方法以及didChangeValueForKey方法,在两个方法中间调用父类的set方法。这个过程,系统将其封装到_NSSetObjectValueAndNotify函数中。通过查看这个函数的汇编代码,可以看到内部封装的willChangeValueForkey方法和didChangeValueForKey方法的调用。
系统并不是只封装了_NSSetObjectValueAndNotify函数,而是会根据属性类型,调用不同的函数。如果是Int类型就会调用_NSSetIntValueAndNotify,这些实现都定义在Foundation框架中。具体的可以通过hopper来查看Foundation框架的实现。
runtime会将新生成的NSKVONotifying_KVOTestsetObject方法的实现,替换成_NSSetObjectValueAndNotify函数,而不是重写setObject函数。通过下面的测试代码,可以查看selector对应的IMP,并且将其实现的地址打印出来。
KVOTest *test = [[KVOTest alloc] init];

[test setObject:[[
NSObject
 alloc] init]];

NSLog
(
@"%p"
, [test methodForSelector:
@selector
(setObject:)]);

[test addObserver:
self
 forKeyPath:
@"object"
 options:
NSKeyValueObservingOptionNew
 context:
nil
];

[test setObject:[[
NSObject
 alloc] init]];

NSLog
(
@"%p"
, [test methodForSelector:
@selector
(setObject:)]);


// 打印结果,第一次的方法地址为0x100c8e270,第二次的方法地址为0x7fff207a3203
(lldb) p (IMP)
0x100c8e270
(IMP) $
0
 = 
0x0000000100c8e270
 (DemoProject`-[KVOTest setObject:] at KVOTest.h:
11
)

(lldb) p (IMP)
0x7fff207a3203
(IMP) $
1
 = 
0x00007fff207a3203
 (Foundation`_NSSetObjectValueAndNotify)

_NSKVONotifyingCreateInfoWithOriginalClass

对于系统实现KVO的原理,可以对object_setClass打断点,或者对objc_allocateClassPair方法打断点也可以,这两个方法都是创建类必走的方法。通过这两个方法的汇编堆栈,向前回溯。随后,可以得到翻译后如下的汇编代码。
可以看到有一些类名拼接规则,随后根据类名创建新类。如果newCls为空则已经创建过,或者可能为空。如果newCls不为空,则注册新创建的类,并且设置SDTestKVOClassIndexedIvars结构体的一些参数。
Class _NSKVONotifyingCreateInfoWithOriginalClass(Class originalClass) {

constchar
 *clsName = class_getName(originalClass);

    size_t len = strlen(clsName);

    len += 
0x10
;

char
 *newClsName = malloc(len);

constchar
 *prefix = 
"NSKVONotifying_"
;

    __strlcpy_chk(newClsName, prefix, len);

    __strlcat_chk(newClsName, clsName, len, 
-1
);

    Class newCls = objc_allocateClassPair(originalClass, newClsName, 
0x68
);

if
 (newCls) {

        objc_registerClassPair(newCls);

        SDTestKVOClassIndexedIvars *indexedIvars = object_getIndexedIvars(newCls);

        indexedIvars->originalClass = originalClass;

        indexedIvars->KVOClass = newCls;

CFMutableSetRef
 mset = 
CFSetCreateMutable
(
nil
0
, kCFCopyStringSetCallBacks);

        indexedIvars->mset = mset;

CFMutableDictionaryRef
 mdict = 
CFDictionaryCreateMutable
(
nil
0
nil
, kCFTypeDictionaryValueCallBacks);

        indexedIvars->mdict = mdict;

        pthread_mutex_init(indexedIvars->lock);

staticdispatch_once_t
 onceToken;

dispatch_once
(&onceToken, ^{

bool
 flag = 
true
;

            IMP willChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, 
@selector
(willChangeValueForKey:));

            IMP didChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, 
@selector
(didChangeValueForKey:));

if
 (willChangeValueForKeyImp == _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange && didChangeValueForKeyImp == _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange) {

                flag = 
false
;

            }

            indexedIvars->flag = flag;

NSKVONotifyingSetMethodImplementation
(indexedIvars, 
@selector
(_isKVOA), 
NSKVOIsAutonotifying
nil
);

NSKVONotifyingSetMethodImplementation
(indexedIvars, 
@selector
(dealloc), 
NSKVODeallocate
nil
);

NSKVONotifyingSetMethodImplementation
(indexedIvars, 
@selector
(
class
), 
NSKVOClass
nil
);

        });

    } 
else
 {

returnnil
;

    }

return
 newCls;

}

验证

为了验证KVO的实现方式,我们加入下面的测试代码。首先创建一个KVOObject类,并在里面加入两个属性,然后重写description方法,并在内部打印一些关键参数。
需要注意的是,为了验证KVO在运行时做了什么,我打印了对象的class方法,以及通过runtime获取对象的类和父类。在添加KVO监听前后,都打印一次,观察系统做了什么。
@interfaceKVOObject : NSObject
@property
 (
nonatomic
copy
  ) 
NSString
 *name;

@property
 (
nonatomic
assign
NSInteger
 age;

@end

- (
NSString
 *)description {

    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 = [
selfclass
];

    Class objectRuntimeClass = object_getClass(
self
);

    Class superClass = class_getSuperclass(objectRuntimeClass);

NSLog
(
@"objectMethodClass : %@, ObjectRuntimeClass : %@, superClass : %@ \n"
, objectMethodClass, objectRuntimeClass, superClass);


NSLog
(
@"object method list \n"
);

unsignedint
 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@""
;

}

创建一个KVOObject对象,在KVO前后分别打印对象的关键信息,看KVO前后有什么变化。
self
.object = [[KVOObject alloc] init];

[
self
.object description];


[
self
.object addObserver:
self
 forKeyPath:
@"name"
 options:
NSKeyValueObservingOptionNew
 | 
NSKeyValueObservingOptionOld
 context:
nil
];


[
self
.object description];

下面是KVO前后打印的关键信息。
我们发现对象被KVO后,其真正类型变为了NSKVONotifying_KVOObject类,已经不是之前的类了。KVO会在运行时动态创建一个新类,将对象的isa指向新创建的类,并且将superClass指向原来的类KVOObject,新创建的类命名规则是NSKVONotifying_xxx的格式。KVO为了使其更像之前的类,还会将对象的class实例方法重写,使其更像原类。
添加KVO之后,由于修改了setName方法和setAge方法的IMP,所以打印这两个方法的IMP,也是一个新的地址,新的实现在NSKVONotifying_KVOObject中。
这种实现方式对业务代码没有侵入性,可以在不影响KVOObject其他对象的前提下,对单个对象进行监听并修改其方法实现,在赋值时触发KVO回调。
在上面的代码中还发现了_isKVOA方法,这个方法可以当做使用了KVO的一个标记,系统可能也是这么用的。如果我们想判断当前类是否是KVO动态生成的类,就可以从方法列表中搜索这个方法。
// 第一次
object address : 
0x604000239340
object setName: IMP 
0x10ddc2770
 object setAge: IMP 
0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : 
NSObject
object method list

method Name = .cxx_destruct

method Name = description

method Name = name

method Name = setName:

method Name = setAge:

method Name = age


// 第二次
object address : 
0x604000239340
object setName: IMP 
0x10ea8defe
 object setAge: IMP 
0x10ea94106
objectMethodClass : KVOObject, ObjectRuntimeClass : 
NSKVONotifying_KVOObject
, superClass : KVOObject

object method list

method Name = setAge:

method Name = setName:

method Name = 
class
method Name = dealloc

method Name = _isKVOA

object_getClass

为什么上面调用runtimeobject_getClass函数,就可以获取到真正的类呢?
调用object_getClass函数后其返回的是一个Class类型,Classobjc_class定义的一个typedef别名,通过objc_class就可以获取到对象的isa指针指向的Class,也就是对象的类对象。
由此可以知道,object_getClass函数内部返回的是对象的isa指针。
typedefstruct
 objc_class *Class;


struct
 objc_class {

    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;


#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;

constchar
 * _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
}

注意点

Crash

KVOaddObserverremoveObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash
苹果官方推荐的方式是,在init的时候进行addObserver,在deallocremoveObserver,这样可以保证addremove是成对出现的,是一种比较理想的使用方式。

错误检查

如果传入一个错误的keyPath并不会有错误提示。在调用KVO时需要传入一个keyPath,由于keyPath是字符串的形式,如果属性名发生改变后,字符串没有改变容易导致Crash。对于这个问题,我们可以利用系统的反射机制将keyPath反射出来,这样编译器可以在@selector()中进行合法性检查。
NSString
 *keyPath = 
NSStringFromSelector
(
@selector
(isFinished));

不能触发回调

由于KVO的实现机制,如果调用成员变量进行赋值,是不会触发KVO的。
@interfaceTestObject : NSObject
{

@public
NSObject
 *object;

}

@end

// 错误的调用方式
self
.object = [[TestObject alloc] init];

[
self
.object addObserver:
self
 forKeyPath:
@"object"
 options:
NSKeyValueObservingOptionNew
 context:
nil
];

self
.object->object = [[
NSObject
 alloc] init];

但是,如果通过KVC的方式调用赋值操作,则会触发KVO的回调方法。这是因为KVCKVO有单独的兼容,在KVC的赋值方法内部,手动调用了willChangeValueForKey:didChangeValueForKey:方法。
// KVC的方式调用
self
.object = [[TestObject alloc] init];

[
self
.object addObserver:
self
 forKeyPath:
@"object"
 options:
NSKeyValueObservingOptionNew
 context:
nil
];

[
self
.object setValue:[[
NSObject
 alloc] init] forKey:
@"object"
];

重复添加

KVO进行重复addObserver并不会导致崩溃,但是会出现重复执行KVO回调方法的问题。
[
self
.testLabel addObserver:
self
 forKeyPath:
@"text"
 options:
NSKeyValueObservingOptionNew
 context:
nil
];

self
.testLabel.text = 
@"test"
;

[
self
.testLabel addObserver:
self
 forKeyPath:
@"text"
 options:
NSKeyValueObservingOptionNew
 context:
nil
];

[
self
.testLabel addObserver:
self
 forKeyPath:
@"text"
 options:
NSKeyValueObservingOptionNew
 context:
nil
];

[
self
.testLabel addObserver:
self
 forKeyPath:
@"text"
 options:
NSKeyValueObservingOptionNew
 context:
nil
];

self
.testLabel.text = 
@"test"
;


// 输出
2018-08-0311
:
48
:
49.502450
+
0800
 KVOTest[
5846
:
412257
] test

2018-08-0311
:
48
:
52.975102
+
0800
 KVOTest[
5846
:
412257
] test

2018-08-0311
:
48
:
53.547145
+
0800
 KVOTest[
5846
:
412257
] test

2018-08-0311
:
48
:
54.087171
+
0800
 KVOTest[
5846
:
412257
] test

2018-08-0311
:
48
:
54.649244
+
0800
 KVOTest[
5846
:
412257
] test

通过上面的测试代码,并且在回调中打印object所对应的Class来看,并不会重复创建子类,始终都是一个类。虽然重复addobserver不会立刻崩溃,但是重复添加后在第一次调用removeObserver时,就会立刻崩溃。从崩溃堆栈来看,和重复移除的问题一样,都是系统主动抛出的异常。
Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <UILabel 0x7f859b547490> for the key path "text" from <UILabel 0x7f859b547490> because it is not registered as an observer.'

重复移除

KVO是不允许对一个keyPath进行重复移除的,如果重复移除,则会导致崩溃。例如下面的测试代码。
[
self
.testLabel addObserver:
self
 forKeyPath:
@"text"
 options:
NSKeyValueObservingOptionNew
 context:
nil
];

self
.testLabel.text = 
@"test"
;

[
self
.testLabel removeObserver:
self
 forKeyPath:
@"text"
];

[
self
.testLabel removeObserver:
self
 forKeyPath:
@"text"
];

[
self
.testLabel removeObserver:
self
 forKeyPath:
@"text"
];

执行上面的测试代码后,会造成下面的崩溃信息。从KVO的崩溃堆栈可以看出来,系统为了实现KVOaddObserverremoveObserver,为NSObject添加了一个名为NSKeyValueObserverRegistrationCategoryKVOaddObserverremoveObserver的实现都在里面。
在移除KVO的监听时,系统会判断当前KVOkeyPath是否已经被移除,如果已经被移除,则主动抛出一个NSException的异常。
2018-08-0310
:
54
:
27.477379
+
0800
 KVOTest[
4939
:
286991
] *** Terminating app due to uncaught exception 
'NSRangeException'
, reason: 
'Cannot remove an observer <ViewController 0x7ff6aee31600> for the key path "text" from <UILabel 0x7ff6aee2e850> because it is not registered as an observer.'
*** First throw call stack:

(

0
   CoreFoundation                      
0x000000010db2312b
 __exceptionPreprocess + 
171
1
   libobjc.A.dylib                     
0x000000010cc6af41
 objc_exception_throw + 
48
2
   CoreFoundation                      
0x000000010db98245
 +[
NSException
 raise:format:] + 
197
3
   Foundation                          
0x0000000108631f15
 -[
NSObject
(
NSKeyValueObserverRegistration
) _removeObserver:forProperty:] + 
497
4
   Foundation                          
0x0000000108631ccb
 -[
NSObject
(
NSKeyValueObserverRegistration
) removeObserver:forKeyPath:] + 
84
5
   KVOTest                             
0x0000000107959a55
 -[ViewController viewDidAppear:] + 
373
// .....
20UIKit0x000000010996d5d6UIApplicationMain
 + 
159
21
  KVOTest                             
0x00000001079696cf
 main + 
111
22
  libdyld.dylib                       
0x000000010fb43d81
 start + 
1
)

libc++abi.dylib: terminating with uncaught exception of type 
NSException

排查链路

KVO是一种事件绑定机制的实现,在keyPath对应的值发生改变后会回调对应的方法。这种数据绑定机制,在对象关系很复杂的情况下,很容易导致不好排查的bug。例如keyPath对应的属性被调用的关系很复杂,就不太建议对这个属性进行KVO

自己实现KVO

除了上面的缺点,KVO还不支持block语法,需要单独重写父类方法,这样加上addremove方法就会导致代码很分散。所以,我通过runtime简单的实现了一个KVO,源码放在我的Github上,叫做EasyKVO。
self
.object = [[KVOObject alloc] init];

[
self
.object lxz_addObserver:
self
 originalSelector:
@selector
(name) callback:^(
id
 observedObject, 
NSString
 *observedKey, 
id
 oldValue, 
id
 newValue) {

// 处理业务逻辑
}];


self
.object.name = 
@"lxz"
;


// 移除通知
[
self
.object lxz_removeObserver:
self
 originalSelector:
@selector
(name)];

调用代码很简单,直接通过lxz_addObserver:originalSelector:callback:方法就可以添加KVO的监听,可以通过callbackblock接收属性发生改变后的回调。而且方法的keyPath接收的是一个SEL类型参数,所以可以通过@selector()传入参数时进行方法合法性检查,如果是未实现的方法直接就会报警告。
通过lxz_removeObserver:originalSelector:方法传入观察者和keyPath,当观察者所有keyPath都移除后则从KVO中移除观察者对象。
如果重复addObserverremoveObserver也没事,内部有判断逻辑。EasyKVO内部通过weak对观察者做引用,并不会影响观察者的生命周期,并且在观察者释放后不会导致Crash。一次add方法调用对应一个block,如果观察者监听多个keyPath属性,不需要在block回调中判断keyPath

KVOController

想在项目中安全便捷的使用KVO的话,推荐Facebook的一个KVO开源第三方框架KVOController。KVOController本质上是对系统KVO的封装,具有原生KVO所有的功能,而且规避了原生KVO的很多问题,兼容blockaction两种回调方式。

源码分析

从源码来看还是比较简单的,主要分为NSObjectCategoryFBKVOController两部分。
Category中提供了KVOControllerKVOControllerNonRetaining两个属性,顾名思义第一个会对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中分为三部分,_FBKVOInfo是一个私有类,这个类的功能很简单,就是以结构化的形式保存FBKVOController所需的各个对象,类似于模型类的功能。
还有一个私有类_FBKVOSharedController,这是FBKVOController框架实现的关键。从命名上可以看出其是一个单例,所有通过FBKVOController实现的KVO,观察者都是它。每次通过FBKVOController添加一个KVO时,_FBKVOSharedController都会将自己设为观察者,并在其内部实现observeValueForKeyPath:ofObject:change:context:方法,将接收到的消息通过blockaction进行转发。
其功能很简单,通过observe:info:方法添加KVO监听,并用一个NSHashTable保存_FBKVOInfo信息。通过unobserve:info:方法移除监听,并从NSHashTable中将对应的_FBKVOInfo移除。这两个方法内部都会调用系统的KVO方法。
在外界使用时需要用FBKVOController类,其内部实现了初始化以及添加和移除监听的操作。在调用添加监听方法后,其内部会创建一个_FBKVOInfo对象,并通过一个NSMapTable对象进行持有,然后会调用_FBKVOSharedController来进行注册监听。
使用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];

}

因为FBKVOController的实现很简单,所以这里就很简单的讲讲,具体实现可以去Github下载源码仔细分析一下。

也许你还想看
(▼点击文章标题或封面查看)
继续阅读
阅读原文