在iOS開發中,我們可以通過KVO機制來監聽某個物件的某個屬性的變化。
用過KVO的同學都應該知道,KVO的回撥是以代理的形式實現的:在給某個物件新增觀察以後,需要在另外一個地方實現回撥代理方法。這種設計給人感覺比較分散,因此突然想試試用Block來實現KVO,將新增觀察的程式碼和回撥處理的程式碼寫在一起。在學習了ImplementKVO的實現以後,自己也寫了一個:SJKVOController
SJKVOController的用法
只需要引入NSObject+SJKVOController.h
標頭檔案就可以使用SJKVOController。
先看一下它的標頭檔案:
#import <Foundation/Foundation.h>
#import "SJKVOHeader.h"
@interface NSObject (SJKVOController)
//============== add observer ===============//
- (void)sj_addObserver:(NSObject *)observer forKeys:(NSArray <NSString *>*)keys withBlock:(SJKVOBlock)block;
- (void)sj_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(SJKVOBlock)block;
//============= remove observer =============//
- (void)sj_removeObserver:(NSObject *)observer forKeys:(NSArray <NSString *>*)keys;
- (void)sj_removeObserver:(NSObject *)observer forKey:(NSString *)key;
- (void)sj_removeObserver:(NSObject *)observer;
- (void)sj_removeAllObservers;
//============= list observers ===============//
- (void)sj_listAllObservers;
@end
複製程式碼
從上面的API可以看出,這個小輪子:
- 支援一次觀察同一物件的多個屬性。
- 可以一次只觀察一個物件的一個屬性。
- 可以移除對某個物件對多個屬性的觀察。
- 可以移除對某個物件對某個屬性的觀察。
- 可以移除某個觀察自己的物件。
- 可以移除所有觀察自己的物件。
- 列印出所有觀察自己的物件的資訊,包括物件本身,觀察的屬性,setter方法。
下面來結合Demo講解一下如何使用這個小輪子:
在點選上面兩個按鈕中的任意一個,增加觀察:
一次性新增:
- (IBAction)addObserversTogether:(UIButton *)sender {
NSArray *keys = @[@"number",@"color"];
[self.model sj_addObserver:self forKeys:keys withBlock:^(id observedObject, NSString *key, id oldValue, id newValue) {
if ([key isEqualToString:@"number"]) {
dispatch_async(dispatch_get_main_queue(), ^{
self.numberLabel.text = [NSString stringWithFormat:@"%@",newValue];
});
}else if ([key isEqualToString:@"color"]){
dispatch_async(dispatch_get_main_queue(), ^{
self.numberLabel.backgroundColor = newValue;
});
}
}];
}
複製程式碼
分兩次新增:
- (IBAction)addObserverSeparatedly:(UIButton *)sender {
[self.model sj_addObserver:self forKey:@"number" withBlock:^(id observedObject, NSString *key, id oldValue, id newValue) {
dispatch_async(dispatch_get_main_queue(), ^{
self.numberLabel.text = [NSString stringWithFormat:@"%@",newValue];
});
}];
[self.model sj_addObserver:self forKey:@"color" withBlock:^(id observedObject, NSString *key, id oldValue, id newValue) {
dispatch_async(dispatch_get_main_queue(), ^{
self.numberLabel.backgroundColor = newValue;
});
}];
}
複製程式碼
新增以後,點選最下面的按鈕來顯示所有的觀察資訊:
- (IBAction)showAllObservingItems:(UIButton *)sender {
[self.model sj_listAllObservers];
}
複製程式碼
輸出:
SJKVOController[80499:4242749] SJKVOLog:==================== Start Listing All Observers: ====================
SJKVOController[80499:4242749] SJKVOLog:observer item:{observer: <ViewController: 0x7fa1577054f0> | key: color | setter: setColor:}
SJKVOController[80499:4242749] SJKVOLog:observer item:{observer: <ViewController: 0x7fa1577054f0> | key: number | setter: setNumber:}
複製程式碼
在這裡我重寫了description方法,列印出了每個觀察的物件和key,以及setter方法。
現在點選更新按鈕,則會更新model的number和color屬性,從而觸發KVO:
- (IBAction)updateNumber:(UIButton *)sender {
//trigger KVO : number
NSInteger newNumber = arc4random() % 100;
self.model.number = [NSNumber numberWithInteger:newNumber];
//trigger KVO : color
NSArray *colors = @[[UIColor redColor],[UIColor yellowColor],[UIColor blueColor],[UIColor greenColor]];
NSInteger colorIndex = arc4random() % 3;
self.model.color = colors[colorIndex];
}
複製程式碼
我們可以看到中間的Label上面顯示的數字和背景色都在變化,成功實現了KVO:
現在我們移除觀察,點選remove按鈕
- (IBAction)removeAllObservingItems:(UIButton *)sender {
[self.model sj_removeAllObservers];
}
複製程式碼
在移除了所有的觀察者以後,則會列印出:
SJKVOController[80499:4242749] SJKVOLog:Removed all obserbing objects of object:<Model: 0x60000003b700>
複製程式碼
而且如果在這個時候列印觀察者list,則會輸出:
SJKVOController[80499:4242749] SJKVOLog:There is no observers obserbing object:<Model: 0x60000003b700>
複製程式碼
需要注意的是,這裡的移除可以有多種選擇:可以移某個物件的某個key,也可以移除某個物件的幾個keys,為了驗證,我們可以結合list方法來驗證一下移除是否成功:
驗證1:在新增number和color的觀察後,移除nunber的觀察:
- (IBAction)removeAllObservingItems:(UIButton *)sender {
[self.model sj_removeObserver:self forKey:@"number"];
}
複製程式碼
在移除以後,我們呼叫list方法,輸出:
SJKVOController[80850:4278383] SJKVOLog:==================== Start Listing All Observers: ====================
SJKVOController[80850:4278383] SJKVOLog:observer item:{observer: <ViewController: 0x7ffeec408560> | key: color | setter: setColor:}
複製程式碼
現在只有color屬性被觀察了。看一下實際的效果:
我們可以看到,現在只有color在變,而數字沒有變化了,驗證此移除方法正確。
驗證2:在新增number和color的觀察後,移除nunber和color的觀察:
- (IBAction)removeAllObservingItems:(UIButton *)sender {
[self.model sj_removeObserver:self forKeys:@[@"number",@"color"]];
}
複製程式碼
在移除以後,我們呼叫list方法,輸出:
SJKVOController[80901:4283311] SJKVOLog:There is no observers obserbing object:<Model: 0x600000220fa0>
複製程式碼
現在color和number屬性都不被觀察了。看一下實際的效果:
我們可以看到,現在color和number都不變了,驗證此移除方法正確。
OK,現在知道了怎麼用SJKVOController,我下面給大家看一下程式碼:
SJKVOController程式碼解析
先大致講解一下SJKVOController的實現思路:
- 為了減少侵入性,SJKVOController被設計為NSObject的一個分類。
- SJKVOController仿照了KVO的實現思路,在新增觀察以後在執行時動態生成當前類的子類,給這個子類新增被觀察的屬性的set方法並使用isa swizzle的方式將當前物件轉換為當前類的子類的實現。
- 同時,這個子類還使用了關聯物件來儲存一個“觀察項”的set,每一個觀察項封裝了一次觀察的行為(有去重機制):包括觀察自己的物件,自己被觀察的屬性,以及傳進來的block。
- 在當前類,也就是子類的set方法被呼叫的時候做三件事情:
- 第一件事情是使用KVC來找出當前屬性的舊值。
- 第二件事情是呼叫父類(原來的類)的set方法(設新值)。
- 第三件事是根據當前的觀察物件和key,在觀察項set裡面找出對應的block並呼叫。
再來看一下這個小輪子的幾個類:
- SJKVOController:實現KVO主要功能的類。
- SJKVOObserverItem:封裝觀察項的類。
- SJKVOTool:setter和getter的相互轉換和相關執行時查詢方法等。
- SJKVOError:封裝錯誤型別。
- SJKVOHeader:引用了執行時的標頭檔案。
下面開始一個一個來講解每個類的原始碼:
SJKVOController
再看一下標頭檔案:
#import <Foundation/Foundation.h>
#import "SJKVOHeader.h"
@interface NSObject (SJKVOController)
//============== add observer ===============//
- (void)sj_addObserver:(NSObject *)observer forKeys:(NSArray <NSString *>*)keys withBlock:(SJKVOBlock)block;
- (void)sj_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(SJKVOBlock)block;
//============= remove observer =============//
- (void)sj_removeObserver:(NSObject *)observer forKeys:(NSArray <NSString *>*)keys;
- (void)sj_removeObserver:(NSObject *)observer forKey:(NSString *)key;
- (void)sj_removeObserver:(NSObject *)observer;
- (void)sj_removeAllObservers;
//============= list observers ===============//
- (void)sj_listAllObservers;
@end
複製程式碼
每個方法的意思相信讀者已經能看懂了,現在講一下具體的實現。從sj_addObserver:forKey withBlock:
開始:
sj_addObserver:forKey withBlock:方法:
除去一些錯誤的判斷,該方法作了下面幾件事情:
1.判斷當前被觀察的類是否存在與傳入key對應的setter方法:
SEL setterSelector = NSSelectorFromString([SJKVOTool setterFromGetter:key]);
Method setterMethod = [SJKVOTool objc_methodFromClass:[self class] selector:setterSelector];
//error: no corresponding setter mothod
if (!setterMethod) {
SJLog(@"%@",[SJKVOError errorNoMatchingSetterForKey:key]);
return;
}
複製程式碼
2. 如果有,判斷當前被觀察到類是否已經是KVO類(在KVO機制中,如果某個物件一旦被觀察,則這個物件就變成了帶有包含KVO字首的類的例項)。如果已經是KVO類,則將當前例項的isa指標指向其父類(最開始被觀察的類):
//get original class(current class,may be KVO class)
NSString *originalClassName = NSStringFromClass(OriginalClass);
//如果當前的類是帶有KVO字首的類(也就是已經被觀察到類),則需要將KVO字首的類刪除,並講
if ([originalClassName hasPrefix:SJKVOClassPrefix]) {
//now,the OriginalClass is KVO class, we should destroy it and make new one
Class CurrentKVOClass = OriginalClass;
object_setClass(self, class_getSuperclass(OriginalClass));
objc_disposeClassPair(CurrentKVOClass);
originalClassName = [originalClassName substringFromIndex:(SJKVOClassPrefix.length)];
}
複製程式碼
3. 如果不是KVO類(說明當前例項沒有被觀察),則建立一個帶有KVO字首的類,並將當前例項的isa指標指向這個新建的類:
//create a KVO class
Class KVOClass = [self createKVOClassFromOriginalClassName:originalClassName];
//swizzle isa from self to KVO class
object_setClass(self, KVOClass);
複製程式碼
看一下如何新建一個新的類:
- (Class)createKVOClassFromOriginalClassName:(NSString *)originalClassName
{
NSString *kvoClassName = [SJKVOClassPrefix stringByAppendingString:originalClassName];
Class KVOClass = NSClassFromString(kvoClassName);
// KVO class already exists
if (KVOClass) {
return KVOClass;
}
// if there is no KVO class, then create one
KVOClass = objc_allocateClassPair(OriginalClass, kvoClassName.UTF8String, 0);//OriginalClass is super class
// pretending to be the original class:return the super class in class method
Method clazzMethod = class_getInstanceMethod(OriginalClass, @selector(class));
class_addMethod(KVOClass, @selector(class), (IMP)return_original_class, method_getTypeEncoding(clazzMethod));
// finally, register this new KVO class
objc_registerClassPair(KVOClass);
return KVOClass;
}
複製程式碼
4. 檢視觀察項set,如果這個set裡面有已經儲存的觀察項,則需要新建一個空的觀察項set,將已經儲存的觀察項放入這個新建的set裡面:
//if we already have some history observer items, we should add them into new KVO class
NSMutableSet* observers = objc_getAssociatedObject(self, &SJKVOObservers);
if (observers.count > 0) {
NSMutableSet *newObservers = [[NSMutableSet alloc] initWithCapacity:5];
objc_setAssociatedObject(self, &SJKVOObservers, newObservers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
for (SJKVOObserverItem *item in observers) {
[self KVOConfigurationWithObserver:item.observer key:item.key block:item.block kvoClass:KVOClass setterSelector:item.setterSelector setterMethod:setterMethod];
}
}
複製程式碼
看一下如何儲存觀察項的:
- (void)KVOConfigurationWithObserver:(NSObject *)observer key:(NSString *)key block:(SJKVOBlock)block kvoClass:(Class)kvoClass setterSelector:(SEL)setterSelector setterMethod:(Method)setterMethod
{
//add setter method in KVO Class
if(![SJKVOTool detectClass:OriginalClass hasSelector:setterSelector]){
class_addMethod(kvoClass, setterSelector, (IMP)kvo_setter_implementation, method_getTypeEncoding(setterMethod));
}
//add item of this observer&&key pair
[self addObserverItem:observer key:key setterSelector:setterSelector setterMethod:setterMethod block:block];
}
複製程式碼
這裡首先給KVO類增加了setter方法:
//implementation of KVO setter method
void kvo_setter_implementation(id self, SEL _cmd, id newValue)
{
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = [SJKVOTool getterFromSetter:setterName];
if (!getterName) {
SJLog(@"%@",[SJKVOError errorTransferSetterToGetterFaildedWithSetterName:setterName]);
return;
}
// create a super class of a specific instance
Class superclass = class_getSuperclass(OriginalClass);
struct objc_super superclass_to_call = {
.super_class = superclass, //super class
.receiver = self, //insatance of this class
};
// cast method pointer
void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
// call super`s setter, the supper is the original class
objc_msgSendSuperCasted(&superclass_to_call, _cmd, newValue);
// look up observers and call the blocks
NSMutableSet *observers = objc_getAssociatedObject(self,&SJKVOObservers);
if (observers.count <= 0) {
SJLog(@"%@",[SJKVOError errorNoObserverOfObject:self]);
return;
}
//get the old value
id oldValue = [self valueForKey:getterName];
for (SJKVOObserverItem *item in observers) {
if ([item.key isEqualToString:getterName]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//call block
item.block(self, getterName, oldValue, newValue);
});
}
}
}
複製程式碼
然後例項化對應的觀察項:
- (void)addObserverItem:(NSObject *)observer
key:(NSString *)key
setterSelector:(SEL)setterSelector
setterMethod:(Method)setterMethod
block:(SJKVOBlock)block
{
NSMutableSet *observers = objc_getAssociatedObject(self, &SJKVOObservers);
if (!observers) {
observers = [[NSMutableSet alloc] initWithCapacity:10];
objc_setAssociatedObject(self, &SJKVOObservers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
SJKVOObserverItem *item = [[SJKVOObserverItem alloc] initWithObserver:observer Key:key setterSelector:setterSelector setterMethod:setterMethod block:block];
if (item) {
[observers addObject:item];
}
}
複製程式碼
5. 判斷新的觀察是否會與已經儲存的觀察項重複(當觀察物件和key一致的時候),如果重複,則不新增新的觀察:
/ /ignore same observer and key:if the observer and key are same with saved observerItem,we should not add them one more time
BOOL findSameObserverAndKey = NO;
if (observers.count>0) {
for (SJKVOObserverItem *item in observers) {
if ( (item.observer == observer) && [item.key isEqualToString:key]) {
findSameObserverAndKey = YES;
}
}
}
if (!findSameObserverAndKey) {
[self KVOConfigurationWithObserver:observer key:key block:block kvoClass:KVOClass setterSelector:setterSelector setterMethod:setterMethod];
}
複製程式碼
而一次性新增多個key的方法,也只是呼叫多次一次性新增單個key的方法罷了:
- (void)sj_addObserver:(NSObject *)observer
forKeys:(NSArray <NSString *>*)keys
withBlock:(SJKVOBlock)block
{
//error: keys array is nil or no elements
if (keys.count == 0) {
SJLog(@"%@",[SJKVOError errorInvalidInputObservingKeys]);
return;
}
//one key corresponding to one specific item, not the observer
[keys enumerateObjectsUsingBlock:^(NSString * key, NSUInteger idx, BOOL * _Nonnull stop) {
[self sj_addObserver:observer forKey:key withBlock:block];
}];
}
複製程式碼
關於移除觀察的實現,只是在觀察項set裡面找出封裝了對應的觀察物件和key的觀察項就可以了:
- (void)sj_removeObserver:(NSObject *)observer
forKey:(NSString *)key
{
NSMutableSet* observers = objc_getAssociatedObject(self, &SJKVOObservers);
if (observers.count > 0) {
SJKVOObserverItem *removingItem = nil;
for (SJKVOObserverItem* item in observers) {
if (item.observer == observer && [item.key isEqualToString:key]) {
removingItem = item;
break;
}
}
if (removingItem) {
[observers removeObject:removingItem];
}
}
}
複製程式碼
再看一下移除所有觀察者:
- (void)sj_removeAllObservers
{
NSMutableSet* observers = objc_getAssociatedObject(self, &SJKVOObservers);
if (observers.count > 0) {
[observers removeAllObjects];
SJLog(@"SJKVOLog:Removed all obserbing objects of object:%@",self);
}else{
SJLog(@"SJKVOLog:There is no observers obserbing object:%@",self);
}
}
複製程式碼
SJKVOObserverItem
這個類負責封裝每一個觀察項的資訊,包括:
- 觀察者物件。
- 被觀察的key。
- setter方法名(SEL)
- setter方法(Method)
- 回撥的block
需要注意的是:
在這個小輪子裡,對於同一個物件可以觀察不同的key的情況,是將這兩個key區分開來的,是屬於不同的觀察項。所以應該用不同的SJKVOObserverItem
例項來封裝。
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
typedef void(^SJKVOBlock)(id observedObject, NSString *key, id oldValue, id newValue);
@interface SJKVOObserverItem : NSObject
@property (nonatomic, strong) NSObject *observer;
@property (nonatomic, copy) NSString *key;
@property (nonatomic, assign) SEL setterSelector;
@property (nonatomic, assign) Method setterMethod;
@property (nonatomic, copy) SJKVOBlock block;
- (instancetype)initWithObserver:(NSObject *)observer Key:(NSString *)key setterSelector:(SEL)setterSelector setterMethod:(Method)setterMethod block:(SJKVOBlock)block;
@end
複製程式碼
SJKVOTool
這個類負責setter方法與getter方法相互轉換,以及和執行時相關的操作,服務於SJKVOController
。看一下它的標頭檔案:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <objc/message.h>
@interface SJKVOTool : NSObject
//setter <-> getter
+ (NSString *)getterFromSetter:(NSString *)setter;
+ (NSString *)setterFromGetter:(NSString *)getter;
//get method from a class by a specific selector
+ (Method)objc_methodFromClass:(Class)cls selector:(SEL)selector;
//check a class has a specific selector or not
+ (BOOL)detectClass:(Class)cls hasSelector:(SEL)selector;
@end
複製程式碼
##SJKVOError
這個小輪子仿照了JSONModel的錯誤管理方式,用單獨的一個類SJKVOError
來返回各種錯誤:
#import <Foundation/Foundation.h>
typedef enum : NSUInteger {
SJKVOErrorTypeNoObervingObject,
SJKVOErrorTypeNoObervingKey,
SJKVOErrorTypeNoObserverOfObject,
SJKVOErrorTypeNoMatchingSetterForKey,
SJKVOErrorTypeTransferSetterToGetterFailded,
SJKVOErrorTypeInvalidInputObservingKeys,
} SJKVOErrorTypes;
@interface SJKVOError : NSError
+ (id)errorNoObervingObject;
+ (id)errorNoObervingKey;
+ (id)errorNoMatchingSetterForKey:(NSString *)key;
+ (id)errorTransferSetterToGetterFaildedWithSetterName:(NSString *)setterName;
+ (id)errorNoObserverOfObject:(id)object;
+ (id)errorInvalidInputObservingKeys;
@end
複製程式碼
OK,這樣就介紹完了,希望各位同學可以積極指正~
本篇已同步到個人部落格:使用Block實現KVO
—————————- 2018年7月17日更新 —————————-
注意注意!!!
筆者在近期開通了個人公眾號,主要分享程式設計,讀書筆記,思考類的文章。
- 程式設計類文章:包括筆者以前釋出的精選技術文章,以及後續釋出的技術文章(以原創為主),並且逐漸脫離 iOS 的內容,將側重點會轉移到提高程式設計能力的方向上。
- 讀書筆記類文章:分享程式設計類,思考類,心理類,職場類書籍的讀書筆記。
- 思考類文章:分享筆者平時在技術上,生活上的思考。
因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。
而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~
掃下方的公眾號二維碼並點選關注,期待與您的共同成長~