手動實(shí)現(xiàn)帶有block的KVO

上篇文章講到了什么是isa指針以及KVO的底層實(shí)現(xiàn),如果對KVO和isa指針不熟悉的需要先看看這篇文章。本篇文章主要是實(shí)現(xiàn)含有Block的KVO方法。先上代碼

1、 KVO的簡單實(shí)現(xiàn)

上篇文章中我們知道KVO的底層是通過運(yùn)行時動態(tài)創(chuàng)建一個子類進(jìn)行監(jiān)聽屬性的變化的。我們這里先給出一個簡單的實(shí)現(xiàn):
1、創(chuàng)建一個Dog類,含有有個name屬性。
2、手動創(chuàng)建一個SimpleKVO_Dog類繼承自Dog類,重寫setName方法。
3、給NSObject添加個category,增加添加觀察者的方法和觀察者回調(diào)方法,實(shí)現(xiàn)代碼如下:
NSObject的category中的代碼:

#import "SimpleKVO_Dog.h"
NSString *const ObserverKey = @"ObserverKey";
@implementation NSObject (SimpleKVO)
- (void)ll_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    // 保存觀察者
    objc_setAssociatedObject(self, (__bridge const void *)(ObserverKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 修改isa指針指向的類(指向了Dog子類),這里將指向我們手動創(chuàng)建的Dog的子類
    object_setClass(self, [SimpleKVO_Dog class]);
}

// 這里做是為了容錯處理
- (void)ll_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context{}
@end

子類重寫的setName方法實(shí)現(xiàn):

- (void)setName:(NSString *)name{
    // 保存舊值
    NSString *oldName = self.name;
    // 調(diào)用父類方法
    [super setName:name];
    // 獲取觀察者
    id obsetver = objc_getAssociatedObject(self, ObserverKey);
    NSDictionary<NSKeyValueChangeKey,id> *changeDict = oldName ? @{NSKeyValueChangeNewKey : name, NSKeyValueChangeOldKey : oldName} : @{NSKeyValueChangeNewKey : name};
    // 調(diào)用回調(diào)方法,傳遞舊值和新值
    [obsetver ll_observeValueForKeyPath:@"name" ofObject:self change:changeDict context:nil];
}

調(diào)用代碼:

- (void)test{
    Dog *dog =  [Dog new];
   //isa --->Dog類
    [dog ll_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew |
     NSKeyValueObservingOptionOld context:nil];
  //isa---> SimpleKVO_Dog類
    dog.name = @"aaa";
    dog.name = @"bbb";
}
- (void)ll_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}

打印結(jié)果:


上述方法中我們是通過手動創(chuàng)建Dog的子類SimpleKVO_Dog類,并重寫了父類的setName
方法,通過修改Dog類實(shí)例isa指針的指向,來調(diào)用子類的setName方法。子類的setName方法中又調(diào)用父類的setName方法以及通知了觀察者屬性改變。


2、帶有block的實(shí)現(xiàn)

下面我們來通過runtime動態(tài)生成子類,并實(shí)現(xiàn)帶有block回調(diào)的方法,我們?nèi)砸訢og類為例。下面我們通過代碼進(jìn)一步去講解:
首先先給出兩個工具方法(getter方法名和setter方法名的相互轉(zhuǎn)化):

//根據(jù)getter方法名返回setter方法名   name -> Name -> setName:
- (NSString *)setterForGetter:(NSString *)key
{
    // 1. 首字母轉(zhuǎn)換成大寫
    unichar c = [key characterAtIndex:0];
    NSString *str = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[NSString stringWithFormat:@"%c", c-32]];
    // 2. 最前增加set, 最后增加:
    NSString *setter = [NSString stringWithFormat:@"set%@:", str];
    return setter;
}

//根據(jù)setter方法名返回getter方法名  setName: -> Name -> name
- (NSString *)getterForSetter:(NSString *)key
{
    // 1. 去掉set
    NSRange range = [key rangeOfString:@"set"];
    NSString *subStr1 = [key substringFromIndex:range.location + range.length];
    // 2. 首字母轉(zhuǎn)換成大寫
    unichar c = [subStr1 characterAtIndex:0];
    NSString *subStr2 = [subStr1 stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[NSString stringWithFormat:@"%c", c+32]];
    // 3. 去掉最后的:
    NSRange range2 = [subStr2 rangeOfString:@":"];
    NSString *getter = [subStr2 substringToIndex:range2.location];
    return getter;
}

下面我們來看具體邏輯實(shí)現(xiàn):

添加觀察者:ll_addObserver:key:callback:步驟:
  • 檢查被觀察對象對應(yīng)的類有沒有相應(yīng)的 setter 方法,沒有則return;
  • 檢查對象 isa 指向的類是不是一個 KVO 類。如果不是,新建一個繼承原來類的子類,并把 isa 指向這個新建的子類;(這里還需要判斷需要新建的子類是否已經(jīng)創(chuàng)建過了,如果創(chuàng)建過了,則直接使用該子類);
  • 檢查對象的 KVO 類重寫過沒有這個 setter 方法。如果沒有,添加重寫的 setter 方法;
  • 將觀察者、觀察的key以及對應(yīng)的block回調(diào)生成相應(yīng)的字典保存到數(shù)組里;
-(void)ll_addObserver:(id)observer key:(NSString *)key callback:(LLKVOBlock)callback{
    //1. 通過觀察的key獲得相應(yīng)的setter方法
    SEL setterSelector = NSSelectorFromString([self setterForGetter:key]);
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod)  return;    //不存在setter方法直接return
    
    //2. 檢查對象 isa 指向的類是不是一個 KVO 類。如果不是,新建一個繼承原來類的子類,并把 isa 指向這個新建的子類
    Class clazz = object_getClass(self);
    NSString *className = NSStringFromClass(clazz);
    if (![className hasPrefix:KVOPrefix]) {//當(dāng)前類不是KVO類
        clazz = [self ll_KVOClassWithOriginalClassName:className];
        object_setClass(self, clazz);
    }

    //-------到這里self已經(jīng)是KVO類了---------
    
    // 3. 檢查KVO類是否已重寫父類的setter方法,如果沒有則為KVO類添加setter方法的實(shí)現(xiàn)
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    };
    
    // 4. 添加該觀察者到觀察者列表中
    // 4.1 創(chuàng)建觀察者相關(guān)信息字典(觀察者對象、觀察的key、block)
    NSDictionary *infoDic = @{@"observer":observer,@"key":key,@"callback":callback};
    // 4.2 獲取關(guān)聯(lián)對象(裝著所有觀察者的數(shù)組)
    NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, ObserverArrayKey, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    [observers addObject:infoDic];
}
創(chuàng)建子類的步驟:
  • 判斷要創(chuàng)建的子類是否已經(jīng)創(chuàng)建,存在直接返回這個類,不存在則去創(chuàng)建;
  • 重寫子類的class方法,使其返回父類的Class;(這步只是模仿系統(tǒng)KVO的實(shí)現(xiàn),對業(yè)務(wù)邏輯沒影響,可不實(shí)現(xiàn))
//  動態(tài)創(chuàng)建子類的方法
-(Class)ll_KVOClassWithOriginalClassName:(NSString *)className
{
    NSString *kvoClassName = [KVOPrefix stringByAppendingString:className];
    Class kvoClass = NSClassFromString(kvoClassName);//如果類不存在這個方法返回的值為nil
    // 如果kvo class存在則返回
    if (kvoClass) {
        return kvoClass;
    }
    // 如果kvo class不存在, 則創(chuàng)建這個類
    Class originClass = object_getClass(self);
    kvoClass = objc_allocateClassPair(originClass, kvoClassName.UTF8String, 0);
    
    // 修改kvo class方法的實(shí)現(xiàn)
    Method clazzMethod = class_getInstanceMethod(kvoClass, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClass, @selector(class), (IMP)ll_class, types);
     // 注冊kvo_class
    objc_registerClassPair(kvoClass);
    
    return kvoClass;
    
}
// 重寫的class方法的IMP
static Class ll_class(id self, SEL cmd)
{
    //模仿Apple的做法, 欺騙人們這個kvo類還是原類
   return  class_getSuperclass(object_getClass(self));
}

下面我們來看一下子類的setter方法的實(shí)現(xiàn),這和簡單實(shí)現(xiàn)的思路是一樣的,同樣是:

  • 獲取原來的值;
  • 調(diào)用父類的setter方法;
  • 通知觀察者屬性改變了(這里換成了block);
static void kvo_setter(id self, SEL _cmd, id newValue)
{
    
    // 1.  獲取舊值
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = [self getterForSetter:setterName];
    id oldValue = [self valueForKey:getterName];
    
    // 2. 調(diào)用父類方法
    struct objc_super superClazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    objc_msgSendSuper(&superClazz, _cmd, newValue);
    
    // 3、獲取觀察者列表,遍歷找出對應(yīng)的觀察者,執(zhí)行響應(yīng)的block
    NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
    for (NSDictionary *info in observers) {
        if ([info[@"key"] isEqualToString:getterName]) {
            // gcd異步調(diào)用callback
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                ((LLKVOBlock)info[@"callback"])(info[@"observer"], getterName, oldValue, newValue);
            });
        }
    }
}

注:這里調(diào)用父類的方法通過id objc_msgSendSuper(struct objc_super *super, SEL op, ...)實(shí)現(xiàn),第一個參數(shù)是一個指向objc_super結(jié)構(gòu)體的指針, objc_super的定義如下

struct objc_super {  
    __unsafe_unretained id receiver;    
    __unsafe_unretained Class super_class;  
};  

objc_super結(jié)構(gòu)體包含兩個成員,receiver表示某個類的實(shí)例,這里為self,super_class表示當(dāng)前類的父類,這里為self的父類。(注:這里的self其實(shí)已經(jīng)是KVO創(chuàng)建的子類類型了)我們這里通過class_getSuperclass(object_getClass(self))方法獲得;
到這里添加觀察者的方法暫時差不多了,為什么說暫時呢因為還有些問題,在下面會提出。那么現(xiàn)在我們還需要添加移除觀察者的方法:

-(void)ll_removeObserver:(id)observer key:(NSString *)key
{
    NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
    if (!observers) return;

    for (NSDictionary *info in observers) {
        if([info[@"key"] isEqualToString:key]) {
            [observers removeObject:info];
            break;
        }
    }
    // 如果觀察者列表count為0,則修改kvo類的isa指針,指向原來的類
    if (observers.count == 0) {
        Class clazz = object_getClass(self);
        NSString *className = NSStringFromClass(clazz);
        Class oriClass =NSClassFromString([className substringFromIndex:KVOPrefix.length]);
        object_setClass(self, oriClass);
    }
}

值得注意的是,當(dāng)所有觀察者都移除后,修改isa指針使其指向原來的類。系統(tǒng)的KVO實(shí)現(xiàn)就是這么做的,我們可以簡單的通過代碼測試一下:

-(void)test1{
    self.dog =  [Dog new];
    NSLog(@"%@",object_getClass(self.dog));
    [self.dog addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |
     NSKeyValueObservingOptionOld context:nil];
    NSLog(@"%@",object_getClass(self.dog));
    [self.dog removeObserver:self forKeyPath:@"age"];
    NSLog(@"%@",object_getClass(self.dog));
}

輸出結(jié)果:Dog----NSKVONotifying_Dog----Dog


3、 存在的問題:

因為重寫setter方法我們的實(shí)現(xiàn)static void kvo_setter(id self, SEL _cmd, id newValue){}是 這樣的,newValue是一個id類型,這就要求我們觀察的屬性必須是OC類的實(shí)例。通過嘗試發(fā)現(xiàn)系統(tǒng)的KVO會將基本類型最終轉(zhuǎn)換成NSNumber類型,再將新/舊值通過字典傳遞。但是OC對象我們可以通過id來統(tǒng)一表示,基本類型我們卻無能為力。所以這里給出兩種思路:

  • 思路一:可以在添加觀察者方法中的第3步給kvo類重寫setter方法,我們通過判斷參數(shù)類型,來添加不同setter的方法實(shí)現(xiàn)。類型的判斷這里用到了@encode關(guān)鍵字,不明白的可以看這篇文章
// 3. 檢查KVO類是否已重寫父類的setter方法,如果沒有則為KVO類添加setter方法的實(shí)現(xiàn)
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        // 獲取參數(shù)類型
        char *type = method_copyArgumentType(setterMethod, 2);
        if (strcmp(type, "@") == 0) {//對象類型
            class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
        }else if (strcmp(type, @encode(long))  == 0) {
            class_addMethod(clazz, setterSelector, (IMP)long_setter, types);
        }else if (strcmp(type, @encode(int)) == 0) {
            class_addMethod(clazz, setterSelector, (IMP)int_setter, types);
        }else if (strcmp(type, @encode(float)) == 0) {
            class_addMethod(clazz, setterSelector, (IMP)double_setter, types);
        }else if (strcmp(type, @encode(double))  == 0) {
            class_addMethod(clazz, setterSelector, (IMP)double_setter, types);
        }else if (strcmp(type, @encode(BOOL)) == 0) {
           class_addMethod(clazz, setterSelector, (IMP)bool_setter, types);
        }
    };

但是這種思路的問題就是需要判斷的類型太多,除對象類型外都需要實(shí)現(xiàn)不同的setter方法的IMP,而且代碼內(nèi)容大致相同,造成代碼的重復(fù)。這里給出int類型的setter的方法IMP:

//int 類型
static void int_setter(id self, SEL _cmd, int newValue)
{
    
    // 1. 檢查getter方法是否存在
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = [self getterForSetter:setterName];
    if (!getterName) {
        
        return;
    }
    
    // 2. 獲取舊值
    id oldValue = [self valueForKey:getterName];
    
    // 3. 調(diào)用父類方法
    struct objc_super superClazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    objc_msgSendSuper(&superClazz, _cmd, newValue);
    
    // 4、獲取觀察者列表,遍歷找出對應(yīng)的觀察者,執(zhí)行響應(yīng)的block
    NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
    for (NSDictionary *info in observers) {
        if ([info[@"key"] isEqualToString:getterName]) {
            // gcd異步調(diào)用callback
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                ((LLKVOBlock)info[@"callback"])(info[@"observer"], getterName, oldValue, [NSNumber numberWithInt:newValue]);
            });
        }
    }
}
  • 思路二:我們是否可以模仿系統(tǒng)KVC的實(shí)現(xiàn)通過[dog setValue:[NSNumber numberWithInteger:5] forKey:@"age"];這樣的形式,在給子類添加setter方法前,通過轉(zhuǎn)換成NSNumer類型后,在實(shí)現(xiàn)setter方法呢。然并卵,我也沒能實(shí)現(xiàn)。需請大神支援~~~

到這里本篇文章基本結(jié)束,文中所涉及代碼都在這里
最后:喜歡我文章的可以多多點(diǎn)贊和關(guān)注,您的鼓勵是我寫作的動力。O(∩_∩)O~


參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容