鍵值觀察提供了一種機(jī)制,該機(jī)制允許將其他對象的特定屬性的更改通知給對象。對于應(yīng)用程序中模型層和控制器層之間的通信特別有用。 (在OS X中,控制器層綁定技術(shù)在很大程度上依賴于鍵值觀察。)控制器對象通常觀察模型對象的屬性,而視圖對象通過控制器觀察模型對象的屬性。但是,此外,模型對象可能會(huì)觀察其他模型對象(通常是確定從屬值何時(shí)更改),甚至是自身(再次確定從屬值何時(shí)更改)。
您可以觀察到一些屬性,包括簡單屬性,一對一關(guān)系和一對多關(guān)系。一對多關(guān)系的觀察者被告知所做更改的類型,以及更改涉及哪些對象。
一、基本使用
1.1 注冊觀察者
/// 注冊觀察者
/// @param observer 觀察者
/// @param keyPath 要觀察的屬性keyPath
/// @param options 觀察者選項(xiàng)。影響通知的生成方式及回調(diào)時(shí)字典中攜帶的信息
/// @param context 上下文
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
context接收一個(gè)void *類型的參數(shù),基本可以傳任何類型。假如子類和他的父類由于不同的原因都注冊了對同一個(gè)屬性的觀察,在回調(diào)中這兩種的處理是不同的,那么回調(diào)中的keyPath和被觀察者對象是無法區(qū)分的,此時(shí)就可以通過context這個(gè)參數(shù)來區(qū)分。
1.2 實(shí)現(xiàn)回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
1.3 移除觀察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
1.4觀察集合類型屬性
@interface Animal : NSObject
@property (nonatomic,strong) NSMutableArray *friends;
@end
//viewContoller代碼
- (void)viewDidLoad {
[super viewDidLoad];
Animal *animal = [Animal alloc];
animal.friends = @[].mutableCopy;
[animal addObserver:self
forKeyPath:@"friends"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:NULL];
//1、被動(dòng)觸發(fā)錯(cuò)誤方式:這里無法觸發(fā)`kvo`回調(diào)
[animal.friends addObject:@"dog"];
//2、被動(dòng)觸發(fā)正確方法
[[animal mutableArrayValueForKey:@"friends"] addObject:@"dog"];
//3、手動(dòng)觸發(fā)
[animal willChangeValueForKey:@"friends"];
[animal.friends addObject:@"dog"];
[animal didChangeValueForKey:@"friends"];
}
第23行代碼相當(dāng)于
NSMutableArray *tmp = [NSMutableArray arrayWithArray:animal.friends];
[tmp addObject:@"dog"];
animal.friends = tmp;
因此觸發(fā)了kvo,21行,因?yàn)镵VO是給予set方法的,這樣不會(huì)觸發(fā)set方法,所以就不會(huì)觸發(fā)KVO通知。
1.5多屬性的關(guān)聯(lián)
我們需要在被觀察者類重寫兩個(gè)方法:
- 一個(gè)系統(tǒng)方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key或者+ (NSSet *)keyPathsForValuesAffecting<xxx> - 一個(gè)是被觀察屬性的
getter方法。
例如:有一個(gè)Downloader.h類,有三個(gè)屬性totalBytes,completedBytes,和百分比進(jìn)度progress:
// Downloader.h
@interface Downloader : NSObject
@property (nonatomic) unsigned long long totalBytes;
@property (nonatomic) unsigned long long completedBytes;
@property (nonatomic, copy) NSString *progress;
@end
在UI層我們只關(guān)注progress,但進(jìn)度是受其他兩個(gè)屬性共同影響的,此時(shí)需要在Downloader.m實(shí)現(xiàn)中重寫兩個(gè)方法:
@implementation Downloader
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"progress"]) {
NSArray *dependKeys = @[@"totalBytes", @"completedBytes"];
keyPaths = [keyPaths setByAddingObjectsFromArray:dependKeys];
}
return keyPaths;
}
- (NSString *)progress {
if (0 == self.totalBytes || 0 == self.completedBytes) {
return @"0";
}
double progress = (double)self.completedBytes / (double)self.totalBytes * 100;
if (progress > 100) {
progress = 100;
}
return [NSString stringWithFormat:@"%d%%", (int)ceil(progress)];
}
@end
二、KVO實(shí)現(xiàn)原理
Automatic key-value observing is implemented using a technique called
isa-swizzling. 具體參考蘋果文檔
當(dāng)一個(gè)類的實(shí)例第一次注冊觀察者時(shí),系統(tǒng)會(huì)做以下事情:
- 動(dòng)態(tài)生成一個(gè)繼承自該類的中間類:
NSKVONotifying_xxx - 將對象的isa指向這個(gè)中間類(
isa-swizzling) - 觀察的是
setter - 子類中重寫
set<xxx>、-class、-dealloc方法,添加一個(gè)-_isKVOA方法,依然返回原類,而非子類 - 移除所有的觀察后,isa會(huì)指回來,但是動(dòng)態(tài)子類不會(huì)銷毀
2.1 原理驗(yàn)證
被觀察類Animal添加代碼:
@interface Animal : NSObject{
@public
NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic,strong) NSMutableArray *friends;
@end
viewController添加代碼:
- (void)viewDidLoad {
[super viewDidLoad];
Animal *animal = [Animal alloc];
[self printClasses:[animal class]];
[self printMethods:[animal class]];
[animal addObserver:self
forKeyPath:@"nickName"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:NULL];
[animal addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:NULL];
[animal addObserver:self
forKeyPath:@"friends"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:NULL];
printf("\n********************************************************\n\n");
[self printClasses:[animal class]];
[self printMethods:NSClassFromString(@"NSKVONotifying_Animal")];
animal.name = @"dog";
animal->nickName = @"cat";
}
/// 打印出指定類及其子類列表
- (void)printClasses:(Class)cls {
int count = objc_getClassList(NULL, 0);
NSMutableArray *results = [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])) {
[results addObject:classes[i]];
}
}
NSLog(@"\nClasses: %@", results);
free(classes);
}
/// 打印出指定類所有的方法
- (void)printMethods:(Class)cls {
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
printf("Methods of class: %s (\n", NSStringFromClass(cls).UTF8String);
for (int i = 0; i < count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = method_getImplementation(method);
printf(" %s-%p\n", NSStringFromSelector(sel).UTF8String, imp);
}
printf(")\n");
free(methodList);
}
控制臺(tái)打印結(jié)果 :
2020-04-13 15:08:51.803441+0800 kvo[23854:22631006]
Classes: (
Animal
)
Methods of class: Animal (
.cxx_destruct-0x10f868e50
name-0x10f868d80
setName:-0x10f868db0
friends-0x10f868df0
setFriends:-0x10f868e10
)
********************************************************
2020-04-13 15:08:51.809383+0800 kvo[23854:22631006]
Classes: (
Animal,
"NSKVONotifying_Animal"
)
Methods of class: NSKVONotifying_Animal (
setFriends:-0x7fff25701c8a
setName:-0x7fff25701c8a
class-0x7fff2570074d
dealloc-0x7fff257004b2
_isKVOA-0x7fff257004aa
)
2020-04-13 15:08:51.809932+0800 kvo[23854:22631006] -------------------{
kind = 1;
new = dog;
old = "<null>";
}
通過上面打印結(jié)果發(fā)現(xiàn):只有屬性發(fā)生了回調(diào),實(shí)例變量并沒有。它們的區(qū)別就是有沒有setter方法,所以我們得出結(jié)果:KVO是通過setter方法進(jìn)行處理回調(diào)的。
蘋果官方推薦盡量使用屬性點(diǎn)語法的形式為屬性賦值和訪問屬性,這樣其實(shí)是在調(diào)用setter和getter,如果重寫了setter和getter在期中增加了額外代碼,可以保證代碼執(zhí)行的正確性。
在viewController中繼續(xù)添加代碼,移除所有的觀察者。
[self performSelector:@selector(removeAllObserver) withObject:nil afterDelay:2];
- (void)removeAllObserver{
[_animal removeObserver:self forKeyPath:@"nickName"];
[_animal removeObserver:self forKeyPath:@"name"];
[_animal removeObserver:self forKeyPath:@"friends"];
printf("\n********************************************************\n\n");
[self printClasses:[_animal class]];
}
打印結(jié)果:
2020-04-13 15:20:17.770486+0800 kvo[24356:22641029]
Classes: (
Animal,
"NSKVONotifying_Animal"
)
你也可以通過
lldb,來探索一下,整個(gè)過程中isa指針的指向,object_getClassName(animal)
2.2 kvc 和 kvo
蘋果文檔有介紹,在理解KVO之前,必須先理解KVC。上篇文章我們也討論了KVC的實(shí)現(xiàn)原理,KVC會(huì)先查找setter或getter進(jìn)行調(diào)用,如果沒有查找到,則調(diào)用類方法+accessInstanceVariablesDirectly,如果返回YES,再去查找成員變量。
KVO也有類似的機(jī)制,在KVO接口中有這三個(gè)接口:
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
+automaticallyNotifiesObserversForKey:默認(rèn)返回YES,動(dòng)態(tài)創(chuàng)建的中間類重寫了setter,雖然無法看到實(shí)現(xiàn)源碼,但可以猜測在修改屬性前后分別調(diào)用了-willChangeValueForKey:和-didChangeValueForKey:類似方法,達(dá)到通知觀察者的目的。
如果子類中重載了+automaticallyNotifiesObserversForKey:并返回NO,則無法觸發(fā)自動(dòng)KVO通知機(jī)制,但我們可以通過手動(dòng)調(diào)用-willChangeValueForKey:和-didChangeValueForKey:來觸發(fā)KVO回調(diào)。
三、自定義KVO
系統(tǒng)kvo使用時(shí)存在不方便的地方,根據(jù)kvo的原理和基本使用,我們可以簡單自定義kvo實(shí)現(xiàn)。
- 入?yún)z查
- 檢查是否有屬性的setter
- 動(dòng)態(tài)創(chuàng)建對象子類BLKVOClass_xxx
- isa-swizzling
- 重寫-class、-dealloc方法
- 重寫setter
- 保存觀察者信息,在屬性發(fā)生變化時(shí)回調(diào)
3.1 動(dòng)態(tài)創(chuàng)建對象子類
Class newClass = NSClassFromString(newClassName);
if (newClass) {
return newClass;
}
/**
* 如果內(nèi)存不存在,創(chuàng)建生成
* 參數(shù)一: 父類
* 參數(shù)二: 新類的名字
* 參數(shù)三: 新類的開辟的額外空間
*/
// 2.1 : 申請類
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2.2 : 注冊類
objc_registerClassPair(newClass);
3.2 isa-swizzling
重寫class方法
Class mm_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
動(dòng)態(tài)子類添加class實(shí)現(xiàn),完成isa-swizzling
// 2.3.1 : 添加class : class的指向是父類
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod(newClass, classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)mm_class, classTypes);
3.3 dealloc
重寫delloc,1、安全移除所有observe,2、銷毀關(guān)聯(lián)對象,3、isa指回父類,4、調(diào)用系統(tǒng)dealloc。
- (void)mm_dealloc {
// Class superClass = [self class];
Class superCls = class_getSuperclass(object_getClass(self));
object_setClass(self, superCls);
// Call system -dealloc
[self mm_dealloc];
}
四、FBKVOController
下面簡單聊一下FBKVOController,它里面有幾個(gè)關(guān)鍵類:
-
_FBKVOSharedController,單利對象,處理、轉(zhuǎn)發(fā)KVOViewController傳過來的所有觀察者事件。 -
_FBKVOInfo,數(shù)據(jù)模型,保存一個(gè)完整的KVO數(shù)據(jù)。 -
KVOViewController,每個(gè)觀察者都有一個(gè)該類的實(shí)例對象,這個(gè)類用于處理觀察者傳過來的所有數(shù)據(jù),下圖是他的主要屬性構(gòu)成。KVOViewController.png
下面是一個(gè)簡單的調(diào)用實(shí)現(xiàn)代碼:
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc] init];
FBKVOController *kvoCtrl = [FBKVOController controllerWithObserver:self];
[kvoCtrl observe:student keyPath:@"nickName" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"****%@****",change);
}];
student.nickName = @"kkk";
}
observe對應(yīng)viewController,student對應(yīng)object。當(dāng)viewController被釋放的時(shí)候,會(huì)先調(diào)用FBKVOController的dealloc方法,在這里會(huì)將_objectInfosMap里所有的被觀察者安全得 remove。
拓展:抖音技術(shù)團(tuán)隊(duì)iOS大解密:玄之又玄的KVO、Objective-C & Swift 最輕量級(jí) Hook 方案
