本文Demo傳送門:CMKVODemo
摘要:這篇文章首先介紹KVO的基本用法,接著探究 KVO (Key-Value Observing) 實現機制,並利用 runtime 模擬實現 KVO的監聽機制:一種Block方式回撥,一種Delegate回撥。同時,本文也會總結KVO實現過程中與 runtime 相關的API用法。
1. KVO理論基礎
1.1 KVO的基本用法
步驟
❶ 註冊觀察者,實施監聽
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
複製程式碼
❷ 回撥方法,在這裡處理屬性發生的變化
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
//...實現監聽處理
}
複製程式碼
❸ 移除觀察者
[self removeObserver:self forKeyPath:@“age"];
複製程式碼
綜合例子
//新增觀察者
_person = [[Person alloc] init];
[_person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
複製程式碼
//KVO回撥方法
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
{
NSLog(@"%@物件的%@屬性改變了,change字典為:%@",object,keyPath,change);
NSLog(@"屬性新值為:%@",change[NSKeyValueChangeNewKey]);
NSLog(@"屬性舊值為:%@",change[NSKeyValueChangeOldKey]);
}
複製程式碼
//移除觀察者
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"age"];
}
複製程式碼
利用了KVO實現鍵值監聽的第三方框架
1.2 KVO的實現原理
KVO 是 Objective-C 對 觀察者模式(Observer Pattern)的實現。當被觀察物件的某個屬性發生更改時,觀察者物件會獲得通知。有意思的是,你不需要給被觀察的物件新增任何額外程式碼,就能使用 KVO 。這是怎麼做到的?
KVO 的實現也依賴於 Objective-C 強大的 Runtime 。Apple 的文件有簡單提到過 KVO 的實現。Apple 的文件唯一有用的資訊是:被觀察物件的 isa 指標會指向一箇中間類,而不是原來真正的類。Apple 並不希望過多暴露 KVO 的實現細節。
不過,要是你用 runtime 提供的方法去深入挖掘,所有被掩蓋的細節都會原形畢露。Mike Ash 早在 2009 年就做了這麼個探究,瞭解更多 點這裡。
KVO 的實現:
當你觀察一個物件時,一個新的類會動態被建立。這個類繼承自該物件的原本的類,並重寫了被觀察屬性的 setter
方法。自然,重寫的 setter
方法會負責在呼叫原 setter
方法之前和之後,通知所有觀察物件值的更改。最後把這個物件的 isa
指標 ( isa 指標告訴 Runtime 系統這個物件的類是什麼 ) 指向這個新建立的子類,物件就神奇的變成了新建立的子類的例項。
這個中間類,繼承自原本的那個類。不僅如此,Apple 還重寫了 -class
方法,企圖欺騙我們這個類沒有變,就是原本那個類。更具體的資訊,去跑一下 Mike Ash 的那篇文章裡的程式碼就能明白,這裡就不再重複。
1.3 KVO的不足
KVO 很強大,沒錯。知道它內部實現,或許能幫助更好地使用它,或在它出錯時更方便除錯。但官方實現的 KVO 提供的 API 實在不怎麼樣。
比如,你只能通過重寫 -observeValueForKeyPath:ofObject:change:context:
方法來獲得通知。想要提供自定義的 selector ,不行;想要傳一個 block ,門都沒有。而且你還要處理父類的情況 - 父類同樣監聽同一個物件的同一個屬性。但有時候,你不知道父類是不是對這個訊息有興趣。雖然 context 這個引數就是幹這個的,也可以解決這個問題 - 在 -addObserver:forKeyPath:options:context:
傳進去一個父類不知道的 context。但總覺得框在這個 API 的設計下,程式碼寫的很彆扭。至少至少,也應該支援 block 吧。
有不少人都覺得官方 KVO 不好使的。Mike Ash 的 Key-Value Observing Done Right,以及獲得不少分享討論的 KVO Considered Harmful 都把 KVO 拿出來吊打了一番。所以在實際開發中 KVO 使用的情景並不多,更多時候還是用 Delegate 或 NotificationCenter。
2. Block實現KVO
2.1 模擬實現
注意:以下都是同一個檔案:NSObject+Block_KVO.m中寫的
- 匯入標頭檔案,並定義兩個靜態變數
#import "NSObject+Block_KVO.h"
#import <objc/runtime.h>
#import <objc/message.h>
//as prefix string of kvo class
static NSString * const kCMkvoClassPrefix_for_Block = @"CMObserver_";
static NSString * const kCMkvoAssiociateObserver_for_Block = @"CMAssiociateObserver";
複製程式碼
- 暴露給呼叫者為被觀察物件新增KVO方法
- (void)CM_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(CM_ObservingHandler)observedHandler
{
//step 1 get setter method, if not, throw exception
SEL setterSelector = NSSelectorFromString(setterForGetter(key));
Method setterMethod = class_getInstanceMethod([self class], setterSelector);
if (!setterMethod) {
@throw [NSException exceptionWithName: NSInvalidArgumentException reason: [NSString stringWithFormat: @"unrecognized selector sent to instance %@", self] userInfo: nil];
return;
}
//自己的類作為被觀察者類
Class observedClass = object_getClass(self);
NSString * className = NSStringFromClass(observedClass);
//如果被監聽者沒有CMObserver_,那麼判斷是否需要建立新類
if (![className hasPrefix: kCMkvoClassPrefix_for_Block]) {
//【程式碼①】
observedClass = [self createKVOClassWithOriginalClassName: className];
//【API註解①】
object_setClass(self, observedClass);
}
//add kvo setter method if its class(or superclass)hasn't implement setter
if (![self hasSelector: setterSelector]) {
const char * types = method_getTypeEncoding(setterMethod);
//【程式碼②】
class_addMethod(observedClass, setterSelector, (IMP)KVO_setter, types);
}
//add this observation info to saved new observer
//【程式碼③】
CM_ObserverInfo_for_Block * newInfo = [[CM_ObserverInfo_for_Block alloc] initWithObserver: observer forKey: key observeHandler: observedHandler];
//【程式碼④】【API註解③】
NSMutableArray * observers = objc_getAssociatedObject(self, (__bridge void *)kCMkvoAssiociateObserver_for_Block);
if (!observers) {
observers = [NSMutableArray array];
objc_setAssociatedObject(self, (__bridge void *)kCMkvoAssiociateObserver_for_Block, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[observers addObject: newInfo];
}
複製程式碼
- 其中【程式碼①】的意思是,被觀察的類如果是被觀察物件本來的類,那麼,就要專門依據本來的類新建一個新的子類,區分是否這個子類的標記是帶有
kCMkvoClassPrefix_for_Block
的字首。怎樣新建一個子類?程式碼如下所示:
- (Class)createKVOClassWithOriginalClassName: (NSString *)className
{
NSString * kvoClassName = [kCMkvoClassPrefix stringByAppendingString: className];
Class observedClass = NSClassFromString(kvoClassName);
if (observedClass) { return observedClass; }
//建立新類,並且新增CMObserver_為類名新字首
Class originalClass = object_getClass(self);
//【API註解②】
Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0);
//獲取監聽物件的class方法實現程式碼,然後替換新建類的class實現
Method classMethod = class_getInstanceMethod(originalClass, @selector(class));
const char * types = method_getTypeEncoding(classMethod);
class_addMethod(kvoClass, @selector(class), (IMP)kvo_Class, types);
objc_registerClassPair(kvoClass);
return kvoClass;
}
複製程式碼
- 另外【程式碼②】的意思是,將原來的setter方法替換一個新的setter方法(這就是runtime的黑魔法,Method Swizzling)。那麼新的setter方法又是什麼呢?如下所示:
#pragma mark -- Override setter and getter Methods
static void KVO_setter(id self, SEL _cmd, id newValue)
{
NSString * setterName = NSStringFromSelector(_cmd);
NSString * getterName = getterForSetter(setterName);
if (!getterName) {
@throw [NSException exceptionWithName: NSInvalidArgumentException reason: [NSString stringWithFormat: @"unrecognized selector sent to instance %p", self] userInfo: nil];
return;
}
id oldValue = [self valueForKey: getterName];
struct objc_super superClass = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
[self willChangeValueForKey: getterName];
void (*objc_msgSendSuperKVO)(void *, SEL, id) = (void *)objc_msgSendSuper;
objc_msgSendSuperKVO(&superClass, _cmd, newValue);
[self didChangeValueForKey: getterName];
//獲取所有監聽回撥物件進行回撥
NSMutableArray * observers = objc_getAssociatedObject(self, (__bridge const void *)kCMkvoAssiociateObserver_for_Block);
for (CM_ObserverInfo_for_Block * info in observers) {
if ([info.key isEqualToString: getterName]) {
dispatch_async(dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
info.handler(self, getterName, oldValue, newValue);
});
}
}
}
複製程式碼
- 【程式碼③】是新建一個觀察者類。這個類的實現寫在同一個class,相當於匯入一個類:CM_ObserverInfo_for_Block。這個類的作用是觀察者,並在初始化的時候負責呼叫者傳過來的Block回撥。如下,
self.handler = handler;
即負責回撥。
@interface CM_ObserverInfo_for_Block : NSObject
@property (nonatomic, weak) NSObject * observer;
@property (nonatomic, copy) NSString * key;
@property (nonatomic, copy) CM_ObservingHandler handler;
@end
@implementation CM_ObserverInfo_for_Block
- (instancetype)initWithObserver: (NSObject *)observer forKey: (NSString *)key observeHandler: (CM_ObservingHandler)handler
{
if (self = [super init]) {
_observer = observer;
self.key = key;
self.handler = handler;
}
return self;
}
@end
複製程式碼
- 【程式碼④】的作用是,以及已知的“屬性名”,型別為NSString的靜態變數
kCMkvoAssiociateObserver_for_Block
來獲取這個“屬性”觀察者陣列(這個其實並不是真正意義的屬性,屬於runtime關聯物件的知識範疇,可理解成 觀察者陣列 這樣一個屬性)。其中,關於(__bridge void *)
的知識後面會講到。
呼叫者:利用上面的API為被觀察者新增KVO
- VC呼叫API
#import "NSObject+Block_KVO.h"
//...........
- (void)viewDidLoad {
[super viewDidLoad];
ObservedObject * object = [ObservedObject new];
object.observedNum = @8;
#pragma mark - Observed By Block
[object CM_addObserver: self forKey: @"observedNum" withBlock: ^(id observedObject, NSString *observedKey, id oldValue, id newValue) {
NSLog(@"Value had changed yet with observing Block");
NSLog(@"oldValue---%@",oldValue);
NSLog(@"newValue---%@",newValue);
}];
object.observedNum = @10;
}
複製程式碼
2.2 runtime關鍵API解析
【API註解①】:object_setClass
我們可以在執行時建立新的class,這個特性用得不多,但其實它還是很強大的。你能通過它建立新的子類,並新增新的方法。
但這樣的一個子類有什麼用呢?別忘了Objective-C的一個關鍵點:object內部有一個叫做isa的變數指向它的class。這個變數可以被改變,而不需要重新建立。然後就可以新增新的ivar和方法了。可以通過以下命令來修改一個object的class
object_setClass(myObject, [MySubclass class]);
複製程式碼
這可以用在Key Value Observing。當你開始observing an object時,Cocoa會建立這個object的class的subclass,然後將這個object的isa指向新建立的subclass。
【API註解②】:objc_allocateClassPair
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name,
size_t extraBytes)
複製程式碼
- 看起來一切都很簡單,執行時建立類只需要三步:
1、為"class pair"分配空間(使用
objc_allocateClassPair
). 2、為建立的類新增方法和成員(上例使用class_addMethod
新增了一個方法)。 3、註冊你建立的這個類,使其可用(使用objc_registerClassPair
)。
為什麼這裡1和3都說到pair,我們知道pair的中文意思是一對,這裡也就是一對類,那這一對類是誰呢?他們就是Class、MetaClass。
- 需要配置的引數為:
1、第一個引數:作為新類的超類,或用Nil來建立一個新的根類。
2、第二個引數:新類的名稱
3、第三個引數:一般傳0
【API註解③】:(__bridge void *)
在 ARC 有效時,通過 (__bridge void *)
轉換 id 和 void * 就能夠相互轉換。為什麼轉換?這是因為objc_getAssociatedObject
的引數要求的。先看一下它的API:
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
複製程式碼
可以知道,這個“屬性名”的key是必須是一個void *
型別的引數。所以需要轉換。關於這個轉換,下面給一個轉換的例子:
id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
id o = (__bridge id)p;
複製程式碼
關於這個轉換可以瞭解更多:ARC 型別轉換:顯示轉換 id 和 void *
當然,如果不通過轉換使用這個API,就需要這樣使用:
- 方式1:
objc_getAssociatedObject(self, @"AddClickedEvent");
複製程式碼
- 方式2:
static const void *registerNibArrayKey = ®isterNibArrayKey;
複製程式碼
NSMutableArray *array = objc_getAssociatedObject(self, registerNibArrayKey);
複製程式碼
- 方式3:
static const char MJErrorKey = '\0';
複製程式碼
objc_getAssociatedObject(self, &MJErrorKey);
複製程式碼
- 方式4:
+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
MJProperty *propertyObj = objc_getAssociatedObject(self, property);
//省略
}
複製程式碼
其中objc_property_t
是runtime的型別
typedef struct objc_property *objc_property_t;
複製程式碼
2.3 runtime其它API解析
剩下的就是runtime的比較常見API了,這裡就不按照上面程式碼的順序的講解了。這裡只做按runtime的知識範疇將這些API做一個分類:
- runtime:關聯物件相關API
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
複製程式碼
- runtime:方法替換相關API
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
object_getClass(id _Nullable obj)
Method class_getInstanceMethod(Class cls, SEL name);
const char * method_getTypeEncoding(Method m);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
複製程式碼
- runtime:訊息機制相關API
objc_msgSendSuper
複製程式碼
- KVO
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
複製程式碼
3. 擴充:Delegate實現KVO
注意:以下都是同一個檔案:NSObject+Block_Delegate.m中寫的
- 觀察類CM_ObserverInfo需要改一個屬性,將Block改為一個Delegate。
@interface CM_ObserverInfo : NSObject
@property (nonatomic, weak) NSObject * observer;
@property (nonatomic, copy) NSString * key;
//修改這裡
@property (nonatomic, assign) id <ObserverDelegate> observerDelegate;
@end
複製程式碼
- 同樣,觀察類CM_ObserverInfo初始化的時候也需要相應初始這個新屬性。
@implementation CM_ObserverInfo
- (instancetype)initWithObserver: (NSObject *)observer forKey: (NSString *)key
{
if (self = [super init]) {
_observer = observer;
self.key = key;
//修改這裡
self.observerDelegate = (id<ObserverDelegate>)observer;
}
return self;
}
@end
複製程式碼
- 暴露給呼叫者為被觀察物件新增KVO方法:不需要傳Block了。
#pragma mark -- NSObject Category(KVO Reconstruct)
@implementation NSObject (Block_KVO)
- (void)CM_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(CM_ObservingHandler)observedHandler
{
//...省略
//add this observation info to saved new observer
//修改這裡
CM_ObserverInfo * newInfo = [[CM_ObserverInfo alloc] initWithObserver: observer forKey: key];
//...省略
}
複製程式碼
呼叫者:利用上面的API為被觀察者新增KVO
- VC呼叫API
#import "NSObject+Delegate_KVO.h"
//...........
- (void)viewDidLoad {
[super viewDidLoad];
ObservedObject * object = [ObservedObject new];
object.observedNum = @8;
#pragma mark - Observed By Delegate
[object CM_addObserver: self forKey: @"observedNum"];
object.observedNum = @10;
}
複製程式碼
- VC實現代理方法
#pragma mark - ObserverDelegate
-(void)CM_ObserveValueForKeyPath:(NSString *)keyPath ofObject:(id)object oldValue:(id)oldValue newValue:(id)newValue{
NSLog(@"Value had changed yet with observing Delegate");
NSLog(@"oldValue---%@",oldValue);
NSLog(@"newValue---%@",newValue);
}
複製程式碼
4. runtime瞭解更多
筆者另外寫了runtime的原理與實踐。如果想了解runtime的更多知識,可以選擇閱讀這些文章: