上一節(jié),我們介紹了KVC原理,而KVC的工作,絕大部分通過(guò)繼承NSObject自動(dòng)處理好了。實(shí)際應(yīng)用中,我們關(guān)注得相對(duì)較少。而基于KVC的KVO,在應(yīng)用中卻是非常的廣泛。
現(xiàn)在我們使用的
響應(yīng)式框架(RAC、RxSwif、Combine等),實(shí)際都是KVO機(jī)制的應(yīng)用。
本節(jié),我們?cè)敿?xì)講解KVO:
- KVO介紹
- KVO應(yīng)用
- KVO原理
引入:
- 我們上一節(jié)
分析KVC時(shí),官方對(duì)KVC的應(yīng)用中,第一個(gè)介紹的就是KVO:
?? KVC文檔鏈接
image.png我們點(diǎn)擊進(jìn)入Key-Value Observing Programming Guide (KVO指引)
1. KVO介紹
KVO,全稱(chēng)為Key-value observing鍵值觀察。
-
鍵值觀察是一種機(jī)制,允許對(duì)象在其他對(duì)象的指定屬性發(fā)生更改時(shí)得到通知。
2 KVO應(yīng)用:
- 測(cè)試代碼:(
監(jiān)聽(tīng)person對(duì)象的name屬性的新值)
// HTPerson
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation HTPerson
@end
// ViewController
@interface ViewController ()
@property (nonatomic, strong) HTPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
self.person.name = @"ht";
// 1. 添加
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
}
// 2. 監(jiān)聽(tīng)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString: @"name"]) {
NSLog(@"新值:%@", change[NSKeyValueChangeNewKey]);
}
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = [NSString stringWithFormat:@"%@ +",self.person.name];
}
-(void)dealloc {
// 3. 移除
[self.person removeObserver:self forKeyPath:@"name" context: NULL];
}
@end
- 這里為了測(cè)試,在
touchesBegan點(diǎn)擊事件中添加了name的變更,多次點(diǎn)擊,打印結(jié)果如下:
image.png
主要步驟: 1. 添加 -> 2. 監(jiān)聽(tīng) ->3. 移除
2.1 添加
-
addObserver 添加操作中,addObserver是監(jiān)聽(tīng)對(duì)象,KeyPath是監(jiān)聽(tīng)路徑,option是監(jiān)聽(tīng)類(lèi)型,是一個(gè)枚舉,包含:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew // 新值
NSKeyValueObservingOptionOld // 舊值
NSKeyValueObservingOptionInitial // 初始值
NSKeyValueObservingOptionPrior // 變化前
};
面試官:
添加通知時(shí),context寫(xiě)什么內(nèi)容?
答:填nil
面試官:回去等通知 ??
- 關(guān)于
context的介紹:

-
照顧英語(yǔ)不好的同學(xué),我們放上
谷歌翻譯:
image.png context的類(lèi)型為(void *),所以不能寫(xiě)nil。但可以寫(xiě)成Null。
如果寫(xiě)Null,會(huì)默認(rèn)通過(guò)KeyPath路徑去確定需要監(jiān)聽(tīng)的對(duì)象。但是這種方法可能導(dǎo)致父類(lèi)由于不同原因觀察到相同的路徑,而產(chǎn)生問(wèn)題。而且查詢(xún)到父類(lèi),消耗的計(jì)算資源更多。
所以蘋(píng)果建議我們可以static void*創(chuàng)建靜態(tài)的context,這樣的好處是:
- 僅從本類(lèi)中查找當(dāng)前
context,節(jié)省計(jì)算資源,更安全;
- 僅從本類(lèi)中查找當(dāng)前
-
observeValueForKeyPath監(jiān)聽(tīng)對(duì)象時(shí),我們可以不再通過(guò)name去區(qū)分當(dāng)前響應(yīng)對(duì)象。而是使用context精準(zhǔn)區(qū)分當(dāng)前響應(yīng)對(duì)象:
image.png
-
-
removeObserver移除對(duì)象時(shí),可以通過(guò)context精準(zhǔn)移除觀察對(duì)象:
image.png
-
2.2 監(jiān)聽(tīng)
-
observeValueForKeyPath 監(jiān)聽(tīng)當(dāng)前控制器的所有變化,change有以下4種情況:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
-
keyPath、object、context和上述一樣。
2.3 移除
一定要移除! 一定要移除! 一定要移除!
網(wǎng)上有很多說(shuō)
Xcode升級(jí)后,不再需要手動(dòng)移除監(jiān)聽(tīng)者。僅僅在當(dāng)前頁(yè)面操作時(shí),確實(shí)不用處理。
- 但如果業(yè)務(wù)變得復(fù)雜,對(duì)于
同一對(duì)象屬性,如果當(dāng)前頁(yè)面進(jìn)行了添加、監(jiān)聽(tīng)和移除,而其他頁(yè)面只進(jìn)行添加和監(jiān)聽(tīng),再觸發(fā)監(jiān)聽(tīng)時(shí),就會(huì)產(chǎn)生KVO Crash。所以我們要養(yǎng)成誰(shuí)使用誰(shuí)銷(xiāo)毀的習(xí)慣。
2.4 開(kāi)關(guān)
-
automaticallyNotifiesObserversForKey控制自動(dòng)和手動(dòng)發(fā)送通知,默認(rèn)值為自動(dòng)( ?? 官方介紹)

- 當(dāng)我們給
HTPerson添加automaticallyNotifiesObserversForKey方法,返回值為NO后,所有監(jiān)聽(tīng)消息都不再發(fā)送。 - 我們
重寫(xiě)屬性setter方法,手動(dòng)調(diào)用API進(jìn)行消息發(fā)送:
@implementation HTPerson
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
-(void)setName:(NSString *)name {
if ([self.name isEqualToString: name]) return; // 值沒(méi)變化,不操作
[self willChangeValueForKey:@"name"]; // 即將改變
_name = name; // 賦值
[self didChangeValueForKey:@"name"]; // 已改變
}
@end
2.5 路徑處理
我們已
downloadProgress下載進(jìn)度為例,下載進(jìn)度等于writtenData已下載數(shù)據(jù)量/totalData總數(shù)據(jù)量。我們可以聚合
writtenData和totalData兩個(gè)屬性,變成監(jiān)聽(tīng)downloadProgress一個(gè)屬性。
// HTPerson
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@end
@implementation HTPerson
- (NSString *)downloadProgress {
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [NSString stringWithFormat:@"%.2f", 1.0f * self.writtenData / self.totalData];
}
// 下載進(jìn)入 writtenData / totalData
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray * affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
@end
// ViewController
@interface ViewController ()
@property (nonatomic, strong) HTPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
// 添加監(jiān)聽(tīng)
[self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context: NULL];
}
// 處理監(jiān)聽(tīng)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString: @"downloadProgress"]) {
NSLog(@"當(dāng)前進(jìn)度:%@", change[NSKeyValueChangeNewKey]);
}
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.writtenData += 20;
self.person.totalData +=10;
}
-(void)dealloc {
// 移除監(jiān)聽(tīng)
[self.person removeObserver:self forKeyPath:@"downloadProgress" context: NULL];
}
@end
-
打印結(jié)果:
image.png 這里將
writtenData和totalData都變化了,可以看到每次打印的是2條記錄。說(shuō)明聚合的downloadProgress中,只要writtenData或totalData值變化,都會(huì)觸發(fā)一次。
(如果你用過(guò)RAC或RxSwift,此處一定非常熟悉,這就是RxSwift的merge的原理。)相關(guān)原理: ?? 官方鏈接
2.6 數(shù)組的觀察
-
集合等類(lèi)型的監(jiān)聽(tīng),與屬性的監(jiān)聽(tīng)不同。我們可以查閱KVC的官方文檔:

// HTPerson
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSMutableArray *dateArray;
@end
@implementation HTPerson
@end
// ViewController
@interface ViewController ()
@property (nonatomic, strong) HTPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
self.person.name = @"ht ";
self.person.dateArray = [NSMutableArray new];
// 添加監(jiān)聽(tīng)
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context: NULL];
}
// 處理監(jiān)聽(tīng)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@: %@", keyPath, change);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = [NSString stringWithFormat:@"%@+", self.person.name];
// [self.person.dateArray addObject:@"6"]; //此賦值僅改變數(shù)組內(nèi)部元素,不會(huì)引起數(shù)組地址的變化
[[self.person mutableArrayValueForKeyPath:@"dateArray"] insertObject:@"6" atIndex:0];
[[self.person mutableArrayValueForKeyPath:@"dateArray"] setObject:@"8" atIndexedSubscript:0];
[[self.person mutableArrayValueForKeyPath:@"dateArray"] removeObjectAtIndex:0];
}
-(void)dealloc {
// 移除監(jiān)聽(tīng)
[self.person removeObserver:self forKeyPath:@"name" context: NULL];
[self.person removeObserver:self forKeyPath:@"dateArray" context: NULL];
}
@end
- 打印結(jié)果:

數(shù)組的設(shè)值,必須使用專(zhuān)屬API才可以觸發(fā)。直接賦值僅改變數(shù)組內(nèi)部元素,不會(huì)引起數(shù)組地址的變化。從打印的結(jié)果上,
change有4種情況:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};

3. KVO底層原理
3.1 KVO只觀察Setter方法
- 我們先觀察一個(gè)案例,此案例有
public聲明的nickName成員變量和@property定義的屬性name,分別監(jiān)聽(tīng)這2個(gè)屬性值: - 測(cè)試代碼:
// HTPerson
@interface HTPerson : NSObject
{
@public NSString * nickName;
}
@property (nonatomic, copy) NSString *name;
@end
@implementation HTPerson
@end
// ViewController
@interface ViewController ()
@property (nonatomic, strong) HTPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
self.person.name = @"ht ";
// 添加監(jiān)聽(tīng)
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context: NULL];
}
// 處理監(jiān)聽(tīng)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@: %@", keyPath, change);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = [NSString stringWithFormat:@"%@+", self.person.name];
self.person->nickName = [NSString stringWithFormat:@"%@+", self.person->nickName];
}
-(void)dealloc {
// 移除監(jiān)聽(tīng)
[self.person removeObserver:self forKeyPath:@"name" context: NULL];
[self.person removeObserver:self forKeyPath:@"nickName" context: NULL];
}
@end
-
打印結(jié)果:
image.png 發(fā)現(xiàn)只
監(jiān)聽(tīng)到屬性的變化,而監(jiān)聽(tīng)不到成員變量的變化。而屬性和成員變量的區(qū)別,核心在于是否實(shí)現(xiàn)setter方法。
3.2 KVO派生類(lèi)
-
在
addObserver處打上斷點(diǎn),運(yùn)行到斷點(diǎn)處后,打印當(dāng)前person類(lèi):
image.png 驚奇的發(fā)現(xiàn),使用
運(yùn)行時(shí)object_getClassName讀取的self.person類(lèi),是NSKVONotifying_HTPerson類(lèi)。而使用self.person.class直接打印的類(lèi),確是HTPerson類(lèi)。好像發(fā)現(xiàn)了一些不可告人的密碼 ?? 蘋(píng)果
金屋藏嬌生成了個(gè)NSKVONotifying_HTPerson,但又故意的不讓外部知道,所以調(diào)用class方法,打印的還是HTPerson類(lèi)。當(dāng)然,從開(kāi)發(fā)的層面,可以理解,對(duì)外事務(wù)越簡(jiǎn)單越好,
高內(nèi)聚,減輕了開(kāi)發(fā)人員的學(xué)習(xí)和使用成本。不過(guò)對(duì)于我們現(xiàn)在探究底層原理而言,就想知道這個(gè)NSKVONotifying_HTPerson是什么。
3.2.1 NSKVONotifying_HTPerson與HTPerson什么關(guān)系?
- 導(dǎo)入
#import <objc/runtime.h>,添加打印本類(lèi)和所有子類(lèi)的方法:
/// 遍歷本類(lèi)及子類(lèi)
-(void) printClasses: (Class)cls {
// 注冊(cè)類(lèi)的總數(shù)
int count = objc_getClassList(NULL, 0);
// 創(chuàng)建1個(gè)數(shù)組
NSMutableArray * mArray = [NSMutableArray arrayWithObject:cls];
//獲取所有已注冊(cè)的類(lèi)
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);
}
- 在
addObserver前后前后打印self.person類(lèi):

- 觀察到
添加觀察者后,HTPerson多了一個(gè)NSKVONotifying_HTPerson子類(lèi)。
我們添加遍歷Ivars、Property、Method的函數(shù):
/// 遍歷Ivars
-(void) printIvars: (Class)cls {
// 仿寫(xiě)Ivar結(jié)構(gòu)
typedef struct HT_ivar_t {
int32_t *offset;
const char *name;
const char *type;
uint32_t alignment_raw;
uint32_t size;
}HT_ivar_t;
// 記錄函數(shù)個(gè)數(shù)
unsigned int count = 0;
// 讀取函數(shù)列表
Ivar * ivars = class_copyIvarList(cls, &count);
for (int i = 0; i < count; i++) {
HT_ivar_t * ivar = (HT_ivar_t *) ivars[i];
NSLog(@"ivar: %@", [NSString stringWithUTF8String: ivar->name]);
}
free(ivars);
}
/// 遍歷屬性
-(void) printProperties: (Class)cls {
// 仿寫(xiě)objc_property_t結(jié)構(gòu)
typedef struct Ht_property_t{
const char *name;
const char *attributes;
}Ht_property_t;
// 記錄函數(shù)個(gè)數(shù)
unsigned int count = 0;
// 讀取函數(shù)列表
objc_property_t * props = class_copyPropertyList(cls, &count);
for (int i = 0; i < count; i++) {
Ht_property_t * prop = (Ht_property_t *)props[i];
NSLog(@"property: %@", [NSString stringWithUTF8String:prop->name]);
}
free(props);
}
/// 遍歷方法
-(void) printMethodes: (Class)cls {
// 記錄函數(shù)個(gè)數(shù)
unsigned int count = 0;
// 讀取函數(shù)列表
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(@"method: %@-%p", NSStringFromSelector(sel), imp);
}
free(methodList);
}
- 在
addObserver處添加打印代碼,分別檢查HTperson本類(lèi)和NSKVONotifying_HTPerson派生類(lèi)的Ivars、Property和Method:
// 添加監(jiān)聽(tīng)
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context: NULL];
NSLog(@"------- NSKVONotifying_HTPerson --------");
[self printMethodes: objc_getClass("NSKVONotifying_HTPerson")];
[self printIvars: objc_getClass("NSKVONotifying_HTPerson")];
[self printProperties: objc_getClass("NSKVONotifying_HTPerson")];
NSLog(@"------- HTPerson --------");
[self printMethodes: HTPerson.class];
[self printIvars: HTPerson.class];
[self printProperties: HTPerson.class];
- 打印結(jié)果:

拓展:
- 檢驗(yàn)同樣
繼承自HTPerosn的子類(lèi)HTStudent,打印結(jié)果:// HTStudent @interface HTStudent : HTPerson @end @implementation HTStudent @endimage.png
結(jié)論:
- 直接繼承的
子類(lèi),沒(méi)有任何方法和屬性??梢源_定:
KVO派生類(lèi)繼承自HTPerosn,重寫(xiě)了setName、class、dealloc方法,新增了_isKVOA方法
3.2.2 KVO派生類(lèi)給父類(lèi)屬性賦值
- 在
addObserver處添加斷點(diǎn),運(yùn)行代碼到此處時(shí),lldb輸入:watchpoint set variable self->_person->_name:

-
設(shè)置成功后,運(yùn)行代碼,點(diǎn)擊屏幕觸發(fā)touchesBegan事件,會(huì)進(jìn)入匯編頁(yè)面(觀察到設(shè)置屬性斷點(diǎn)處)


- 可以觀察到,當(dāng)
派生類(lèi)在調(diào)用willChange和didChange中間,調(diào)用了[HTPerson setName]方法,完成了給父類(lèi)HTPerson的name屬性賦值。(此時(shí)的willChange和didChange方法是繼承自NSObject的)
3.2.3 KVO派生類(lèi)何時(shí)移除,是否真移除?
-
我們?cè)?code>removeObserver處加入斷點(diǎn),分別在
removeObserver前后使用object_getClassName()打印當(dāng)前isa指向的類(lèi):
image.png 發(fā)現(xiàn)
NSKVONotifying_HTPerson在外部removeObserver時(shí),完成的移除操作。將isa指回了原類(lèi)。但是我們?cè)?code>removeObserver移除操作之后,打印
HTPerosn類(lèi)和子類(lèi)的信息,發(fā)現(xiàn)NSKVONotifying_HTPerson派生類(lèi)并沒(méi)有移除。
ps:
頁(yè)面銷(xiāo)毀之后再打印HTPerosn類(lèi)和子類(lèi),也一樣存在NSKVONotifying_HTPerson派生類(lèi)。
KVO派生類(lèi)只要生成,就會(huì)一直存在,這樣可以減少頻繁的添加操作
至此,我們已經(jīng)知道KVO是創(chuàng)建派生類(lèi)實(shí)現(xiàn)了鍵值觀察。
- 添加:
addObserver時(shí),創(chuàng)建了派生類(lèi),派生類(lèi)是當(dāng)前類(lèi)的子類(lèi),重寫(xiě)了被監(jiān)聽(tīng)屬性的setter方法,并將當(dāng)前類(lèi)的isa指向了派生類(lèi)。
(此時(shí)開(kāi)始,所有調(diào)用本類(lèi)的方法,都是調(diào)用的派生類(lèi)。派生類(lèi)中沒(méi)有的方法,就會(huì)沿著繼承鏈查詢(xún)到本類(lèi))
- 添加:
- 賦值:
派生類(lèi)重寫(xiě)了被監(jiān)聽(tīng)屬性的setter方法,在派生類(lèi)的setter方法觸發(fā)時(shí):在willChange之后,didChange之前,調(diào)用父類(lèi)屬性settter方法,完成父類(lèi)屬性的賦值`。
- 賦值:
- 移除: 在
removeObserver后,isa從派生類(lèi)指回本類(lèi)。 但創(chuàng)建過(guò)的派生類(lèi),不會(huì)被本類(lèi)從子類(lèi)列表中移除,會(huì)一直存在。
- 移除: 在
- 假象: 之所以外部
打印class永遠(yuǎn)看不到派生類(lèi),是因?yàn)?code>派生類(lèi)將class方法重寫(xiě)了,故意不讓外界看到。
(知道越多,煩惱越多 ?? ,就讓派生類(lèi)做個(gè)默默付出的無(wú)名英雄吧)
- 假象: 之所以外部
下一節(jié),我們純代碼自定義KVO。(簡(jiǎn)化版,重在理解派生類(lèi)的流程和功能)









