一、KVO原理
1. KVO 簡介
KVO 是 Objective-C 對觀察者設計模式的一種實現。KVO 提供一種機制,指定一個被觀察對象,當對象某個屬性發生改變時,對象會獲得通知,并做出相應處理;
在 MVC 設計架構的項目,KVO機制很適合實現 model 模型和 View 視圖之間的通訊。
2. 實現原理
KVO 的實現依賴于 Objective-C 強大的 Runtime。
當觀察某對象α時,KVO 機制動態創建一個對象α當前類的子類,并為這個新的子類重寫被觀察屬性 keyPath 的 setter 方法。setter 方法隨后負責通知觀察對象屬性的改變狀況。
3. 深入剖析:
Apple 使用了 isa-swizzling 來實現 KVO。當觀察對象α時,KVO 機制動態創建一個新的名為:NSKVONotifying_α的新類,該類繼承自對象α的本類,且 KVO 為 NSKVONotifying_α重寫觀察屬性的 setter 方法,setter 方法會負責在調用原setter 方法之前和之后,通知所有觀察對象屬性值的更改情況。
NSKVONotifying_α類剖析:在這個過程,被觀察對象的 isa 指針從原來的α類,被 KVO 機制修改為指向系統新創建的子類 NSKVONotifying_α類,來實現當前類屬性值改變的監聽。
所以當我們從應用層面上看來,完全沒有意識到有新的類出現,這是系統對 KVO 的底層實現過程,此時如果我們創建一個新的名為“NSKVONotifying_α”的類,就會發現系統運行到注冊 KVO 的那段代碼時程序就會崩潰。因為系統在注冊監聽的時候創建了名為 NSKVONotifying_α的中間類,并指向這個中間類。
(isa 指針的作用: 每個對象都有 isa 指針,指向該對象的類,它告訴 Runtime 系統這個對象的類是什么。所以對象注冊為觀察者時,isa 指針指向新子類,那么這個被觀察的對象就神奇地變成新子類的對象(或實例)了。)因而在該對象上對 setter 的調用就會調用已重寫的 setter,從而激活鍵值通知機制。
子類 setter 方法剖析:KVO 的鍵值觀察通知依賴于 NSOjbect 的兩個方法:willChangeValueForKey:和 didChangeValueForKey:,在存取數值的前后分別調用2個方法:
被觀察屬性發生改變之前,willChangeValueForKey:被調用,通知系統該 keyPath 的屬性值即將變更;當改變發生后,didChangeValueForKey:被調用,通知系統該 keyPath 的屬性值已經變更;之后,observeValueForKey:ofObject:change:context:也會被調用。且重寫觀察屬性的 setter 方法這種繼承方式的注入是在運行時而不是編譯時實現的。
KVO 為子類的觀察者屬性重寫調用存取方法的工作原理在代碼中相當于:
- (void)setName:(NSString *)newName{
[self willChangeValueForKey:@"name"];
[super setValue:newName forKey:@"name"];
[self didiChangeValueForKey:@"name"];
}
4.特點:
觀察者觀察的是屬性,只有遵循 KVO 變更屬性值的方式才會執行 KVO 的回調方法,例如是否執行了 setter 方法、或者是否使用了 KVC 賦值。如果賦值沒有通過 setter 方法或者 KVC,而是直接修改屬性對應的成員變量,例如:僅調用_name = @"newName",這事是不會觸發 KVO 機制,更加不會調用回調方法的。所以使用 KVO 機制的前提是遵循 KVO 的屬性設置方式來變更屬性值。
5.步驟
- 1.注冊觀察者,實施監聽;
- 2.在回調方法中處理屬性發生的變化;
- 3.移除觀察者
6. 實現方法
A. 注冊觀察者:
//第一個參數 observer:觀察者(這里觀察 self.myKVO 對象的屬性變化)
//第二個參數 keyPath: 被觀察的屬性名稱(這里 self.myKVO 中 num屬性值的改變)
//第三個參數 options:觀察屬性的新值、舊值等的一些配置(枚舉值,可以根據需要設置,例如這里可以使用兩項)
//第四個參數 context: 上下文,可以為 kvo 的回調方法傳值(例如設定為一個放置數據的字典)
[self.myKVO addObserver:self forKeyPath:@"num" options: NSKeyValueObservingOptionOld|NSKeyValueObservingOpitonNew context:nil];
B.屬性(keyPath)的值發生變化時,收到通知,調用以下方法:
//keyPath:屬性名稱
//object: 被觀察的對象
//change:變化前后的值都存儲在 change 字典中
//context: 注冊觀察者時,context 傳過來的值
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *) change context:(void *)context{
}
7.拓展
1.KVC 和 KVO 的不同
KVC(鍵值編碼),即 Key-Value Coding,一個非正式的 Protocol,使用字符串(鍵)訪問一個對象實例變量的機制。而不是通過調用 Setter、Getter 方法等顯式的存取方法去訪問。KVO(鍵值監聽),即 Key-Value Observing,它提供一種機制,當指定的對象的屬性被修改后,對象就會接受到通知,前提是執行了 setter 方法、或者使用了 KVC 賦值。
2.和 notification(通知)的區別?
notification 比 KVO 多了發送通知的一步。
兩者都是一對多,但是對象之間直接的交互,notification 明顯很多,需要 notificationCenter 來作為中間交互。而 KVO 如我們介紹的,設置觀察者->處理屬性變化,
notification 的優點是監聽不局限與屬性的變化,還可以對多種多樣的狀態變化進行監聽,監聽范圍廣,例如鍵盤、前后臺等系統通知的使用也更更顯靈活方便。
- 與 delegate 的不同
和 delegate 一樣,KVO 和 NSNotification 的作用都是類與類之間的通信。但是與 delegate 不同的是:
這兩個都是負責發送接收通知,剩下的事情由系統處理,所以不同返回值;而 delegate 則需要通信的對象通過變量(代理)聯系;
delegate 一般是一對一,而這兩個可以一對多。
二、Category實現原理
1. 簡介
category 是 Objective-C 2.0 之后添加的語言特性。主要作用是為已經存在的類添加方法。還有以下兩種用法:
可以把類的實現分開在幾個不同的文件里面。1)可以減少單個文件的體積, 2)可以把不同的功能組織到不同的 category 里 3)可以由多個開發者共同完成一個類 4)可以按需加載想要的 category 等等
聲明私有方法。
2. 比較 category 和 extension
extension 在編譯期決定,它是類的一部分,在編譯期和頭文件里的@interface 以及實現文件里的@implement 一起形成一個完整的類,它伴隨類的產生而產生,消亡而消亡。extension 一般用來隱藏類的私有信息,你必須有一個類的源碼才能為一個類添加 extension,所以無法為系統的類添加 extension。
但是 category 實在運行期決定的,就 category 和 extension 的區別來看,extension 可以添加實例變量,而 category 無法添加實例變量(因為在運行期,對象的內存布局已經確定,如果添加實例變量就會破壞類的內部布局,這對編譯型語言來說是災難性的)。
3. category 原理
所有 OC 類和對象,在 Runtime 層都是用 struct 表示的,category 也不例外,在 runtime 層,category 用結構體 category_t (在 objc-runtime-new.h 中可以找到此定義),它包括了:
1)類的名字(name)
2)類(cls)
3)category 中所有給類添加的實例方法的列表(instanceMethods)
- category 中所有添加的類方法的列表(classMethods)
5) category 實現的所有協議的列表(Protocols)
6) category 中添加的所有屬性(instanceProperties)
typedef struct category_t {
const char * name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t * classMethods;
struct protocol_list_t * protocols;
struct property_list_t * instanceProperties;
} category_t;
從 category 的定義也可以看出 category 的可為(可以添加實例方法,類方法,甚至可以實現協議,添加屬性)和不可為(無法添加實例變量)。
MyClass.h:
#import <Foundation/Foundation.h>
@interface MyClass:NSObject
- (void)printName;
@end
@interface MyClass(MyAddition)
@property(nonatomic,copy) NSString *name;
- (void)printName;
@end
MyClass.m
#import"MyClass.h"
@implementation MyClass
- (void)printName{
NSLog(@"%@",@"MyClass");
}
@end
@implementation MyClass(MyAddition)
- (void)printName{
NSLog(@"%@",@"MyAddition");
}
4. category 和關聯對象
在 category 里是無法為 category 添加實例變量的。但是我們很多時候需要在 category 中添加和對象關聯的值,這個時候可以求助關聯對象來實現。
MyClass + Category.h:
#import “MyClass.h”
@interface MyClass(Category1)
@property(nonatomic,copy) NSString *name;
@end
MyClass+Category.m:
#import "MyClass+Category.h"
#import <objc/runtime.h>
@implementation MyClass(Category)
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY);
}
-(NSString *)name{
NSString *nameObject = objc_getAssociatedObject(self, "name");
return nameObject;
}
@end