一:kvo簡介
Objective-C 中的鍵(key)-值(value)觀察(KVO)并不是什么新鮮事物,它來源于設(shè)計(jì)模式中的觀察者模式,其基本思想就是:
一個(gè)目標(biāo)對象管理所有依賴于它的觀察者對象障斋,并在它自身的狀態(tài)改變時(shí)主動(dòng)通知觀察者對象。這個(gè)主動(dòng)通知通常是通過調(diào)用各觀察者對象所提供的接口方法來實(shí)現(xiàn)的墨微。觀察者模式較完美地將目標(biāo)對象與觀察者對象解耦嘀略。
二:運(yùn)用鍵值觀察
1:注冊與解除注冊
如果我們已經(jīng)有了包含可供鍵值觀察屬性的類,那么就可以通過在該類的對象(被觀察對象)上調(diào)用名為 NSKeyValueObserverRegistration 的 category 方法將觀察者對象與被觀察者對象注冊與解除注冊:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
這兩個(gè)方法的定義在 Foundation/NSKeyValueObserving.h 中踩窖,NSObject坡氯,NSArray,NSSet均實(shí)現(xiàn)了以上方法,因此我們不僅可以觀察普通對象箫柳,還可以觀察數(shù)組或結(jié)合類對象手形。在該頭文件中,我們還可以看到 NSObject 還實(shí)現(xiàn)了 NSKeyValueObserverNotification 的 category 方法(更多類似方法悯恍,請查看該頭文件):
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
這兩個(gè)方法在手動(dòng)實(shí)現(xiàn)鍵值觀察時(shí)會(huì)用到库糠,暫且不提,值得注意的是:不要忘記解除注冊,否則會(huì)導(dǎo)致資源泄露涮毫。
2:設(shè)置屬性
將觀察者與被觀察者注冊好之后瞬欧,就可以對觀察者對象的屬性進(jìn)行操作,這些變更操作就會(huì)被通知給觀察者對象罢防。注意艘虎,只有遵循 KVO 方式來設(shè)置屬性,觀察者對象才會(huì)獲取通知咒吐,也就是說遵循使用屬性的 setter 方法野建,或通過 key-path 來設(shè)置:
[target setAge:30];
[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];
3:處理變更通知
觀察者需要實(shí)現(xiàn)名為 NSKeyValueObserving 的 category 方法來處理收到的變更通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
在這里,change 這個(gè)字典保存了變更信息恬叹,具體是哪些信息取決于注冊時(shí)的 NSKeyValueObservingOptions
4:下面來看看一個(gè)完整的使用示例:
觀察者類
Observer.h
#import <Foundation/Foundation.h>
@interface Observer : NSObject
@end
Observer.m
#import "Observer.h"
#import <objc/runtime.h>
#import "Target.h"
#import "TargetWrapper.h"
@implementation Observer
//屬性改變時(shí)的回調(diào)方法
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%@類的對象%@改變了\n%@",object,keyPath,change);
// 下面是對TargetWtapper類的對象information(其值依賴于Target類的屬性age和grade的值)的監(jiān)聽的實(shí)現(xiàn)
if ([keyPath isEqualToString:@"age"])
{
Class classInfo = (Class)context;
NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
encoding:NSUTF8StringEncoding];
NSLog(@" >> class: %@, Age changed", className);
NSLog(@" old age is %@", [change objectForKey:@"old"]);
NSLog(@" new age is %@", [change objectForKey:@"new"]);
}
else if ([keyPath isEqualToString:@"information"])
{
Class classInfo = (Class)context;
NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
encoding:NSUTF8StringEncoding];
NSLog(@" >> class: %@, Information changed", className);
NSLog(@" old information is %@", [change objectForKey:@"old"]);
NSLog(@" new information is %@", [change objectForKey:@"new"]);
}
else
{
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
@end
注意:在實(shí)現(xiàn)處理變更通知方法 observeValueForKeyPath 時(shí)候生,要將不能處理的 key 轉(zhuǎn)發(fā)給 super 的 observeValueForKeyPath 來處理。
使用實(shí)例如下
Observer * observer = [[[Observer alloc] init] autorelease];
Target * target = [[[Target alloc] init] autorelease];
[target addObserver:observer
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:[Target class]];
[target setAge:30];
//[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];
[target removeObserver:observer forKeyPath:@"age"];
TargetWrapper * wrapper = [[[TargetWrapper alloc] init:target] autorelease];
[wrapper addObserver:observer
forKeyPath:@"information"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:[TargetWrapper class]];
[target setGrade:1];
[wrapper removeObserver:observer forKeyPath:@"information"];
在這里 observer 觀察 target 的 age 屬性變化绽昼,運(yùn)行結(jié)果如下:
>> class: Target, Age changed
old age is 10
new age is 30
三:手動(dòng)實(shí)現(xiàn)鍵值觀察
上面的 Target類應(yīng)該怎么實(shí)現(xiàn)呢唯鸭?首先來看手動(dòng)實(shí)現(xiàn)。
1:首先绪励,需要手動(dòng)實(shí)現(xiàn)屬性的 setter 方法
并在設(shè)置操作的前后分別調(diào)用 willChangeValueForKey: 和 didChangeValueForKey方法肿孵,這兩個(gè)方法用于通知系統(tǒng)該 key 的屬性值即將和已經(jīng)變更了;
2:其次疏魏,要實(shí)現(xiàn)類方法
automaticallyNotifiesObserversForKey停做,并在其中設(shè)置對該 key 不自動(dòng)發(fā)送通知(返回 NO 即可)。這里要注意大莫,對其它非手動(dòng)實(shí)現(xiàn)的 key蛉腌,要轉(zhuǎn)交給 super 來處理。
具體代碼如下
Target.h
#import <Foundation/Foundation.h>
@interface Target : NSObject
{
int age;
}
@property (nonatomic, readwrite) int grade;
@property (nonatomic, readwrite) int age;
@end
Target.m
#import "Target.h"
@implementation Target
@synthesize age; // for automatic KVO - age
@synthesize grade;
- (id) init
{
self = [super init];
if (nil != self)
{
age = 10;
grade = 0;
}
return self;
}
//手動(dòng)監(jiān)聽age的操作
// for manual KVO - age
- (int) age
{
return age;
}
- (void) setAge:(int)theAge
{
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end
五:鍵值觀察依賴鍵
有時(shí)候一個(gè)屬性的值依賴于另一對象中的一個(gè)或多個(gè)屬性只厘,如果這些屬性中任一屬性的值發(fā)生變更烙丛,被依賴的屬性值也應(yīng)當(dāng)為其變更進(jìn)行標(biāo)記。因此羔味,object 引入了依賴鍵河咽。
1:觀察依賴鍵
觀察依賴鍵的方式與前面描述的一樣,下面先在 Observer 的 observeValueForKeyPath:ofObject:change:context: 中添加處理變更通知的代碼:
Observer.m
#import "Observer.h"
#import <objc/runtime.h>
#import "Target.h"
#import "TargetWrapper.h"
@implementation Observer
//屬性改變時(shí)的回調(diào)方法
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// NSLog(@"%@類的對象%@改變了\n%@",object,keyPath,change);
// 下面是對TargetWtapper類的對象information(其值依賴于Target類的屬性age和grade的值)的監(jiān)聽的實(shí)現(xiàn)
if ([keyPath isEqualToString:@"age"])
{
Class classInfo = (Class)context;
NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
encoding:NSUTF8StringEncoding];
NSLog(@" >> class: %@, Age changed", className);
NSLog(@" old age is %@", [change objectForKey:@"old"]);
NSLog(@" new age is %@", [change objectForKey:@"new"]);
}
else if ([keyPath isEqualToString:@"information"])
{
Class classInfo = (Class)context;
NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
encoding:NSUTF8StringEncoding];
NSLog(@" >> class: %@, Information changed", className);
NSLog(@" old information is %@", [change objectForKey:@"old"]);
NSLog(@" new information is %@", [change objectForKey:@"new"]);
}
else
{
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
@end
2:實(shí)現(xiàn)依賴鍵
在這里赋元,觀察的是 TargetWrapper 類的 information 屬性,該屬性是依賴于 Target 類的 age 和 grade 屬性忘蟹。為此飒房,我在 Target 中添加了 grade 屬性:
<1>首先,要手動(dòng)實(shí)現(xiàn)屬性 information 的 setter/getter 方法
在其中使用 Target 的屬性來完成其 setter 和 getter媚值。
<2>其次狠毯,要實(shí)現(xiàn) keyPathsForValuesAffectingInformation 或 keyPathsForValuesAffectingValueForKey: 方法
來告訴系統(tǒng) information 屬性依賴于哪些其他屬性,這兩個(gè)方法都返回一個(gè)key-path 的集合褥芒。在這里要多說幾句嚼松,如果選擇實(shí)現(xiàn) keyPathsForValuesAffectingValueForKey,要先獲取 super 返回的結(jié)果 set锰扶,然后判斷 key 是不是目標(biāo) key献酗,如果是就將依賴屬性的 key-path 結(jié)合追加到 super 返回的結(jié)果 set 中,否則直接返回 super的結(jié)果坷牛。
在這里凌摄,information 屬性依賴于 target 的 age 和 grade 屬性,target 的 age/grade 屬性任一發(fā)生變化漓帅,information 的觀察者都會(huì)得到通知。
TargetWrappe.h
#import <Foundation/Foundation.h>
@class Target;
@interface TargetWrapper : NSObject
{
@private
Target * _target;
}
@property(nonatomic, assign) NSString * information;
@property(nonatomic, retain) Target * target;
-(id) init:(Target *)aTarget;
@end
TargetWrappe.m
#import "TargetWrapper.h"
#import "Target.h"
@implementation TargetWrapper
@synthesize target = _target;
-(id) init:(Target *)aTarget
{
self = [super init];
if (nil != self) {
_target = [aTarget retain];
}
return self;
}
-(void) dealloc
{
self.target = nil;
[super dealloc];
}
- (NSString *)information
{
return [[[NSString alloc] initWithFormat:@"%d#%d", [_target grade], [_target age]] autorelease];
}
- (void)setInformation:(NSString *)theInformation
{
NSArray * array = [theInformation componentsSeparatedByString:@"#"];
[_target setGrade:[[array objectAtIndex:0] intValue]];
[_target setAge:[[array objectAtIndex:1] intValue]];
}
//關(guān)聯(lián)的屬性的改變需要實(shí)現(xiàn)的代理方法是下面的這兩個(gè)之中的其中一個(gè),不要再實(shí)現(xiàn)- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 這個(gè)方法了,因?yàn)樗麜?huì)回調(diào)在Observe.m之中的那個(gè)方法
+ (NSSet *)keyPathsForValuesAffectingInformation
{
NSSet * keyPaths = [NSSet setWithObjects:@"target.age", @"target.grade", nil];
return keyPaths;
}
//+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
//{
// NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
// NSArray * moreKeyPaths = nil;
//
// if ([key isEqualToString:@"information"])
// {
// moreKeyPaths = [NSArray arrayWithObjects:@"target.age", @"target.grade", nil];
// }
//
// if (moreKeyPaths)
// {
// keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
// }
//
// return keyPaths;
//}
輸出結(jié)果:
>> class: TargetWrapper, Information changed
old information is 0#10
new information is 0#30
>> class: TargetWrapper, Information changed
old information is 0#30
new information is 1#30
六:鍵值觀察是如何實(shí)現(xiàn)的
1:實(shí)現(xiàn)機(jī)理
鍵值觀察用處很多痴怨,Core Binding 背后的實(shí)現(xiàn)就有它的身影忙干,那鍵值觀察背后的實(shí)現(xiàn)又如何呢?想一想在上面的自動(dòng)實(shí)現(xiàn)方式中浪藻,我們并不需要在被觀察對象 Target 中添加額外的代碼捐迫,就能獲得鍵值觀察的功能,這很好很強(qiáng)大爱葵,這是怎么做到的呢施戴?答案就是 Objective C 強(qiáng)大的 runtime 動(dòng)態(tài)能力,下面我們一起來窺探下其內(nèi)部實(shí)現(xiàn)過程萌丈。
** 當(dāng)某個(gè)類的對象第一次被觀察時(shí)赞哗,系統(tǒng)就會(huì)在運(yùn)行期動(dòng)態(tài)地創(chuàng)建該類的一個(gè)派生類,在這個(gè)派生類中重寫基類中任何被觀察屬性的 setter 方法辆雾。
派生類在被重寫的 setter 方法實(shí)現(xiàn)真正的通知機(jī)制肪笋,就如前面手動(dòng)實(shí)現(xiàn)鍵值觀察那樣。這么做是基于設(shè)置屬性會(huì)調(diào)用 setter 方法度迂,而通過重寫就獲得了 KVO 需要的通知機(jī)制藤乙。當(dāng)然前提是要通過遵循 KVO 的屬性設(shè)置方式來變更屬性值,如果僅是直接修改屬性對應(yīng)的成員變量惭墓,是無法實(shí)現(xiàn) KVO 的.
同時(shí)派生類還重寫了 class 方法以“欺騙”外部調(diào)用者它就是起初的那個(gè)類坛梁。然后系統(tǒng)將這個(gè)對象的 isa 指針指向這個(gè)新誕生的派生類,因此這個(gè)對象就成為該派生類的對象了腊凶,因而在該對象上對 setter 的調(diào)用就會(huì)調(diào)用重寫的 setter划咐,從而激活鍵值通知機(jī)制拴念。此外,派生類還重寫了 dealloc 方法來釋放資源尖殃。**
2:代碼分析
由于派生類中被重寫的 class 對我們?nèi)鲋e(它說它就是起初的基類)丈莺,我們只有通過調(diào)用 runtime 函數(shù)才能揭開派生類的真面目。 下面來看 Mike Ash 的代碼:
首先是帶有 x, y, z 三個(gè)屬性的觀察目標(biāo) Foo:
@interface Foo : NSObject
{
int x;
int y;
int z;
}
@property int x;
@property int y;
@property int z;
@end
@implementation Foo
@synthesize x, y, z;
@end
下面是檢驗(yàn)代碼:
#import <objc/runtime.h>
static NSArray * ClassMethodNames(Class c)
{
NSMutableArray * array = [NSMutableArray array];
unsigned int methodCount = 0;
Method * methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for(i = 0; i < methodCount; i++) {
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
return array;
}
static void PrintDescription(NSString * name, id obj)
{
NSString * str = [NSString stringWithFormat:
@"\n\t%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
name,
obj,
class_getName([obj class]),
class_getName(obj->isa),
[ClassMethodNames(obj->isa) componentsJoinedByString:@", "]];
NSLog(@"%@", str);
}
int main (int argc, const char * argv[])
{
@autoreleasepool {
// Deep into KVO: kesalin@gmail.com
//
Foo * anything = [[Foo alloc] init];
Foo * x = [[Foo alloc] init];
Foo * y = [[Foo alloc] init];
Foo * xy = [[Foo alloc] init];
Foo * control = [[Foo alloc] init];
[x addObserver:anything forKeyPath:@"x" options:0 context:NULL];
[y addObserver:anything forKeyPath:@"y" options:0 context:NULL];
[xy addObserver:anything forKeyPath:@"x" options:0 context:NULL];
[xy addObserver:anything forKeyPath:@"y" options:0 context:NULL];
PrintDescription(@"control", control);
PrintDescription(@"x", x);
PrintDescription(@"y", y);
PrintDescription(@"xy", xy);
NSLog(@"\n\tUsing NSObject methods, normal setX: is %p, overridden setX: is %p\n",
[control methodForSelector:@selector(setX:)],
[x methodForSelector:@selector(setX:)]);
NSLog(@"\n\tUsing libobjc functions, normal setX: is %p, overridden setX: is %p\n",
method_getImplementation(class_getInstanceMethod(object_getClass(control),
@selector(setX:))),
method_getImplementation(class_getInstanceMethod(object_getClass(x),
@selector(setX:))));
}
return 0;
}
在上面的代碼中送丰,輔助函數(shù) ClassMethodNames 使用 runtime 函數(shù)來獲取類的方法列表缔俄,PrintDescription 打印對象的信息,包括通過 -class 獲取的類名器躏, isa 指針指向的類的名字以及其中方法列表俐载。
在這里,我創(chuàng)建了四個(gè)對象登失,x 對象的 x 屬性被觀察遏佣,y 對象的 y 屬性被觀察,xy 對象的 x 和 y 屬性均被觀察揽浙,參照對象 control 沒有屬性被觀察状婶。在代碼的最后部分,分別通過兩種方式(對象方法和 runtime 方法)打印出參數(shù)對象 control 和被觀察對象 x 對象的 setX 方面的實(shí)現(xiàn)地址馅巷,來對比顯示正常情況下 setter 實(shí)現(xiàn)以及派生類中重寫的 setter 實(shí)現(xiàn)膛虫。
編譯運(yùn)行,輸出如下:
control: <Foo: 0x10010c980>
NSObject class Foo
libobjc class Foo
implements methods <x, setX:, y, setY:, z, setZ:>
x: <Foo: 0x10010c920>
NSObject class Foo
libobjc class NSKVONotifying_Foo
implements methods <setY:, setX:, class, dealloc, _isKVOA>
y: <Foo: 0x10010c940>
NSObject class Foo
libobjc class NSKVONotifying_Foo
implements methods <setY:, setX:, class, dealloc, _isKVOA>
xy: <Foo: 0x10010c960>
NSObject class Foo
libobjc class NSKVONotifying_Foo
implements methods <setY:, setX:, class, dealloc, _isKVOA>
Using NSObject methods, normal setX: is 0x100001df0, overridden setX: is 0x100001df0
Using libobjc functions, normal setX: is 0x100001df0, overridden setX: is 0x7fff8458e025
從上面的輸出可以看到钓猬,如果使用對象的 -class 方面輸出類名始終為:Foo稍刀,這是因?yàn)樾抡Q生的派生類重寫了 -class 方法聲稱它就是起初的基類,只有使用 runtime 函數(shù) object_getClass 才能一睹芳容:NSKVONotifying_Foo敞曹。注意看:x账月,y 以及 xy 三個(gè)被觀察對象真正的類型都是 NSKVONotifying_Foo,而且該類實(shí)現(xiàn)了:setY:, setX:, class, dealloc, _isKVOA 這些方法澳迫。其中 setX:, setY:, class 和 dealloc 前面已經(jīng)講到過局齿,私有方法 _isKVOA 估計(jì)是用來標(biāo)示該類是一個(gè) KVO 機(jī)制聲稱的類。在這里 Objective C 做了一些優(yōu)化纲刀,它對所有被觀察對象只生成一個(gè)派生類项炼,該派生類實(shí)現(xiàn)所有被觀察對象的 setter 方法,這樣就減少了派生類的數(shù)量示绊,提供了效率锭部。所有 NSKVONotifying_Foo 這個(gè)派生類重寫了 setX,setY方法(留意:沒有必要重寫 setZ 方法)面褐。
接著來看最后兩行輸出拌禾,地址 0x100001df0 是 Foo 類中的實(shí)現(xiàn),而地址是 0x7fff8458e025 是派生類 NSKVONotifying_Foo 類中的實(shí)現(xiàn)展哭。那后面那個(gè)地址到底是什么呢湃窍?可以通過 GDB 的 info 命令加 symbol 參數(shù)來查看該地址的信息:
(gdb) info symbol 0x7fff8458e025
_NSSetIntValueAndNotify in section LC_SEGMENT.__TEXT.__text of /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
看起來它是 Foundation 框架提供的私有函數(shù):_NSSetIntValueAndNotify闻蛀。更進(jìn)一步,我們來看看 Foundation 到底提供了哪些用于 KVO 的輔助函數(shù)您市。打開 terminal觉痛,使用 nm -a 命令查看 Foundation 中的信息:
nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
其中查找到我們關(guān)注的函數(shù):
00000000000233e7 t __NSSetDoubleValueAndNotify
00000000000f32ba t __NSSetFloatValueAndNotify
0000000000025025 t __NSSetIntValueAndNotify
000000000007fbb5 t __NSSetLongLongValueAndNotify
00000000000f33e8 t __NSSetLongValueAndNotify
000000000002d36c t __NSSetObjectValueAndNotify
0000000000024dc5 t __NSSetPointValueAndNotify
00000000000f39ba t __NSSetRangeValueAndNotify
00000000000f3aeb t __NSSetRectValueAndNotify
00000000000f3512 t __NSSetShortValueAndNotify
00000000000f3c2f t __NSSetSizeValueAndNotify
00000000000f363b t __NSSetUnsignedCharValueAndNotify
000000000006e91f t __NSSetUnsignedIntValueAndNotify
0000000000034b5b t __NSSetUnsignedLongLongValueAndNotify
00000000000f3766 t __NSSetUnsignedLongValueAndNotify
00000000000f3890 t __NSSetUnsignedShortValueAndNotify
00000000000f3060 t __NSSetValueAndNotifyForKeyInIvar
00000000000f30d7 t __NSSetValueAndNotifyForUndefinedKey
Foundation 提供了大部分基礎(chǔ)數(shù)據(jù)類型的輔助函數(shù)(Objective C中的 Boolean 只是 unsigned char 的 typedef,所以包括了茵休,但沒有 C++中的 bool)薪棒,此外還包括一些常見的 Cocoa 結(jié)構(gòu)體如 Point, Range, Rect, Size,這表明這些結(jié)構(gòu)體也可以用于自動(dòng)鍵值觀察榕莺,但要注意除此之外的結(jié)構(gòu)體就不能用于自動(dòng)鍵值觀察了俐芯。對于所有 Objective C 對象對應(yīng)的是 __NSSetObjectValueAndNotify 方法。
七:總結(jié)
KVO 并不是什么新事物钉鸯,換湯不換藥吧史,它只是觀察者模式在 Objective C 中的一種運(yùn)用,這是 KVO 的指導(dǎo)思想所在唠雕。其他語言實(shí)現(xiàn)中也有“KVO”贸营,如 WPF 中的 binding。而在 Objective C 中又是通過強(qiáng)大的 runtime 來實(shí)現(xiàn)自動(dòng)鍵值觀察的岩睁。至此莽使,對 KVO 的使用以及注意事項(xiàng),內(nèi)部實(shí)現(xiàn)都介紹完畢笙僚,對 KVO 的理解又深入一層了。Objective 中的 KVO 雖然可以用灵再,但卻非完美肋层,有興趣的了解朋友請查看《KVO 的缺陷》 以及改良實(shí)現(xiàn) MAKVONotificationCenter 。
本文代碼下載地址
八:引用,如有侵權(quán),本人立刻刪除
深入淺出Cocoa詳解鍵值觀察KVO及其實(shí)現(xiàn)機(jī)理
Key-Value Observing Programming Guide官方文檔
官方 KVO 實(shí)現(xiàn)的缺陷
深入淺出Cocoa 之動(dòng)態(tài)創(chuàng)建類
深入淺出Cocoa之類與對象