小碼哥iOS學習筆記第4天: KVO的本質

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"];
}
  • 運行程序, 點擊屏幕後, 沒有任何打印
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章