KVO的全稱是Key-Value Observing, 俗稱"鍵值監聽", 可以用於監聽某個對象屬性值的改變
一、KVO的使用
- 新建工程, 定義Person類繼承自NSObject, 並添加int類型的屬性age
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@implementation Person
@end
- 在ViewController中添加兩個Person類型的屬性person1和person2, 並給person1添加監聽age屬性的觀察者, 當點擊屏幕時修改這兩個對象的age屬性值
@interface ViewController ()
@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年齡"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 21;
self.person2.age = 22;
}
/**
當被觀察的屬性, 使用`set`方法賦值時, 觸發觀察者
@param keyPath 被監聽的屬性
@param object 添加監聽的對象
@param change 屬性改變前後的值
@param context 添加觀察者時傳入的參數
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"%@ - %@ - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc
{
[self.person1 removeObserver:self forKeyPath:@"age"];
}
@end
- 運行程序, 點擊屏幕, 會有如下打印:
<Person: 0x60c000014b20> - age - {
kind = 1;
new = 21;
old = 1;
} - 年齡
- 打印中只有person1的屬性值發生改變信息, 而沒有person2的屬性值改變的信息
- 這是因爲給person1添加了觀察者, 而person2沒有添加
-
對象添加KVO監聽屬性, 類似於下圖
問: 爲什麼同樣都是調用了setAge:方法, person1卻能監聽age的屬性值改變?
二、添加KVO的對象的isa指針指向何處
- 下圖就是上面創建的工程, 我們在touchesBegan:withEvent:方法中
-
打斷點, 當點擊控制器的view後, 查看person1和person2的isa指針指向何處
- 根據結果可以知道
person1的isa指向類對象NSKVONotifying_Person
person2的isa指向類對象Person -
通過person1的isa找到NSKVONotifying_Person後, 再次調用superclass, 可以看到NSKVONotifying_Person的父類是Person
- 說明: 添加了觀察者(KVO)的對象, 它的isa指針發生了改變, 指向了系統動態生成的子類NSKVONotifying_Person
- 已經知道對象調用方法的過程:
首先通過isa指針, 找到類對象
在類對象中查找方法, 如果方法存在就會調用 - 所以person1調用的setAge:方法, 是子類NSKVONotifying_Person中重寫的setAge:方法
- 這就是爲什麼, 明明person1和person2都調用了setAge:方法, 而person1會有屬性監聽
1、未使用KVO監聽的Person對象
2、使用KVO監聽的Person對象
- 上面是通過isa驗證了person1指向了NSKVONotifying_Person類, 下面使用代碼進行驗證
- 在給person1添加觀察者的前後, 分別打印person1和person2的類型
3、通過代碼驗證person1的類型是NSKVONotifying_Person
NSLog(@"%@ - %@",
object_getClass(self.person1),
object_getClass(self.person2));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年齡"];
NSLog(@"%@ - %@",
object_getClass(self.person1),
object_getClass(self.person2));
- 執行後打印如下:
Person - Person
NSKVONotifying_Person - Person
- 根據打印結果, 可以證明, 在給person1添加觀察者之後, person1的類型是NSKVONotifying_Person
三、驗證NSKVONotifying_Person中setAge:的方法實現, 是_NSSetIntValueAndNotify函數
- 在person1添加觀察者的前後, 設置打印setAge方法地址的代碼
NSLog(@"添加KVO之前 - %p - %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年齡"];
NSLog(@"添加KVO之後 - %p - %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
-
打印結果如下圖:
- 很明顯, 在添加KVO的前後, person1調用的setAge:方法已經改變
-
下面使用lldb打印一下setAge:方法
四、探索NSKVONotifying_Person類對象的isa, 指向何處
- 在添加KVO前後, 添加如下代碼, 打印person1的類對象和元類對象
NSLog(@"添加KVO之前 - %@ - %@",
object_getClass(self.person1),
object_getClass(object_getClass(self.person1)));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年齡"];
NSLog(@"添加KVO之前 - %@ - %@",
object_getClass(self.person1),
object_getClass(object_getClass(self.person1)));
-
打印結果如下
- 由打印可知: NSKVONotifying_Person類對象的isa指向NSKVONotifying_Person的元類對象
五、通過逆向, 查看Fundation中的_NSSetIntValueAndNotify函數
-
使用iFunBox查看越獄手機中的動態庫文件
- 我使用的iPhone6, 所以這裏查看arm64架構下的動態庫文件
-
將dyld_shared_cache_arm64文件託至電腦(複製)
- 通過終端, 使用dsc_extractor對dyld_shared_cache_arm64進行分解
# 終端命令
./dsc_extractor dyld_shared_cache_arm64 test
-
分解出的Fundation動態庫如下
- 接下來使用反編譯工具 hopper, 對Fundation反編譯
-
使用hopper打開Fundation動態庫文件
-
Next
-
OK, 可以看到反編譯成功
-
搜索_NSSetIntValueAndNotify, 可以看到Fundation中確實有_NSSetIntValueAndNotify函數
-
搜索ValueAndNotify, 可以看到有很多類似的方法
- 根據搜索結果可以推斷出, 對不同類型的屬性添加觀察, 就會調用對應屬性類型的_NSSet*ValueAndNotify方法, *表示類型
- 驗證這個推斷, 將Person的age屬性類型, 從int改爲double
@interface Person : NSObject
@property (nonatomic, assign) double age;
@end
-
再次打印setAge:方法實現:
- 根據結果, 驗證推斷正確
六、_NSSet*ValueAndNotify的內部實現
[self willChangeValueForKey:@"age"];
// 原來的setter實現
[self didChangeValueForKey:@"age"];
- 調用順序:
調用willChangeValueForKey:
調用原來的setter實現
調用didChangeValueForKey:
didChangeValueForKey:內部會調用observeValueForKeyPath:ofObject:change:context: - 通過代碼驗證, 在Person.m中手動實現willChangeValueForKey和didChangeValueForKey以及setAge:方法, 代碼如下:
@implementation Person
- (void)setAge:(int)age
{
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key
{
NSLog(@"willChangeValueForKey - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
- 運行程序, 點擊控制器的View, 修改person1的age屬性, 有如下打印:
// 1
willChangeValueForKey - begin
// 2
willChangeValueForKey - end
// 3
setAge:
// 4
didChangeValueForKey - begin
// 5
<Person: 0x60800001a510> - age - {
kind = 1;
new = 21;
old = 1;
} - 年齡
// 6
didChangeValueForKey - end
- 根據打印: 可以證明以上的_NSSet*ValueAndNotify的內部實現
七、子類內部的方法
- 在使用KVO監聽的Person對象的圖片中, NSKVONotifying_Person的類對象中, 一共有兩個指針isa和superclass, 四個方法setAge:, class, dealloc和_isKVOA
- 下面使用Runtime代碼, 來驗證NSKVONotifying_Person中確實存在這四個方法
- 首先在ViewController中添加下面的方法:
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[array addObject:methodName];
}
NSLog(@"%@ - %@", cls, array);
}
- 同時刪除Person.m中的所有方法
@implementation Person
@end
- 接着在給person1添加觀察者的後面調用方法, 傳入cls
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年齡"];
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
- 運行程序後, 有以下打印:
NSKVONotifying_Person - (
"setAge:",
class,
dealloc,
"_isKVOA"
)
Person - (
"setAge:",
age
)
- 可以看到NSKVONotifying_Person中有四個方法:
setAge:
class
dealloc
_isKVOA
1、推斷NSKVONotifying_Person中class方法的實現
- 首先在給person1添加觀察者的後面添加打印[self.person1 class]的代碼
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年齡"];
NSLog(@"%@", [self.person1 class]);
- 執行後, 打印結果如下:
// 打印結果:
Person
- 打印結果是Person, 而person1的isa指向是NSKVONotifying_Person, 這說明在NSKVONotifying_Person中, 對class進行了重寫
- 現在推斷NSKVONotifying_Person中的clss方法實現如下:
- (Class)class {
return [Preson class];
}
2、關於dealloc和_isKVOA方法
- 由於沒辦法看到NSKVONotifying_Person中具體的源碼, 所以只能模糊推斷
- 因爲NSKVONotifying_Person是爲了觀察age屬性, 才創建出來的, 所以在dealloc中會進行一些結尾操作
- 而_isKVOA方法, 則推斷爲:
- (BOOL)_isKVOA {
return YES
}
八、面試題
1、iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼)
- 利用RuntimeAPI動態生成一個子類, 並且讓instance對象的isa指向這個全新的子類
- 當修改instance對象的屬性時, 會調用Fundation的_NSSet*ValueAndNotify函數
willChangeValueForKey:
父類原來的setter方法
didChangeValueForKey: - 內部會觸發監聽器Observer的監聽方法(observeValueForKeyPath:ofObject:change:context:)
2、如果直接修改對象的成員變量, 是否會觸發監聽器的(observeValueForKeyPath:ofObject:change:context:)方法?
- 將Person類的_age暴露出來
@interface Person : NSObject
{
@public
int _age;
}
@property (nonatomic, assign) int age;
@end
- 將ViewController中的touchesBegan:withEvent:方法修改如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1->_age = 21;
}
- 運行程序後, 發現並沒有觸發observeValueForKeyPath:ofObject:change:context:方法
- 所以, 直接修改對象的成員變量, 而不調用set方法, 將不會觸發觀察者的observeValueForKeyPath:ofObject:change:context:方法
3、如何手動觸發KVO?
- 已知實例對象被觀察的屬性, 在調用set方法進行修改時, 會觸發_NSSet*ValueAndNotify函數
- 並觸發willChangeValueForKey:和didChangeValueForKey:這兩個方法, 所以我們可以手動添加這兩個方法, 來觸發KVO
- 現在已知直接修改成員變量時, 不會觸發KVO, 那麼就在修改成員變量的前後添加這兩個方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.person1 willChangeValueForKey:@"age"];
self.person1->_age = 21;
[self.person1 didChangeValueForKey:@"age"];
}
- 運行程序, 點擊ViewController的view, 有如下打印:
<Person: 0x60000001c6c0> - age - {
kind = 1;
new = 21;
old = 1;
} - 年齡
- 所以, 通過調用willChangeValueForKey:和didChangeValueForKey:方法, 就可以手動的調用KVO
注意:
willChangeValueForKey:和didChangeValueForKey:, 兩個方法必須同時出現, 如果只有一個, 將不會觸發KVO
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1->_age = 21;
[self.person1 didChangeValueForKey:@"age"];
}
- 運行程序, 點擊屏幕後, 沒有任何打印