深入理解KVO

CoderSpr1ngHall發表於2019-04-14
先來說說什麼是KVO,KVO全稱為Key Value Observing,鍵值監聽機制,由NSKeyValueObserving協議提供支援,NSObject類繼承了該協議,所以NSObject的子類都可使用該方法。

KVO的使用

1、註冊觀察者

//註冊觀察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
複製程式碼

option:

  • NSKeyValueObservingOptionOld 把更改之前的值提供給處理方法
  •  NSKeyValueObservingOptionNew 把更改之後的值提供給處理方法
  •  NSKeyValueObservingOptionInitial 把初始化的值提供給處理方法,一旦註冊,立馬就會呼叫一次。通常它會帶有新值,而不會帶有舊值。
  •  NSKeyValueObservingOptionPrior 分2次呼叫。在值改變之前和值改變之後。

context:

這裡的context字面上面的意思是上下文。但是在實際的開發中,我們可以把它理解成為一個標記。通常是為了在類和類的子類中同時對一個屬性進行監聽時,為了區分兩個監聽,則可以在context中傳入一個標識"person_name"。如果不需要傳入值,則傳入NULL即可。

那麼用context有什麼好處呢?在我們接收屬性變化的回撥的時候,同時會拿到相應的keyPathobjectchangecontext。如果去判斷keyPath的話,我們需要判斷快取列表,還要判斷類的列表,才能找到相應的屬性值。但是context可以看做是一個靜態的值放在記憶體中,所以用context會讓效能提升不少。


2、監聽回撥

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {    
    NSLog(@"%@",change);
}複製程式碼

3、移除觀察者

- (void)dealloc {    
    [self.person removeObserver:self forKeyPath:@"name"];
}複製程式碼

那麼我們可能要思考為什麼要移除觀察者呢?我們再檢視了官方文件之後,就能清楚的看見:

When removing an observer, keep several points in mind:

  • Asking to be removed as an observer if not already registered as one results in an NSRangeException. You either callremoveObserver:forKeyPath:context: exactly once for the corresponding call to addObserver:forKeyPath:options:context:, or if that is not feasible in your app, place the removeObserver:forKeyPath:context: call inside a try/catch block to process the potential exception.

  • An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.

  • The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in init or viewDidLoad) and unregister during deallocation (usually in dealloc), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.

解除分配時,觀察者不會自動刪除自身。被觀察物件繼續傳送通知,無視觀察者的狀態。但是傳送到已釋出物件的更改通知與任何其他訊息一樣,會去出發記憶體訪問異常。因此,要確保觀察者在從記憶體中消失之前將其移除。

KVO的監聽

1、自動監聽屬性值

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {    
    if ([key isEqualToString:@"name"]) {        
        return YES;    
    }    
    return NO;
}複製程式碼

2、手動觀察屬性值

- (void)setName:(NSString *)name {    
    [self willChangeValueForKey:@"name"];
    _name = name;    
    [self didChangeValueForKey:@"name"];
}複製程式碼

KVO原理分析

在檢視了官方文件之後,發現KVO的原理其實是改變了isa指標,將isa指標指向了另一個動態生成的類。為了分析其中的原理,我們在下圖地方做一個斷點,看看到底是生成了什麼樣的類。

深入理解KVO

然後我們用LLDB列印一下相應的類。

深入理解KVO

我們發現出來了一個新的類NSKVONotifing_Person。那麼這個新的類跟Person類是什麼關係呢?於是決定列印新增了observer前後類到底有什麼變化。

呼叫printClasses方法可以列印所有的子類的資訊:

#pragma mark - ======== 遍歷類以及子類 ========
- (void)printClasses:(Class)cls {    
    //註冊類的總和    
    int count = objc_getClassList(NULL, 0);    
    //建立一個陣列,其中包含給定的物件    
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];    
    //獲取所有已註冊的類    
    Class *classes = (Class *)malloc(sizeof(Class) * count);    
    objc_getClassList(classes, count);    
    for (int i = 0; i < count; i++) {        
        if (cls == class_getSuperclass(classes[i])) {            
        [mArray addObject:classes[i]];        
        }    
    }    
    free(classes);   
    NSLog(@"classes = %@",mArray);
}複製程式碼


然後我們再呼叫前和呼叫後分別呼叫方法:

    [self printClasses:[Person class]];    
    NSLog(@"*********新增前*********");    
    //[self printClasses:NSClassFromString(@"NSKOVNotifing_Person")];        
    self.person = [[Person alloc]init];    
    self.person.name = @"zy";    
    //註冊觀察者    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];    
    NSLog(@"*********進去了*********");    
    [self printClasses:[Person class]];    
    NSLog(@"*********新增後:NSKVONotifying_Person*********");    
    [self printClasses:NSClassFromString(@"NSKVONotifying_Person")];複製程式碼

列印出來的值為

2019-04-14 20:27:48.150739+0800 KVODemo[1959:134800] classes = (

Person

)

2019-04-14 20:27:48.150926+0800 KVODemo[1959:134800] *********新增前*********

2019-04-14 20:27:48.151333+0800 KVODemo[1959:134800] *********進去了*********

2019-04-14 20:27:48.157911+0800 KVODemo[1959:134800] classes = (

Person,

"NSKVONotifying_Person"

)

2019-04-14 20:27:48.158066+0800 KVODemo[1959:134800] *********新增後:NSKVONotifying_Person*********

2019-04-14 20:27:48.162250+0800 KVODemo[1959:134800] classes = (

"NSKVONotifying_Person"

)

那麼我們可以明確的看到,NSKVONotifying_Person是繼承與Person的,他是動態生成的Person的子類。

弄清楚了類的關係,我們再看看方法有什麼變化。我們新增一個遍歷所有方法的函式:

#pragma mark - ======== 遍歷方法-ivar-property ========
- (void)printClassAllMethod:(Class)cls {    
    unsigned int count = 0;    
    Method *methodList = class_copyMethodList(cls, &count);    
    for (int i = 0; i < count; i++) {        
        Method method = methodList[i];        
        SEL sel = method_getName(method);        
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}複製程式碼

然後在新增前後分別呼叫printClassAllMethod方法,列印的結果為:

2019-04-14 20:44:43.451245+0800 KVODemo[2131:144186] hello-0x10a759290

2019-04-14 20:44:43.451427+0800 KVODemo[2131:144186] world-0x10a7592c0

2019-04-14 20:44:43.451529+0800 KVODemo[2131:144186] nick-0x10a759320

2019-04-14 20:44:43.451633+0800 KVODemo[2131:144186] setNick:-0x10a759350

2019-04-14 20:44:43.451738+0800 KVODemo[2131:144186] .cxx_destruct-0x10a759390

2019-04-14 20:44:43.451879+0800 KVODemo[2131:144186] name-0x10a7592f0

2019-04-14 20:44:43.451963+0800 KVODemo[2131:144186] setName:-0x10a7591f0

2019-04-14 20:44:43.452100+0800 KVODemo[2131:144186] *********新增前*********

2019-04-14 20:44:43.452582+0800 KVODemo[2131:144186] *********進去了*********

2019-04-14 20:44:43.452672+0800 KVODemo[2131:144186] *********新增後:NSKVONotifying_Person*********

2019-04-14 20:44:43.452786+0800 KVODemo[2131:144186] setName:-0x10aab263a

2019-04-14 20:44:43.452884+0800 KVODemo[2131:144186] class-0x10aab106e

2019-04-14 20:44:43.452968+0800 KVODemo[2131:144186] dealloc-0x10aab0e12

2019-04-14 20:44:43.453067+0800 KVODemo[2131:144186] _isKVOA-0x10aab0e0a


那麼,通過對方法的地址分析,我們可以得到一個結論,NSKVONotifying_Person類重寫了setName的方法,然後新增了class方法、dealloc方法和_isKVOA方法。

結論

綜合上面的測試,我們可以總結出來KVO的原理:

  1. 驗證是否存在setter方法,目的是為了不讓例項進來
  2. 動態生成子類NSKVONotifying_Person:先開闢一個新的類,然後註冊類,重寫class的方法,講class指向Person,接著重寫setter方法,通過對setter賦值,實現父類的方法self.name = @"xlh",最後通過objc_getAssociatedObject關聯住我們的觀察者
  3. 講isa的指標指向NSKVONotifying_Person
  4. 最後通過訊息轉發響應響應的回撥