相信讀者對KVO的使用應該已經很熟練了,本文主要講KVO的一些注意點和原理,對詳細的使用不做過多的展示。
日常使用注意點
context 參數(shù)
1.context填NULL還是nil?先看源代碼:
[self addObserver:self.person forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:<#(nullable void *)#>]
下面context應該填什么?日常我們一般不會用到context這個參數(shù),一般會填nil或者NULL;那么到底是應該填nil,還是NULL;答案是:NULL;
why? 看看在context的placehold上顯示的是context:<#(nullable void *)#>,是一個可空void *指針,既然不是oc對象,那么就應該填NULL。我們再看看KVO文檔,文檔中有這么一點段話

2.這個context的作用?show U code
######Person
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *nickName;
@end
######Animal
@interface Animal : NSObject
@property (nonatomic,copy) NSString *name;
@end
######ViewController
#import "ViewController.h"
#import "Person.h"
#import "Animal.h"
static void *PersonNameContext = &PersonNameContext;
static void *PersonNickNameContext = &PersonNickNameContext;
static void *AnimalNameContext = &AnimalNameContext;
@interface ViewController ()
@property (nonatomic,strong) Person *person;
@property (nonatomic,strong) Animal *animal;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
self.person = [Person new];
self.animal = [Animal new];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
[self.animal addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:AnimalNameContext];
[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:PersonNickNameContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
//常規(guī)做法
// if ([object isEqual:self.person]) {
// if ([keyPath isEqualToString:@"name"]) {
// <#statements#>
// } else if ([keyPath isEqualToString:@"nickName"]) {
// <#statements#>
// }
// } else if ([object isEqual:self.animal]) {
// if ([keyPath isEqualToString:@"name"]) {
// <#statements#>
// }
// }
//context做法
if (context == PersonNameContext) {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"name=%@",self.person.name);
}
} else if (context == AnimalNameContext ) {
if ([keyPath isEqualToString:@"name"]) {
}
} else if (context == PersonNickNameContext) {
if ([keyPath isEqualToString:@"nickName"]) {
NSLog(@"nickName=%@",self.person.nickName);
}
}
}
static int a = 1;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
a ++;
self.person.name = [NSString stringWithFormat:@"name+%d",a];
self.person.nickName = [NSString stringWithFormat:@"nickName+%d",a];
}
-(void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"nickName"];
[self.animal removeObserver:self forKeyPath:@"name"];
}
@end
所以:使用context參數(shù)會更加的便捷高效安全
觀察者要移除
日常開發(fā)中,一定要寫移除觀察者的代碼,如果沒有移除,會有造成野指針,成為崩潰隱患。
多次修改代碼高效設置
eg:假設在上面context 參數(shù)內容段中,因為需求Person類中的name屬性昨天是需要觀察的,而今天一上班,產品經理說需求又改了,又不需要再觀察了這個那么屬性。通常遇到這種情況的時候,我們會刪掉(注銷)之前寫好的代碼,然后又過了幾天產品經理要求改回來;遇到這個情況估計你的內心會有一萬匹草泥馬跑過。因為KVO代碼量分散且并不少,這種操作其實讓人很煩;這個時候你可以在Person類中重寫這個方法automaticallyNotifiesObserversForKey:(是否自動觀察屬性)這樣操作:只會觀察nickName,而不會觀察name
#import "Person.h"
@implementation Person
// 自動開關
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;
}
return YES;
}
@end
使用automaticallyNotifiesObserversForKey:根據(jù)key去判斷,可以讓程序更加健壯;
多個因素影響
當被觀察的對象受到其他多個因素影響時;
eg:下載進度受當前下載量和總下載量的影響,但是我們需要觀察的是進度,可以使用keyPathsForValuesAffectingValueForKey:
#####DownLoadManager
@interface DownLoadManager : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@end
#import "DownLoadManager.h"
@implementation DownLoadManager
// 下載進度 -- 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;
}
- (NSString *)downloadProgress{
return [NSString stringWithFormat:@"downloadProgress =%.02f%%",self.writtenData/self.totalData*100];
}
@end
######ViewController
#import "ViewController.h"
#import "DownLoadManager.h"
@interface ViewController ()
@property (nonatomic,strong) DownLoadManager *downLoadManager;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.downLoadManager = [DownLoadManager new];
self.downLoadManager.writtenData = 10;
self.downLoadManager.totalData = 100;
// 多個因素影響 - 下載進度 = 當前下載量 / 總量
[self.downLoadManager addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"downloadProgress"]) {
NSLog(@"downloadProgress = %@",self.downLoadManager.downloadProgress);
}
}
//點擊屏幕
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.downLoadManager.writtenData += 10;
self.downLoadManager.totalData += 5;
}
-(void)dealloc {
[self.downLoadManager removeObserver:self forKeyPath:@"downloadProgress"];
}
@end
點擊屏幕打印結果:
2020-02-21 11:08:38.002797+0800 KTest[5745:107244] downloadProgress = downloadProgress =20.00%
2020-02-21 11:08:38.002912+0800 KTest[5745:107244] downloadProgress = downloadProgress =19.05%
2020-02-21 11:08:39.408895+0800 KTest[5745:107244] downloadProgress = downloadProgress =28.57%
2020-02-21 11:08:39.409002+0800 KTest[5745:107244] downloadProgress = downloadProgress =27.27%
2020-02-21 11:08:40.105935+0800 KTest[5745:107244] downloadProgress = downloadProgress =36.36%
2020-02-21 11:08:40.106029+0800 KTest[5745:107244] downloadProgress = downloadProgress =34.78%
可變數(shù)組
觀察可變數(shù)組的增刪改查時,不要直接使用addObject:或者removeObject:直接使用會崩潰,需要先通過mutableArrayValueForKey:獲得數(shù)組對象,才能進一步操作;原因:iOS默認不支持對數(shù)組的KVO,KVO是通過KVC實現(xiàn)的,普通方式監(jiān)聽的對象的地址的變化,而數(shù)組地址不變,而是里面的值發(fā)生了改變;
eg:
######Person
@interface Person : NSObject
@property (nonatomic,copy) NSMutableArray *studentNameArray;
@end
######ViewController
#import "ViewController.h"
#import "Person.h"
#import "Animal.h"
#import "DownLoadManager.h"
static void *PersonNameContext = &PersonNameContext;
static void *PersonNickNameContext = &PersonNickNameContext;
static void *AnimalNameContext = &AnimalNameContext;
@interface ViewController ()
@property (nonatomic,strong) Person *person;
@property (nonatomic,strong) Animal *animal;
@property (nonatomic,strong) DownLoadManager *downLoadManager;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [Person new];
self.animal = [Animal new];
self.person.studentNameArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"studentNameArray" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"studentNameArray"]) {
NSLog(@"studentNameArray = %@",self.person.studentNameArray);
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// KVO 建立在 KVC上
// [self.person.studentNameArray addObject:@"lee"];不要這么做,會崩潰
//使用mutableArrayValueForKey獲取數(shù)組對象
[[self.person mutableArrayValueForKey:@"studentNameArray"] addObject:@"lee"];
[[self.person mutableArrayValueForKey:@"studentNameArray"] removeObject:@"lee"];
}
-(void)dealloc {
[self.person removeObserver:self forKeyPath:@"studentNameArray"];
}
@end
KVO原理
Automatic key-value observing is implemented using a technique called isa-swizzling.
這段話來自官方文檔:KVO是通過isa-swizzling實現(xiàn)的;
那具體是如何isa-swizzling的呢?
1.動態(tài)的生成子類:NSKVONotifying_XXX
驗證:
######Person
@interface Person : NSObject
@property (nonatomic,copy) NSString *nickName;
@end
######KVOIMPViewController
#import "KVOIMPViewController.h"
#import <objc/runtime.h>
#import "Person.h"
@interface KVOIMPViewController ()
@property (nonatomic,strong) Person *person;
@end
@implementation KVOIMPViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor greenColor];
self.person = [[Person alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
int a ;
}
在[self.person addObserver:self forKey...和int a兩個地方分別打上斷點;使用LLDB調試:
(lldb) po object_getClassName(self.person)
"Person"
(lldb) po object_getClassName(self.person)
"NSKVONotifying_Person"
可以看到在運行了addObserver:(NSObject *)observer forKeyPath:...之后,當前Person類變成了NSKVONotifying_Person。
上面只能說明生成了NSKVONotifying_Person類但不能說明是Person類的子類;繼續(xù)驗證
- (void)viewDidLoad {
[super viewDidLoad];
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_Person")];
[self printClasses:[Person class]];
}
#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);
}
打印結果
KTest[10265:237411] classes = (
Person,
"NSKVONotifying_Person"
)
可以看到Person類下面確實還有子類NSKVONotifying_Person
內部關系圖:

2.動態(tài)子類生重寫了很多方法
打印觀察前后,類方法的變化
######Person
@interface Person : NSObject
//@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *nickName;
//@property (nonatomic,copy) NSMutableArray *studentNameArray;
@end
######KVOIMPViewController
#import "KVOIMPViewController.h"
#import <objc/runtime.h>
#import "Person.h"
@interface KVOIMPViewController ()
@property (nonatomic,strong) Person *person;
@end
@implementation KVOIMPViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
[self printClassAllMethod:NSClassFromString(@"Person")];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_Person")];
}
#pragma mark - 遍歷方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
NSLog(@"*********************%@類的方法list",NSStringFromClass(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);
}
打印結果:
KTest[9724:222902] *********************Person類的方法list
KTest[9724:222902] .cxx_destruct-0x106ace0d0
KTest[9724:222902] nickName-0x106ace060
KTest[9724:222902] setNickName:-0x106ace090
KTest[9724:222902] *********************NSKVONotifying_Person類的方法list
KTest[9724:222902] setNickName:-0x7fff25721c7a
KTest[9724:222902] class-0x7fff2572073d
KTest[9724:222902] dealloc-0x7fff257204a2
KTest[9724:222902] _isKVOA-0x7fff2572049a
從打印可以看到:
NSKVONotifying_Person重寫了setNickName class dealloc _isKVOA方法;
1.setNickName:
重寫的setNickName:方法內部大概這么實現(xiàn)的
@implementation Person
- (void)setNickName:(NSString *)nickName {
[self willChangeValueForKey:@"nickName"];
_nickName = nickName;
[self didChangeValueForKey:@"nickName"];
}
@end
2.class
為何重寫class方法:為了讓外界感受不到子類NSKVONotifying_Person的生成
- dealloc
重寫這個方法應該是為了在對象銷毀的時候做一些操作吧,尚未探究 - _isKVOA
_isKVOA:判斷是否是KVO生成的類
3.移除觀察之后 isa指針是否指回來?
斷點調試:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor greenColor];
self.person = [[Person alloc] init];
NSLog(@"*******添加觀察者之前");
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"*******添加觀察者之后");
[self.person removeObserver:self forKeyPath:@"nickName"];
NSLog(@"*******移除觀察者之后");
}
打印結果:
KTest[10766:251988] *******添加觀察者之前
(lldb) po object_getClassName(self.person)
"Person"
KTest[10766:251988] *******添加觀察者之后
(lldb) po object_getClassName(self.person)
"NSKVONotifying_Person"
KTest[10766:251988] *******移除觀察者之后
(lldb) po object_getClassName(self.person)
"Person"
可以看到,person對象的類又指回了Person類
移除觀察者后動態(tài)子類會被銷毀嗎?不會。
驗證:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
NSLog(@"*******添加觀察者之前");
[self printClasses:[Person class]];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"*******添加觀察者之后");
[self printClasses:[Person class]];
[self.person removeObserver:self forKeyPath:@"nickName"];
NSLog(@"*******移除觀察者之后");
[self printClasses:[Person class]];
}
#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);
}
打印結果:
KTest[11026:259919] *******添加觀察者之前
KTest[11026:259919] classes = (
Person
)
KTest[11026:259919] *******添加觀察者之后
KTest[11026:259919] classes = (
Person,
"NSKVONotifying_Person"
)
KTest[11026:259919] *******移除觀察者之后
KTest[11026:259919] classes = (
Person,
"NSKVONotifying_Person"
)
可以看到在移除觀察者之后沒有移除動態(tài)子類NSKVONotifying_Person
總結
日常注意點
1.context參數(shù)的在不使用時推薦填寫NULL,其功能:程序更加便捷、高效、安全
2.觀察者要移除
3.多個因素影響時可以使用:keyPathsForValuesAffectingValueForKey:
4.針對經常需要改動的代碼可以使用:automaticallyNotifiesObserversForKey:方法對key進行選擇處理
5.觀察可變數(shù)組時,要使用mutableArrayValueForKey:
原理
1.KVO通過isa-swizzling實現(xiàn)
2.動態(tài)的生成子類:NSKVONotifying_XXX
3.主要是觀察setXXX方法;內部重寫
4.還重寫了setXXX class dealloc _isKVOA方法;
5.移除觀察之后 isa指針會重新指回來
6.移除觀察者后動態(tài)子類會被銷毀嗎?不會。