KVO概念
KVO ->Key-Value observing吝羞,鍵值觀察,當(dāng)被觀察對象中指定屬性發(fā)現(xiàn)變化時仔掸,觀察者就可以得到通知脆贵,進而進行后續(xù)操作。
KVO使用
根據(jù)KVO官方文檔 得知起暮,正常使用大體分為以下流程:
- 注冊觀察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
- KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"%@",change[NSKeyValueChangeNewKey]);
}
}
- 移除觀察者
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
context 用法及意義
根據(jù)官方文檔解讀可以得知:
context
即上下文
主要作用就是防止根據(jù)keypath
查詢通知來源
時卖氨,因父類
、子類
觀察到相同路徑
而出現(xiàn)的問題负懦,不同的keypath
創(chuàng)建不同的context
筒捺,這樣就可以不用通過字符串比較
的方式去確定keypath
,從而提高性能以及代碼可讀性
纸厉。
//定義context
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
//注冊觀察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
//KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNickContext) {
NSLog(@"%@",change);
}else if (context == PersonNameContext){
NSLog(@"%@",change);
}
}
觀察者移除
根據(jù)官方文檔可以得知:
如果沒有注冊而移除了觀察者系吭,時就會出現(xiàn)
NSRangeException
,如果進行了一次addObserver
颗品,則對應(yīng)的也需要removeObserver
釋放后肯尺,觀察者不會自動將其自身移除。被觀察對象繼續(xù)發(fā)送通知躯枢,而忽略了觀察者的狀態(tài)则吟。但是,與發(fā)送到已釋放對象的任何其他消息一樣锄蹂,更改通知會觸發(fā)內(nèi)存訪問異常氓仲。因此,您可以確保觀察者在從內(nèi)存中消失之前將自己刪除得糜。
該協(xié)議無法詢問對象是觀察者還是被觀察者敬扛。構(gòu)造代碼以避免發(fā)布相關(guān)的錯誤。一種典型的模式是在觀察者初始化期間(例如朝抖,在
init或viewDidLoad
中)注冊為觀察者啥箭,并在釋放過程中(通常在dealloc
中)注銷,以確保成對和有序地添加和刪除消息治宣,并確保觀察者在注冊之前被取消注冊捉蚤,從內(nèi)存中釋放出來
總結(jié):
KVO
注冊觀察者(addObserver
)與移除觀察者(removeObserver
)是成對出現(xiàn)的抬驴,如果只注冊炼七,不移除
缆巧,則會出現(xiàn)野指針
類型的崩潰,如果只移除豌拙,不注冊
陕悬,則會NSRangeException
KVO 的自動與手動觸發(fā)
系統(tǒng)默認的事自動觸發(fā)
,即如果添加了觀察者
按傅,并且回調(diào)方法
中存在相應(yīng)的處理捉超,這時只要屬性值發(fā)生改變
,就會調(diào)用唯绍,如果關(guān)閉
自動觸發(fā)automaticallyNotifiesObserversForKey設(shè)置為NO
拼岳,這時就需要將需要觀察的屬性改變前增加willChangeValueForKey
,改變后增加didChangeValueForKey
况芒,這樣就可以觸發(fā)惜纸,通過手動觸發(fā)能夠更好地貼合項目中的需求,增加擴展性
KVO一對多
通過keyPathsForValuesAffectingValueForKey
方法將多個屬性合并成一個進行觀察绝骚,以下載進度為例
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
self.person.writtenData += 10;
self.person.totalData += 1;
[self.person removeObserver:self forKeyPath:@"downloadProgress"];
- (NSString *)downloadProgress{
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
KVO 鍵值變化類型
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//設(shè)值
NSKeyValueChangeInsertion = 2,//插入
NSKeyValueChangeRemoval = 3,//移除
NSKeyValueChangeReplacement = 4,//替換
};
//根據(jù)下面代碼來實現(xiàn)不同的kind
self.student.name = [NSString stringWithFormat:@"%@+",self.student.name];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
[[self.person mutableArrayValueForKey:@"dateArray"] removeLastObject];
[[self.person mutableArrayValueForKey:@"dateArray"] replaceObjectAtIndex:0 withObject:@"3"];
- 注:當(dāng)當(dāng)前觀察對象為
數(shù)組
時耐版,不可以直接通過賦值的方式進行更改,而是需要mutableArrayValueForKey
或mutableArrayValueForKeyPath
進行獲取压汪,然后才能實現(xiàn)更改回調(diào)
KVO 底層探究
由于KVO沒有對應(yīng)的開源代碼粪牲,故而通過跟流程的方式查看
- 觀察的是setter方法
驗證:根據(jù)屬性
和成員變量
的區(qū)別可以得知,兩者之間屬性存在setter
方法止剖,而成員變量沒有
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"實際情況:%@-%@",self.person.nickName,self.person->name);
self.person.nickName = @"KC";
self.person->name = @"Cooci";
}
#pragma mark - KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
根據(jù)上述結(jié)果可以得知腺阳,屬性
nickName
更改成功收到回調(diào),而成員變量name
沒有收到回調(diào)穿香,故而可以驗證KVO
觀察的是setter
方法
- 中間類
根據(jù)官方文檔得知亭引,在注冊觀察者之后,觀察對象的isa
會發(fā)生改變
根據(jù)斷點獲取className
可以看出isa的指向
確實發(fā)生了改變
[self printClasses:[LGPerson class]];
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
// [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClasses:[LGPerson class]];
[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];
[self printClassAllMethod:[LGStudent class]];
#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);
}
#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
// 注冊類的總數(shù)
int count = objc_getClassList(NULL, 0);
// 創(chuàng)建一個數(shù)組扔水, 其中包含給定對象
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);
}
通過遍歷方法和子類可以看出
- 在沒有注冊觀察值之前痛侍,只存在
LGPerson
與LGStudent
兩個類,而注冊完成后魔市,多出了一個NSKVONotifying_LGPerson
主届,故而可以確定,NSKVONotifying_LGPerson
是LGPerson
的子類 - 通過觀察
NSKVONotifying_LGPerson
中的方法列表可以看出待德,子類中存在setter
方法君丁,在LGStudent
中實現(xiàn)了setNickName
的重寫,與NSKVONotifying_LGPerson
中一致将宪,故而可以確定绘闷,在NSKVONotifying_LGPerson
實現(xiàn)了setter橡庞、class、dealloc印蔗、_isKVO
的重寫
扒最,而非繼承
- 根據(jù)上圖可以得知,在移除觀察者之前isa指向的是子類
NSKVONotifying_LGPerson
华嘹,移除之后指回LGPerson
- 雖然觀察者移除了吧趣,但是在其它頁面查看
LGPerson
子類時可以發(fā)現(xiàn)中間類NSKVONotifying_LGPerson
并沒有銷毀
,這樣可以避免每次進行創(chuàng)建而造成性能低下耙厚,通過重用的方式使得中間類一經(jīng)創(chuàng)建就一直存在
KVO自定義實現(xiàn)
完整代碼只是基本實現(xiàn)