Objective-C 中的鍵(key)-值(value)觀察(KVO)並非什麼新鮮事物,它來源於設計模式中的觀察者模式,其基本思想就是:html
一個目標對象管理全部依賴於它的觀察者對象,並在它自身的狀態改變時主動通知觀察者對象。這個主動通知一般是經過調用各觀察者對象所提供的接口方法來實現的。觀察者模式較完美地將目標對象與觀察者對象解耦。git
在 Objective-C 中有兩種使用鍵值觀察的方式:手動或自動,此外還支持註冊依賴鍵(即一個鍵依賴於其餘鍵,其餘鍵的變化也會做用到該鍵)。下面將一一講述這些,並會深刻 Objective-C 內部一窺鍵值觀察是如何實現的。github
本文源碼下載:點此下載設計模式
若是咱們已經有了包含可供鍵值觀察屬性的類,那麼就能夠經過在該類的對象(被觀察對象)上調用名爲 NSKeyValueObserverRegistration 的 category 方法將觀察者對象與被觀察者對象註冊與解除註冊:數組
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context; - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
這兩個方法的定義在 Foundation/NSKeyValueObserving.h 中,NSObject,NSArray,NSSet均實現了以上方法,所以咱們不只能夠觀察普通對象,還能夠觀察數組或結合類對象。在該頭文件中,咱們還能夠看到 NSObject 還實現了 NSKeyValueObserverNotification 的 category 方法(更多相似方法,請查看該頭文件):app
- (void)willChangeValueForKey:(NSString *)key; - (void)didChangeValueForKey:(NSString *)key;
這兩個方法在手動實現鍵值觀察時會用到,暫且不提。框架
值得注意的是:不要忘記解除註冊,不然會致使資源泄露。函數
將觀察者與被觀察者註冊好以後,就能夠對觀察者對象的屬性進行操做,這些變動操做就會被通知給觀察者對象。注意,只有遵循 KVO 方式來設置屬性,觀察者對象纔會獲取通知,也就是說遵循使用屬性的 setter 方法,或經過 key-path 來設置:優化
[target setAge:30]; [target setValue:[NSNumber numberWithInt:30] forKey:@"age"];
觀察者須要實現名爲 NSKeyValueObserving 的 category 方法來處理收到的變動通知:atom
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
在這裏,change 這個字典保存了變動信息,具體是哪些信息取決於註冊時的 NSKeyValueObservingOptions。
觀察者類:
// Observer.h @interface Observer : NSObject @end // Observer.m #import "Observer.h" #import <objc/runtime.h> #import "Target.h" @implementation Observer - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 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 { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } @end
注意:在實現處理變動通知方法 observeValueForKeyPath 時,要將不能處理的 key 轉發給 super 的 observeValueForKeyPath 來處理。
使用示例:
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"];
在這裏 observer 觀察 target 的 age 屬性變化,運行結果以下:
>> class: Target, Age changed
old age is 10
new age is 30
上面的 Target 應該怎麼實現呢?首先來看手動實現。
@interface Target : NSObject { int age; } // for manual KVO - age - (int) age; - (void) setAge:(int)theAge; @end @implementation Target - (id) init { self = [super init]; if (nil != self) { age = 10; } return self; } // 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
首先,須要手動實現屬性的 setter 方法,並在設置操做的先後分別調用 willChangeValueForKey: 和 didChangeValueForKey方法,這兩個方法用於通知系統該 key 的屬性值即將和已經變動了;
其次,要實現類方法 automaticallyNotifiesObserversForKey,並在其中設置對該 key 不自動發送通知(返回 NO 便可)。這裏要注意,對其它非手動實現的 key,要轉交給 super 來處理。
自動實現鍵值觀察就很是簡單了,只要使用了自動屬性便可。
@interface Target : NSObject // for automatic KVO - age @property (nonatomic, readwrite) int age; @end @implementation Target @synthesize age; // for automatic KVO - age - (id) init { self = [super init]; if (nil != self) { age = 10; } return self; } @end
有時候一個屬性的值依賴於另外一對象中的一個或多個屬性,若是這些屬性中任一屬性的值發生變動,被依賴的屬性值也應當爲其變動進行標記。所以,object 引入了依賴鍵。
觀察依賴鍵的方式與前面描述的同樣,下面先在 Observer 的 observeValueForKeyPath:ofObject:change:context: 中添加處理變動通知的代碼:
#import "TargetWrapper.h" - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 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]; } }
在這裏,觀察的是 TargetWrapper 類的 information 屬性,該屬性是依賴於 Target 類的 age 和 grade 屬性。爲此,我在 Target 中添加了 grade 屬性:
@interface Target : NSObject @property (nonatomic, readwrite) int grade; @property (nonatomic, readwrite) int age; @end @implementation Target @synthesize age; // for automatic KVO - age @synthesize grade; @end
下面來看看 TragetWrapper 中的依賴鍵屬性是如何實現的。
@class Target; @interface TargetWrapper : NSObject { @private Target * _target; } @property(nonatomic, assign) NSString * information; @property(nonatomic, retain) Target * target; -(id) init:(Target *)aTarget; @end #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]]; } + (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; //} @end
首先,要手動實現屬性 information 的 setter/getter 方法,在其中使用 Target 的屬性來完成其 setter 和 getter。
其次,要實現 keyPathsForValuesAffectingInformation 或 keyPathsForValuesAffectingValueForKey: 方法來告訴系統 information 屬性依賴於哪些其餘屬性,這兩個方法都返回一個key-path 的集合。在這裏要多說幾句,若是選擇實現 keyPathsForValuesAffectingValueForKey,要先獲取 super 返回的結果 set,而後判斷 key 是否是目標 key,若是是就將依賴屬性的 key-path 結合追加到 super 返回的結果 set 中,不然直接返回 super的結果。
在這裏,information 屬性依賴於 target 的 age 和 grade 屬性,target 的 age/grade 屬性任一發生變化,information 的觀察者都會獲得通知。
Observer * observer = [[[Observer alloc] init] autorelease]; Target * target = [[[Target alloc] init] autorelease]; TargetWrapper * wrapper = [[[TargetWrapper alloc] init:target] autorelease]; [wrapper addObserver:observer forKeyPath:@"information" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:[TargetWrapper class]]; [target setAge:30]; [target setGrade:1]; [wrapper removeObserver:observer forKeyPath:@"information"];
輸出結果:
>> 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
鍵值觀察用處不少,Core Binding 背後的實現就有它的身影,那鍵值觀察背後的實現又如何呢?想想在上面的自動實現方式中,咱們並不須要在被觀察對象 Target 中添加額外的代碼,就能得到鍵值觀察的功能,這很好很強大,這是怎麼作到的呢?答案就是 Objective C 強大的 runtime 動態能力,下面咱們一塊兒來窺探下其內部實現過程。
當某個類的對象第一次被觀察時,系統就會在運行期動態地建立該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的 setter 方法。
派生類在被重寫的 setter 方法實現真正的通知機制,就如前面手動實現鍵值觀察那樣。這麼作是基於設置屬性會調用 setter 方法,而經過重寫就得到了 KVO 須要的通知機制。固然前提是要經過遵循 KVO 的屬性設置方式來變動屬性值,若是僅是直接修改屬性對應的成員變量,是沒法實現 KVO 的。
同時派生類還重寫了 class 方法以「欺騙」外部調用者它就是起初的那個類。而後系統將這個對象的 isa 指針指向這個新誕生的派生類,所以這個對象就成爲該派生類的對象了,於是在該對象上對 setter 的調用就會調用重寫的 setter,從而激活鍵值通知機制。此外,派生類還重寫了 dealloc 方法來釋放資源。
若是你對類和對象的關係不太明白,請閱讀《深刻淺出Cocoa之類與對象》;若是你對如何動態建立類不太明白,請閱讀《深刻淺出Cocoa 之動態建立類》。
蘋果官方文檔說得很簡潔:
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
The
isa
pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the
isa
pointer to determine class membership. Instead, you should use theclass
method to determine the class of an object instance.
因爲派生類中被重寫的 class 對咱們撒謊(它說它就是起初的基類),咱們只有經過調用 runtime 函數才能揭開派生類的真面目。 下面來看 Mike Ash 的代碼:
首先是帶有 x, y, z 三個屬性的觀察目標 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
下面是檢驗代碼:
#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; }
在上面的代碼中,輔助函數 ClassMethodNames 使用 runtime 函數來獲取類的方法列表,PrintDescription 打印對象的信息,包括經過 -class 獲取的類名, isa 指針指向的類的名字以及其中方法列表。
在這裏,我建立了四個對象,x 對象的 x 屬性被觀察,y 對象的 y 屬性被觀察,xy 對象的 x 和 y 屬性均被觀察,參照對象 control 沒有屬性被觀察。在代碼的最後部分,分別經過兩種方式(對象方法和 runtime 方法)打印出參數對象 control 和被觀察對象 x 對象的 setX 方面的實現地址,來對比顯示正常狀況下 setter 實現以及派生類中重寫的 setter 實現。
編譯運行,輸出以下:
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,這是由於新誕生的派生類重寫了 -class 方法聲稱它就是起初的基類,只有使用 runtime 函數 object_getClass 才能一睹芳容:NSKVONotifying_Foo。注意看:x,y 以及 xy 三個被觀察對象真正的類型都是 NSKVONotifying_Foo,並且該類實現了:setY:, setX:, class, dealloc, _isKVOA 這些方法。其中 setX:, setY:, class 和 dealloc 前面已經講到過,私有方法 _isKVOA 估計是用來標示該類是一個 KVO 機制聲稱的類。在這裏 Objective C 作了一些優化,它對全部被觀察對象只生成一個派生類,該派生類實現全部被觀察對象的 setter 方法,這樣就減小了派生類的數量,提供了效率。全部 NSKVONotifying_Foo 這個派生類重寫了 setX,setY方法(留意:沒有必要重寫 setZ 方法)。
接着來看最後兩行輸出,地址 0x100001df0 是 Foo 類中的實現,而地址是 0x7fff8458e025 是派生類 NSKVONotifying_Foo 類中的實現。那後面那個地址究竟是什麼呢?能夠經過 GDB 的 info 命令加 symbol 參數來查看該地址的信息:
(gdb) info symbol 0x7fff8458e025
_NSSetIntValueAndNotify in section LC_SEGMENT.__TEXT.__text of /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
看起來它是 Foundation 框架提供的私有函數:_NSSetIntValueAndNotify。更進一步,咱們來看看 Foundation 到底提供了哪些用於 KVO 的輔助函數。打開 terminal,使用 nm -a 命令查看 Foundation 中的信息:
nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
其中查找到咱們關注的函數:
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 提供了大部分基礎數據類型的輔助函數(Objective C中的 Boolean 只是 unsigned char 的 typedef,因此包括了,但沒有 C++中的 bool),此外還包括一些常見的 Cocoa 結構體如 Point, Range, Rect, Size,這代表這些結構體也能夠用於自動鍵值觀察,但要注意除此以外的結構體就不能用於自動鍵值觀察了。對於全部 Objective C 對象對應的是 __NSSetObjectValueAndNotify 方法。
KVO 並非什麼新事物,換湯不換藥,它只是觀察者模式在 Objective C 中的一種運用,這是 KVO 的指導思想所在。其餘語言實現中也有「KVO」,如 WPF 中的 binding。而在 Objective C 中又是經過強大的 runtime 來實現自動鍵值觀察的。至此,對 KVO 的使用以及注意事項,內部實現都介紹完畢,對 KVO 的理解又深刻一層了。Objective 中的 KVO 雖然能夠用,但卻非完美,有興趣的瞭解朋友請查看《KVO 的缺陷》 以及改良實現 MAKVONotificationCenter 。
KVC簡單使用:http://blog.csdn.net/yuquan0821/article/details/6645354
KVO簡單使用:http://blog.csdn.net/yuquan0821/article/details/6646400