1. 程式人生 > >iOS下KVO的使用以及一些實現細節

iOS下KVO的使用以及一些實現細節

      KVO的是Key Value Observe的縮寫,中文是鍵值觀察。這是一個典型的觀察者模式,觀察者在鍵值改變時會得到通知。iOS中有個Notification的機制,也可以獲得通知,但這個機制需要有個Center,相比之下KVO更加簡潔而直接。
      KVO的使用也很簡單,就是簡單的3步。
      1.註冊需要觀察的物件的屬性addObserver:forKeyPath:options:context:
      2.實現observeValueForKeyPath:ofObject:change:context:方法,這個方法當觀察的屬性變化時會自動呼叫
      3.取消註冊觀察removeObserver:forKeyPath:context:


      不多說了,上程式碼:
@interface myPerson : NSObject
{
    NSString *_name;
    int      _age;
    int      _height;
    int      _weight;
}
@end

@interface testViewController : UIViewController
@property (nonatomic, retain) myPerson *testPerson;

- (IBAction)onBtnTest:(id)sender;
@end

- (void)testKVO
{
    testPerson = [[myPerson alloc] init];
    
    [testPerson addObserver:self forKeyPath:@"height" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"height"]) {
        NSLog(@"Height is changed! new=%@", [change valueForKey:NSKeyValueChangeNewKey]);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (IBAction)onBtnTest:(id)sender {
    int h = [[testPerson valueForKey:@"height"] intValue];    
    [testPerson setValue:[NSNumber numberWithInt:h+1] forKey:@"height"];
    NSLog(@"person height=%@", [testPerson valueForKey:@"height"]);
}

- (void)dealloc
{
    [testPerson removeObserver:self forKeyPath:@"height" context:nil];
    [super dealloc];
}
      第一段程式碼聲明瞭myPerson類,裡面有個_height的屬性。在testViewController有一個testPerson的物件指標。
      在testKVO這個方法裡面,我們註冊了testPerson這個物件height屬性的觀察,這樣當testPerson的height屬性變化時,會得到通知。在這個方法中還通過NSKeyValueObservingOptionNew這個引數要求把新值在dictionary中傳遞過來。
      重寫了observeValueForKeyPath:ofObject:change:context:方法,這個方法裡的change這個NSDictionary物件包含了相應的值。

      需要強調的是KVO的回撥要被呼叫,屬性必須是通過KVC的方法來修改的,如果是呼叫類的其他方法來修改屬性,這個觀察者是不會得到通知的。

       因為Cocoa是嚴格遵循MVC模式的,所以KVO在觀察Modal的資料變化時很有用。那麼KVO是怎麼實現的呢,蘋果官方文件上說的比較簡單:“Automatic key-value observing is implemented using a technique called isa-swizzling.”
       “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.”
        就是說在執行時會生成一個派生類,在這個派生類中重寫基類中任何被觀察屬性的 setter 方法,用來欺騙系統頂替原先的類。

        繼續觀察一下程式碼:
@interface myPerson : NSObject
{
    NSString *_name;
}

@property (nonatomic)int height;
@property (nonatomic)int weight;
@property (nonatomic)int age;
@end

@implementation myPerson
@synthesize height, weight, age;
@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);
}

- (void)testKVOImplementation
{
    myPerson * anything = [[myPerson alloc] init];
    myPerson * hObserver = [[myPerson alloc] init];
    myPerson * wObserver = [[myPerson alloc] init];
    myPerson * hwObserver = [[myPerson alloc] init];
    myPerson * normal = [[myPerson alloc] init];
    
    [hObserver addObserver:anything forKeyPath:@"height" options:0 context:NULL];
    [wObserver addObserver:anything forKeyPath:@"weight" options:0 context:NULL];
    
    [hwObserver addObserver:anything forKeyPath:@"height" options:0 context:NULL];
    [hwObserver addObserver:anything forKeyPath:@"weight" options:0 context:NULL];
    
    PrintDescription(@"normal", normal);
    PrintDescription(@"hObserver", hObserver);
    PrintDescription(@"wObserver", wObserver);
    PrintDescription(@"hwOBserver", hwObserver);
    
    NSLog(@"\n\tUsing NSObject methods, normal setHeight: is %p, overridden setHeight: is %p\n",
          [normal methodForSelector:@selector(setHeight:)],
          [hObserver methodForSelector:@selector(setHeight:)]);
    NSLog(@"\n\tUsing libobjc functions, normal setHeight: is %p, overridden setHeight: is %p\n",
          method_getImplementation(class_getInstanceMethod(object_getClass(normal),
                                                           @selector(setHeight:))),
          method_getImplementation(class_getInstanceMethod(object_getClass(hObserver),
                                                           @selector(setHeight:))));
}
        略微改寫了一下myPerson,age/height/weight兩個屬性增加了getter/setter方法,然後運用runtime的方法,列印相應的內容,執行的log如下:
2013-11-02 20:36:22.391 test[2438:c07] 
normal: <myPerson: 0x886b840>
NSObject class myPerson
libobjc class myPerson
implements methods <weight, setWeight:, age, setAge:, height, setHeight:>
2013-11-02 20:36:22.393 test[2438:c07] 
hObserver: <myPerson: 0x886b7e0>
NSObject class myPerson
libobjc class NSKVONotifying_myPerson
implements methods <setWeight:, setHeight:, class, dealloc, _isKVOA>
2013-11-02 20:36:22.393 test[2438:c07] 
wObserver: <myPerson: 0x886b800>
NSObject class myPerson
libobjc class NSKVONotifying_myPerson
implements methods <setWeight:, setHeight:, class, dealloc, _isKVOA>
2013-11-02 20:36:22.393 test[2438:c07] 
hwOBserver: <myPerson: 0x886b820>
NSObject class myPerson
libobjc class NSKVONotifying_myPerson
implements methods <setWeight:, setHeight:, class, dealloc, _isKVOA>
2013-11-02 20:36:22.394 test[2438:c07] 
Using NSObject methods, normal setHeight: is 0x37e0, overridden setHeight: is 0x37e0
2013-11-02 20:36:22.394 test[2438:c07] 
Using libobjc functions, normal setHeight: is 0x37e0, overridden setHeight: is 0xb859e0

        從log資訊可以清楚的看到派生了一個NSKVONotifying_XXX的類,這個派生類集合了每個KVO觀察者的資訊,所以這個派生類可以全域性公用。
        另外,觀察原來類的方法和派生類的方法,每個被觀察的屬性都重寫了,比如:setWeight:方法和setHeight:方法,沒被觀察的屬性都沒有重新生成,比如:height:方法、weight:方法、age:方法和setAge:方法。