從幾個面試題出發:
1.KVO的底層是如何實現的?
2.addObserver:forKeyPath:options:context:的context有什麼用?
3.直接修改爲員變量會觸發KVO嗎?
4.咱們知道KVC會修改爲員變量,那麼它會觸發KVO嗎?
5.如何監聽可變數組的內容修改?
複製代碼
看到上述問題,你有答案了嗎?若是你有疑惑,帶着疑問咱們開啓一段KVO的探索之旅。 web
KVO
全稱Key-Value Observing
,是蘋果提供的一套事件通知機制,容許一個對象在其餘對象的指定屬性發生更改時獲得通知的機制。面試
你們都瞭解KVO
的基本使用方法,無非就是添加觀察者、接收通知和移除觀察者,下面咱們經過一個簡單的Demo來了解一下具體的實現。數組
#import "ViewController.h"
#import "SSBoy.h"
#import "SSGirl.h"
#import <objc/runtime.h>
@interface ViewController ()
@property (nonatomic, strong) SSBoy *boy;
@property (nonatomic, strong) SSGirl *girl;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.boy = [SSBoy new];
self.girl = [SSGirl new];
// 添加觀察者
[self.boy addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.boy addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];
[self.girl addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 修改相應的值
self.boy.name = @"sanliangsan";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
// 接收通知回調
if ([object isEqual:self.boy]) {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"boy change");
}
} else {
NSLog(@"girl change");
}
}
@end
複製代碼
上述代碼清晰標註了KVO
的簡單實現,不過這份簡單代碼有一些問題哦?那麼問題在哪呢?安全
1.boy
和girl
同時觀察了相同的name
屬性,咱們的observeValueForKeyPath
方法的接收中多層的嵌套判斷比較複雜,並且還容易出錯,這就引出了上述關於context
的面試題,咱們引用一段官方文檔:app
You may specify NULL and rely entirely on the key path string
to determine the origin of a change notification, but this
approach may cause problems for an object whose superclass is
also observing the same key path for different reasons.
A safer and more extensible approach is to use the context to
ensure notifications you receive are destined for your
observer and not a superclass
複製代碼
咱們能夠指定NULL
做爲context
,可是這樣會由於一些不一樣的緣由致使對象的父類也同時會觀察相同的屬性key
,使用context
能夠更安全以及更具備擴展性。同時也告知了咱們context
若是爲空應該是用NULL
而非nil
。編輯器
Context
具體如何使用呢ide
// 定義context
static void *BoyNameContext = &BoyNameContext; // 添加觀察者
[self.boy addObserver:self forKeyPath:@"name"options:NSKeyValueObservingOptionNew context:BoyNameContext];
// 接收通知回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == BoyNameContext) {
// do....
}
}
複製代碼
Context
你會用了嗎?聰明的你可能還發現了最上面的簡單例子還有一個致命的問題?post
正是:咱們沒有對相應觀察者進行移除,在咱們的觀察者釋放的時候咱們要移除相應觀察。測試
- (void)dealloc {
[self.boy removeObserver:self forKeyPath:@"name" context:BoyNameContext];
}
複製代碼
若是觀察的對象是一個單例,而他在幾個不一樣的場景都有觀察一樣的屬性,那麼在某個場景消失的時候別的地方觸發屬性修改就會致使單例去尋找已經釋放的對象,就是野指針的狀況。具體的實現還請各位本身去測試。到此KVO
的簡單實現你會了嗎?ui
接下來咱們看看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.
You should never rely on the isa pointer to determine class
membership. Instead, you should use the class method to
determine the class of an object instance.
複製代碼
KVO
底層的實現是運用了一項 isa-swizzling
的技術,當咱們添加觀察者的時候,系統動態的給咱們的對象建立了一個子類,將對象的isa
指向了動態子類,而KVO
的全部實現都是經過這個動態子類的,添加一個動態子類讓類的職責更單一具體,並且讓咱們的KVO
透明化,建立動態子類的過程咱們是沒法感知的,同時咱們也知道了獲取一個類不能經過isa
的指向而是要看class
的方法返回。多說無益,咱們驗證一下:
仍是一樣的代碼,咱們看到添加觀察者以前,isa
指向SSBoy
,可是添加觀察者以後就指向一個叫NSKVONotifying_SSBoy
的類,這正好驗證了上述文檔所說。
KVO
到底研究的是什麼?
說到屬性,咱們無不和實例變量牽扯到一塊兒,他們之間的區別就是是否有setter
方法,屬性的修改咱們在最初已經驗證過了,如今咱們看看修改實例變量是否會觸發KVO
。
// 實例變量
@interface SSBoy : NSObject
{
@public
NSString *nickName;
}
// 添加觀察者
[self.boy addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:BoyNameContext];
// 修改實例變量
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 修改相應的值
self.boy->nickName = @"sanliangsan";
}
// 沒有響應
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
// 接收通知回調
if (context == BoyNameContext) {
}
}
複製代碼
最終咱們的結果是不會觸發,那麼爲何成員變量的修改不會觸發KVO
,動態子類究竟都幹了啥?
瞭解一個類咱們無非是從屬性,成員變量,方法等去研究,這裏咱們從方法入手,其餘的你們能夠下去一一驗證。
咱們去打印原類和動態子類的全部方法做以對比。
1.NSKVONotifying_SSBoy
中爲啥沒有setAge?
由於沒有針對age添加觀察者,這也證實了KVO的動態中間子類是經過實現setter方法去實現的。
複製代碼
2.NSKVONotifying_SSBoy
中爲啥有class方法?
重寫class方法,由於class指向類自己,【假裝】爲了讓這一層更透明,蘋
果重寫class方法從新指向SSBoy,讓上層對動態子類的生成沒有感知,透明化&隱私化
複製代碼
3.dealloc
方法爲了啥?
最初咱們已經瞭解了,在添加觀察者的時候會動態生成子類,並且對
象的isa會指向動態子類,當動態子類調用dealloc的時候,isa固然會從新指向回原類。
複製代碼
以前的一篇文章 KVC原理與自定義 有講述KVC
底層是如何一步一步實現修改對象的屬性的,那麼問題來了,KVC
會觸發KVO
嗎,仔細閱讀KVO
官方文檔咱們看到一段話:
NSObject provides a basic implementation of automatic ke
y-value change notification. Automatic key-value change no
tification informs observers of changes made using key-val
ue compliant accessors, as well as the key-value coding me
thods. Automatic notification is also supported by the col
lection proxy objects returned by, for example, mutableAr
rayValueForKey:
複製代碼
大概意思就是NSObject
提供了自動key-value
觀察的實現,並且經過setter
方法和key-value coding
方法是同樣的,換言之:key-value coding
也能實現自動KVO
,同時文檔還給出相關能觸發KVO
的實例:
Examples of method calls that cause KVO change notifications to be emitted
// Call the accessor method.
[account setName:@"Savings"];
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
複製代碼
經過以上咱們得知:KVC
是能自動實現KVO
的,並且能夠驗證不管是否有某屬性都會自動通知到觀察者。
咱們在某些狀況下想對一個數組進行觀察,添加、刪除,修改等等,可是實際測試發現,普通的方法調用並不會觸發KVO
,其緣由很簡單,利用咱們上述的原理就得以解釋:咱們對數組的各類添加、刪除、修改並不會調用setter
方法,因爲KVC
會觸發KVO
咱們在KVC
裏邊找到相關的方法得以實現:
[[_arrayModel mutableArrayValueForKeyPath:@"dataArray"] addObject:XXX];
[[_arrayModel mutableArrayValueForKeyPath:@"dataArray"] removeObject:XXX];
複製代碼
至此,咱們對KVO
的簡單使用以及原理分析已經完結,那些面試題的答案你都知曉了嗎?實際的使用過程當中咱們還會碰到更多的問題,期待你的交流和溝通。