OC底層探索(十六) KVO底層原理

正在登出賬號發表於2020-10-27

OC底層文章彙總

KVO全稱KeyValueObserving,是蘋果提供的一套事件通知機制。允許物件監聽另一個物件特定屬性的改變,並在改變時接收到事件。由於KVO的實現機制,所以對屬性才會發生作用,一般繼承自NSObject的物件都預設支援KVO

KVONSNotificationCenter都是iOS中觀察者模式的一種實現。區別在於,相對於被觀察者觀察者之間的關係,KVO一對一的,而不一對多的。KVO對被監聽物件無侵入性,不需要修改其內部程式碼即可實現監聽。

KVO可以監聽個屬性的變化,也可以監聽集合物件的變化。通過KVCmutableArrayValueForKey:等方法獲得代理物件,當代理物件的內部物件發生改變時,會回撥KVO監聽的方法。集合物件包含NSArrayNSSet

KVO的使用

1、基本使用

@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@property (nonatomic, strong) NSMutableArray *dateArray;
@property (nonatomic, strong) LGStudent *st;
@end

static void *PersonNickContext = &PersonNickContext;

@interface LGViewController ()
@property (nonatomic, strong) LGPerson  *person;
@property (nonatomic, strong) LGStudent *student;
@end

@interface LGViewController ()
@property (nonatomic, strong) LGPerson  *person;
@property (nonatomic, strong) LGStudent *student;
@end

@implementation LGViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person  = [LGPerson new];

    // 1: context : 上下文
  
    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.nick = [NSString stringWithFormat:@"%@+",self.person.nick];
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    // 效能 + 程式碼可讀性
    NSLog(@"%@",change);
}


- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"nick" context:NULL];

}
@end

1.1 監聽屬性name

[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
  • context 上下文

在官方文件中有是這樣介紹的

The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing.

百度翻譯:

addObserver:forKeyPath:options:context: message中的上下文指標包含將在相應的更改通知中傳遞迴觀察者的任意資料。您可以指定NULL並完全依賴於鍵路徑字串來確定更改通知的來源,但是這種方法可能會給其超類出於不同的原因也在觀察相同鍵路徑的物件造成問題。
一種更安全、更可擴充套件的方法是使用content來確保接收到的通知是傳送給觀察者的,而不是超類。
類中唯一命名的靜態變數的地址可以作為一個良好的上下文(content)。在超類或子類中以類似方式選擇的上下文將不太可能重疊。您可以為整個類選擇一個上下文,並依賴通知訊息中的關鍵路徑字串來確定更改的內容。或者,您可以為觀察到的每個鍵路徑建立不同的上下文,這樣就完全不必進行字串比較,從而提高通知解析的效率。

  • 就是說context就相當於通知中的那個key,以更方便、更安全、更可擴充套件的方式。注意:如果在新增監聽時設定了context,那麼刪除時,也需要設定同樣的context。例如:

static void *PersonNickContext = &PersonNickContext;


[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (context == PersonNickContext) {
    }
}

- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"nick" context:PersonNickContext];

}

1.2 改變name的值

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.nick = [NSString stringWithFormat:@"%@+",self.person.nick];
}

1.3 觸發的方法

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    // 效能 + 程式碼可讀性
    NSLog(@"%@",change);
}

1.4 移除監聽

[self.person removeObserver:self forKeyPath:@"nick" context:NULL];

2、陣列觀察

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person  = [LGPerson new];
    // 5: 陣列觀察
    self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // KVC 集合 array
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"dateArray"];

}

3、路徑方式


- (void)viewDidLoad {
    [super viewDidLoad];
    self.person  = [LGPerson new];
    // 4: 路徑處理
    // 下載的進度 = 已下載 / 總下載
    [self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.writtenData += 10;
    self.person.totalData  += 1;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}

- (void)dealloc{
//    [self.person removeObserver:self forKeyPath:@"nick" context:NULL];
    [self.person removeObserver:self forKeyPath:@"downloadProgress"];

}


@implementation LGPerson

// 下載進度 -- 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{
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}

@end

4、手動和自動開關

觀察者預設是自動開啟的,我們也可以手動開啟首頁在觀察類中實現

// 自動開關
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}

再開啟需要觀察的物件

- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}

KVO底層探索

KVO官方文件

1、KVO觀察屬性

探索:OC在類中有屬性和成員變數,那KVO觀察的是屬性還是成員變數呢?

  • 定義LGPerson類,自定義成員變數name與屬性nickName,原始碼如下:
@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;

@end

  • 在最上面的程式碼中再新增nickName的監聽,檢視列印結果

在這裡插入圖片描述

  • 通過列印結果可知,KVO只對屬性進行監聽,對成員變數不監聽
    • 屬性成員變數區別在於屬性存在 settergetter方法,而成員變數沒有。

2、新增完KVO後生成了中間類

2.1 修改ISA指向

  • 分別新增KVO之前和KVO之後打一個斷點,在控制檯輸出以下person物件的ISA。

在這裡插入圖片描述
(由於電腦不在,不方便除錯截圖,圖片來自荒唐的天梯的部落格)

  • 根據列印的內容可以看到,在新增KVO之後,person物件的ISA指向了NSKVONotifying_LGPerson。

2.2 NSKVONotifying_LGPerson是LGPerson的分類

  • 自定義檢視本類和子類的方法
#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
    
    // 註冊類的總數
    int count = objc_getClassList(NULL, 0);
    // 建立一個陣列, 其中包含給定物件
    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);
}

  • 分別在新增KVO之前和KVO之後呼叫該方法,檢視以下列印結果。

在這裡插入圖片描述

  • 根據列印我們發現 NSKVONotifying_LGPerson是LGPerson的分類。

2.3 還原ISA指向

思考:person物件的ISA指向發生了改變,那還會不會在還原ISA指向呢?

  • 分別在刪除前和刪除後打一個斷點,在控制檯分別列印person的ISA

在這裡插入圖片描述

  • 根據列印我們發現刪除後person的ISA又重新指向了LGPerson

思考: 那麼isa還原後,NSKVONotifying_LGPerson會不會刪除呢?

  • 在刪除後呼叫自定義的方法,檢視列印,中間類是沒有被移除的。

在這裡插入圖片描述

總結:

  • 新增KVO後,物件的ISA指向了以NSKVONotifying_NSKVONotifying_開頭的中間類,且是原類的分類。
  • 在刪除KVO後將ISA還原。
  • 刪除KVO後,生成的中間類不會被刪除,以便下次使用。

3、探索中間類

3.1 檢視中間類中的方法

  • 自定義方法,列印輸出中間類的所有的方法
#pragma mark - 遍歷方法-ivar-property
- (void)printClassAllMethod:(Class)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);
}

  • 呼叫列印

在這裡插入圖片描述

  • 從列印結果可知,在NSKVONotifying_LGPerson類中新增了四個方法,分別為:setNickName、class、dealloc、_isKVOA這四個方法。

    • _isKVOA :判斷當前是否為KVO類
    • dealloc: 釋放
    • setNickName :nickName屬性的setter方法
    • class:clasaa方法

3.2 setNickName 中做了什麼

探索setNikeName是重寫還是繼承

- 定義一個類,列印檢視。
	
	- 定義一個LGStudent,並繼承與LGPerson,不重寫setNikeName,列印檢視發現並沒有setNikeName的列印。

 - 在探索類的結果時,methodlist只存放自己的方法,如果是繼承,那麼就需要去父類的methodList中查詢。

重寫setNikeName方法中具體做了什麼呢?

大膽猜測一下,首先,在呼叫NSKVONotifying_LGPerson重寫setter方法的時候,改變的是其父類LGPerson的nickName的值,那麼在重寫的setter方法中一定有對父類nickName進行傳值的操作。

  • 設定觀察self->_pserson->_nickName,在控制檯手動輸入一個斷點。具體命令為:
watchpoint set variable self->_person->_nickName

在這裡插入圖片描述

  • 執行發現進入了斷點,那麼我們猜測的沒有問題。

  • 檢視堆疊中的情況
    在這裡插入圖片描述

  • 堆疊2在斷點NSKeyValueWillChange方法之後執行的
    在這裡插入圖片描述

3.3 總結

  1. 中間類中是對屬性的set方法進行了重寫;
  2. 重寫的set方法中,是對父類的屬性進行賦值,並將ISA還原。

相關文章