上一篇:iOS-KVC淺談
前言:KVO 作為 KVC 的同袍兄弟拂玻,功能更強(qiáng)大,聊聊 KVO。
一策治、KVO 簡介
1.1 KVO 概述
1.KVO 是鍵值觀察者(key-value-observing)首装。
2.KVO提供了一種觀察者的機(jī)制创夜,通過對某個(gè)對象的某個(gè)屬性添加觀察者,當(dāng)該屬性改變仙逻,就會調(diào)用"observeValueForKeyPath:"方法驰吓,為我們提供一個(gè)“對象值改變了!”的時(shí)機(jī)進(jìn)行一些操作系奉。
3.KVO 是一個(gè)觀察者模式檬贰。觀察一個(gè)對象的屬性,注冊一個(gè)指定的路徑缺亮,若這個(gè)對象的的屬性被修改翁涤,則 KVO 會自動(dòng)通知觀察者。
4.基本思想:一個(gè)目標(biāo)對象管理所有依賴于它的觀察者對象萌踱,并在它自身的狀態(tài)改變時(shí)主動(dòng)通知觀察者對象葵礼。這個(gè)主動(dòng)通知通常是通過調(diào)用各觀察者對象所提供的接口方法來實(shí)現(xiàn)的。觀察者模式較完美地將目標(biāo)對象與觀察者對象解耦并鸵。
5.任何對象都允許觀察其他對象的屬性鸳粉,并且可以接收其他對象狀態(tài)變化的通知。
6.Objective-C 中有兩種使用鍵值觀察的方式:手動(dòng)或自動(dòng)园担,此外還支持注冊依賴鍵(即一個(gè)鍵依賴于其他鍵届谈,其他鍵的變化也會作用到該鍵)枯夜。
二、KVO 應(yīng)用
2.1 基本使用
- 注冊觀察者疼约,實(shí)施監(jiān)聽卤档;
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
- 回調(diào)方法,在這里處理屬性發(fā)生的變化程剥;
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
- 移除觀察者劝枣;
[self removeObserver:self forKeyPath:@"age"];
代碼示例
_person = [[Person alloc] init];
/**
* 添加觀察者
*
* @param observer 觀察者
* @param keyPath 被觀察的屬性名稱
* @param options 觀察屬性的新值、舊值等的一些配置(枚舉值织鲸,可以根據(jù)需要設(shè)置舔腾,例如這里可以使用兩項(xiàng))
* 注: options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld 返回未改變之前的值和改變之后的值 context可以為空
* @param context 上下文,可以為nil搂擦。
*/
[_person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
/**
* KVO回調(diào)方法
*
* @param keyPath 被修改的屬性
* @param object 被修改的屬性所屬對象
* @param change 屬性改變情況(新舊值)
* @param context context傳過來的值
*/
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
{
NSLog(@"%@對象的%@屬性改變了:%@",object,keyPath,change);
}
/**
* 移除觀察者
*/
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"age"];
}
2. KVO的使用場景
KVO用于監(jiān)聽對象屬性的改變稳诚。
(1)下拉刷新瀑踢、下拉加載監(jiān)聽UIScrollView的contentoffsize扳还;
(2)webview混排監(jiān)聽contentsize橱夭;
“本唷(3)監(jiān)聽模型屬性實(shí)時(shí)更新UI;
〖印(4)監(jiān)聽控制器frame改變俏让,實(shí)現(xiàn)抽屜效果。
監(jiān)聽 ScrollView 的 contentOffSet 屬性:
[scrollview addObserver:self
forKeyPath:@"contentOffset"
options:NSKeyValueObservingOptionNew
context:nil];
三茬暇、鍵值觀察
3.1 運(yùn)用鍵值觀察
1.注冊與解除注冊
如果我們已經(jīng)有了包含可供鍵值觀察屬性的類首昔,那么就可以通過在該類的對象(被觀察對象)上調(diào)用名為 NSKeyValueObserverRegistration 的 category 方法將觀察者對象與被觀察者對象注冊與解除注冊:
- Foundation/NSKeyValueObserving.h 中,NSObject糙俗,NSArray勒奇,NSSet均實(shí)現(xiàn)了以下方法,因此我們不僅可以觀察普通對象巧骚,還可以觀察數(shù)組或結(jié)合類對象撬陵。
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- NSObject 還實(shí)現(xiàn)了 NSKeyValueObserverNotification 的 category 方法:
這兩個(gè)方法在手動(dòng)實(shí)現(xiàn)鍵值觀察時(shí)會用到
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
- 注:不要忘記解除注冊,否則會導(dǎo)致資源泄露网缝。
2.設(shè)置屬性
將觀察者與被觀察者注冊好之后,就可以對觀察者對象的屬性進(jìn)行操作蟋定,這些變更操作就會被通知給觀察者對象粉臊。注意,只有遵循 KVO 方式來設(shè)置屬性驶兜,觀察者對象才會獲取通知扼仲,也就是說遵循使用屬性的 setter 方法远寸,或通過key-path
來設(shè)置:
[target setAge:30];
[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];
3.處理變更通知
觀察者需要實(shí)現(xiàn)名為 NSKeyValueObserving
的 category 方法來處理收到的變更通知:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
在這里,change 這個(gè)字典保存了變更信息屠凶,具體是哪些信息取決于注冊時(shí)的 NSKeyValueObservingOptions驰后。
5.代碼示例
在實(shí)現(xiàn)處理變更通知方法observeValueForKeyPath
時(shí),要將不能處理的 key 轉(zhuǎn)發(fā)給 super 的 observeValueForKeyPath
來處理矗愧。
// 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
調(diào)用示例
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"];
輸出結(jié)果:
class: Target, Age changed
old age is 10
new age is 30
3.2 手動(dòng)實(shí)現(xiàn)鍵值觀察
首先灶芝,需要手動(dòng)實(shí)現(xiàn)屬性的 setter 方法,并在設(shè)置操作的前后分別調(diào)用 willChangeValueForKey: 和 didChangeValueForKey方法唉韭,這兩個(gè)方法用于通知系統(tǒng)該 key 的屬性值即將和已經(jīng)變更了夜涕;
其次,要實(shí)現(xiàn)類方法 automaticallyNotifiesObserversForKey属愤,并在其中設(shè)置對該 key 不自動(dòng)發(fā)送通知(返回 NO 即可)女器。這里要注意,對其它非手動(dòng)實(shí)現(xiàn)的 key住诸,要轉(zhuǎn)交給 super 來處理驾胆。
@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
3.3 自動(dòng)實(shí)現(xiàn)鍵值觀察
自動(dòng)實(shí)現(xiàn)鍵值觀察就非常簡單了,只要使用了自動(dòng)屬性即可贱呐。
@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
3.4 鍵值觀察依賴鍵
有時(shí)候一個(gè)屬性的值依賴于另一對象中的一個(gè)或多個(gè)屬性丧诺,如果這些屬性中任一屬性的值發(fā)生變更,被依賴的屬性值也應(yīng)當(dāng)為其變更進(jìn)行標(biāo)記吼句。因此锅必,object 引入了依賴鍵。
1. 觀察依賴鍵
觀察依賴鍵的方式與前面描述的一樣惕艳,下面先在 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];
}
}
2.實(shí)現(xiàn)依賴鍵
在這里搞隐,觀察的是 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 中的依賴鍵屬性是實(shí)現(xiàn):
@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
- 首先,要手動(dòng)實(shí)現(xiàn)屬性 information 的 setter/getter 方法谁鳍,在其中使用 Target 的屬性來完成其 setter 和 getter癞季。
- 其次,要實(shí)現(xiàn) keyPathsForValuesAffectingInformation 或 keyPathsForValuesAffectingValueForKey: 方法來告訴系統(tǒng) information 屬性依賴于哪些其他屬性倘潜,這兩個(gè)方法都返回一個(gè)key-path 的集合绷柒。如果選擇實(shí)現(xiàn) keyPathsForValuesAffectingValueForKey,要先獲取 super 返回的結(jié)果 set涮因,然后判斷 key 是不是目標(biāo) key废睦,如果是就將依賴屬性的 key-path 結(jié)合追加到 super 返回的結(jié)果 set 中,否則直接返回 super的結(jié)果养泡。
- information 屬性依賴于 target 的 age 和 grade 屬性嗜湃,target 的 age/grade 屬性任一發(fā)生變化奈应,information 的觀察者都會得到通知。
3.使用示例
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"];
輸出結(jié)果:
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
四购披、KVO 原理
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
Theisa
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 theisa
pointer to determine class membership. Instead, you should use the[class](http://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Protocols/NSObject_Protocol/Reference/NSObject.html#//apple_ref/occ/intfm/NSObject/class)
method to determine the class of an object instance.
- 當(dāng)某個(gè)類的對象第一次被觀察時(shí)杖挣,系統(tǒng)就會在運(yùn)行期動(dòng)態(tài)地創(chuàng)建該類的一個(gè)派生類
- 在這個(gè)派生類中重寫基類中任何被觀察屬性的 setter 方法。
- 派生類在被重寫的 setter 方法實(shí)現(xiàn)真正的通知機(jī)制刚陡,就如前面手動(dòng)實(shí)現(xiàn)鍵值觀察那樣惩妇。
- 基于設(shè)置屬性會調(diào)用 setter 方法,而通過重寫就獲得了 KVO 需要的通知機(jī)制橘荠。
- 前提是要通過遵循 KVO 的屬性設(shè)置方式來變更屬性值屿附,如果僅是直接修改屬性對應(yīng)的成員變量,是無法實(shí)現(xiàn) KVO 的哥童。
- 同時(shí)派生類還重寫了 class 方法以“欺騙”外部調(diào)用者它就是起初的那個(gè)類挺份。
- 系統(tǒng)將這個(gè)對象的 isa 指針指向這個(gè)新誕生的派生類,因此這個(gè)對象就成為該派生類的對象了贮懈,因而在該對象上對 setter 的調(diào)用就會調(diào)用重寫的 setter匀泊,從而激活鍵值通知機(jī)制。
- 派生類還重寫了 dealloc 方法來釋放資源朵你。
- 當(dāng)一個(gè)觀察者注冊對象的一個(gè)屬性 isa 觀察對象的指針被修改各聘,指著一個(gè)中間類而不是在真正的類。
- isa 指針的作用:每個(gè)對象都有 isa 指針抡医,指向該對象的類躲因,它告訴 runtime 系統(tǒng)這個(gè)對象的類是什么。
4.1 派生類 NSKVONotifying_Person 剖析:
在這個(gè)過程忌傻,被觀察對象的 isa 指針從指向原來的 Person 類大脉,被 KVO 機(jī)制修改為指向系統(tǒng)新創(chuàng)建的子類 NSKVONotifying_Person 類,來實(shí)現(xiàn)當(dāng)前類屬性值改變的監(jiān)聽水孩。
所以當(dāng)我們從應(yīng)用層面上看來镰矿,完全沒有意識到有新的類出現(xiàn),這是系統(tǒng)“隱瞞”了對 KVO 的底層實(shí)現(xiàn)過程俘种,讓我們誤以為還是原來的類秤标。但是此時(shí)如果我們創(chuàng)建一個(gè)新的名為 NSKVONotifying_Person 的類(),就會發(fā)現(xiàn)系統(tǒng)運(yùn)行到注冊 KVO 的那段代碼時(shí)程序就崩潰宙刘,因?yàn)橄到y(tǒng)在注冊監(jiān)聽的時(shí)候動(dòng)態(tài)創(chuàng)建了名為 NSKVONotifying_Person 的中間類辜腺,并指向這個(gè)中間類了秋忙。
因而在該對象上對 setter 的調(diào)用就會調(diào)用已重寫的 setter容达,從而激活鍵值通知機(jī)制蹂随。這也是 KVO 回調(diào)機(jī)制,為什么都俗稱 KVO 技術(shù)為黑魔法的原因之一吧:內(nèi)部神秘、外觀簡潔屈嗤。
4.2 子類 setter 方法剖析:
1.KVO 在調(diào)用存取方法之前總是調(diào)用 willChangeValueForKey:
,通知系統(tǒng)該 keyPath 的屬性值即將變更吊输。
2.當(dāng)改變發(fā)生后饶号,didChangeValueForKey:
被調(diào)用,通知系統(tǒng)該 keyPath 的屬性值已經(jīng)變更季蚂。
3.之后茫船,observeValueForKey:ofObject:change:context:
也會被調(diào)用。
重寫觀察屬性的 setter 方法這種方式是在運(yùn)行時(shí)而不是編譯時(shí)實(shí)現(xiàn)的扭屁。 KVO 為子類的觀察者屬性重寫調(diào)用存取方法的工作原理在代碼中相當(dāng)于:
- (void)setName:(NSString *)newName
{
[self willChangeValueForKey:@"name"]; // KVO在調(diào)用存取方法之前總調(diào)用
[super setValue:newName forKey:@"name"]; // 調(diào)用父類的存取方法
[self didChangeValueForKey:@"name"]; // KVO在調(diào)用存取方法之后總調(diào)用
}
總結(jié):
KVO 的本質(zhì)就是監(jiān)聽對象的屬性進(jìn)行賦值的時(shí)候有沒有調(diào)用 setter 方法
- 系統(tǒng)會動(dòng)態(tài)創(chuàng)建一個(gè)繼承于 Person 的 NSKVONotifying_Person
- person 的 isa 指針指向的類 Person 變成 NSKVONotifying_Person算谈,所以接下來的 person.age = newAge 的時(shí)候,他調(diào)用的不是 Person 的 setter 方法料滥,而是 NSKVONotifying_Person(子類)的 setter 方法
- 重寫NSKVONotifying_Person的setter方法:[super setName:newName]
- 通知觀察者告訴屬性改變然眼。
補(bǔ)充
- Apple 使用了 isa 混寫(isa-swizzling)來實(shí)現(xiàn)KVO
- 使用setter方法改變值 KVO生效
- 使用setValue:forKey: 改變值 KVO生效
- 成員變量直接修改值 KVO失效,必須手動(dòng)添加方法才生效
- 系統(tǒng)利用運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建一個(gè) NSKVONotifying_Person的子類葵腹,改寫isa指針的指向高每,并重寫子類的setter方法