手动实现带有block的KVO

上篇文章讲到了什么是isa指针以及KVO的底层实现,如果对KVO和isa指针不熟悉的需要先看看这篇文章。本篇文章主要是实现含有Block的KVO方法。先上代码

1、 KVO的简单实现

上篇文章中我们知道KVO的底层是通过运行时动态创建一个子类进行监听属性的变化的。我们这里先给出一个简单的实现:
1、创建一个Dog类,含有有个name属性。
2、手动创建一个SimpleKVO_Dog类继承自Dog类,重写setName方法。
3、给NSObject添加个category,增加添加观察者的方法和观察者回调方法,实现代码如下:
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子类),这里将指向我们手动创建的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方法实现:

- (void)setName:(NSString *)name{
    // 保存旧值
    NSString *oldName = self.name;
    // 调用父类方法
    [super setName:name];
    // 获取观察者
    id obsetver = objc_getAssociatedObject(self, ObserverKey);
    NSDictionary<NSKeyValueChangeKey,id> *changeDict = oldName ? @{NSKeyValueChangeNewKey : name, NSKeyValueChangeOldKey : oldName} : @{NSKeyValueChangeNewKey : name};
    // 调用回调方法,传递旧值和新值
    [obsetver ll_observeValueForKeyPath:@"name" ofObject:self change:changeDict context:nil];
}

调用代码:

- (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);
}

打印结果:


上述方法中我们是通过手动创建Dog的子类SimpleKVO_Dog类,并重写了父类的setName
方法,通过修改Dog类实例isa指针的指向,来调用子类的setName方法。子类的setName方法中又调用父类的setName方法以及通知了观察者属性改变。


2、带有block的实现

下面我们来通过runtime动态生成子类,并实现带有block回调的方法,我们仍以Dog类为例。下面我们通过代码进一步去讲解:
首先先给出两个工具方法(getter方法名和setter方法名的相互转化):

//根据getter方法名返回setter方法名   name -> Name -> setName:
- (NSString *)setterForGetter:(NSString *)key
{
    // 1. 首字母转换成大写
    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;
}

//根据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. 首字母转换成大写
    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;
}

下面我们来看具体逻辑实现:

添加观察者:ll_addObserver:key:callback:步骤:
  • 检查被观察对象对应的类有没有相应的 setter 方法,没有则return;
  • 检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类;(这里还需要判断需要新建的子类是否已经创建过了,如果创建过了,则直接使用该子类);
  • 检查对象的 KVO 类重写过没有这个 setter 方法。如果没有,添加重写的 setter 方法;
  • 将观察者、观察的key以及对应的block回调生成相应的字典保存到数组里;
-(void)ll_addObserver:(id)observer key:(NSString *)key callback:(LLKVOBlock)callback{
    //1. 通过观察的key获得相应的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]) {//当前类不是KVO类
        clazz = [self ll_KVOClassWithOriginalClassName:className];
        object_setClass(self, clazz);
    }

    //-------到这里self已经是KVO类了---------
    
    // 3. 检查KVO类是否已重写父类的setter方法,如果没有则为KVO类添加setter方法的实现
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    };
    
    // 4. 添加该观察者到观察者列表中
    // 4.1 创建观察者相关信息字典(观察者对象、观察的key、block)
    NSDictionary *infoDic = @{@"observer":observer,@"key":key,@"callback":callback};
    // 4.2 获取关联对象(装着所有观察者的数组)
    NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, ObserverArrayKey, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    [observers addObject:infoDic];
}
创建子类的步骤:
  • 判断要创建的子类是否已经创建,存在直接返回这个类,不存在则去创建;
  • 重写子类的class方法,使其返回父类的Class;(这步只是模仿系统KVO的实现,对业务逻辑没影响,可不实现)
//  动态创建子类的方法
-(Class)ll_KVOClassWithOriginalClassName:(NSString *)className
{
    NSString *kvoClassName = [KVOPrefix stringByAppendingString:className];
    Class kvoClass = NSClassFromString(kvoClassName);//如果类不存在这个方法返回的值为nil
    // 如果kvo class存在则返回
    if (kvoClass) {
        return kvoClass;
    }
    // 如果kvo class不存在, 则创建这个类
    Class originClass = object_getClass(self);
    kvoClass = objc_allocateClassPair(originClass, kvoClassName.UTF8String, 0);
    
    // 修改kvo class方法的实现
    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方法的实现,这和简单实现的思路是一样的,同样是:

  • 获取原来的值;
  • 调用父类的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. 调用父类方法
    struct objc_super superClazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    objc_msgSendSuper(&superClazz, _cmd, newValue);
    
    // 3、获取观察者列表,遍历找出对应的观察者,执行响应的block
    NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
    for (NSDictionary *info in observers) {
        if ([info[@"key"] isEqualToString:getterName]) {
            // gcd异步调用callback
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                ((LLKVOBlock)info[@"callback"])(info[@"observer"], getterName, oldValue, newValue);
            });
        }
    }
}

注:这里调用父类的方法通过id objc_msgSendSuper(struct objc_super *super, SEL op, ...)实现,第一个参数是一个指向objc_super结构体的指针, objc_super的定义如下

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

objc_super结构体包含两个成员,receiver表示某个类的实例,这里为self,super_class表示当前类的父类,这里为self的父类。(注:这里的self其实已经是KVO创建的子类类型了)我们这里通过class_getSuperclass(object_getClass(self))方法获得;
到这里添加观察者的方法暂时差不多了,为什么说暂时呢因为还有些问题,在下面会提出。那么现在我们还需要添加移除观察者的方法:

-(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);
    }
}

值得注意的是,当所有观察者都移除后,修改isa指针使其指向原来的类。系统的KVO实现就是这么做的,我们可以简单的通过代码测试一下:

-(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));
}

输出结果:Dog----NSKVONotifying_Dog----Dog


3、 存在的问题:

因为重写setter方法我们的实现static void kvo_setter(id self, SEL _cmd, id newValue){}是 这样的,newValue是一个id类型,这就要求我们观察的属性必须是OC类的实例。通过尝试发现系统的KVO会将基本类型最终转换成NSNumber类型,再将新/旧值通过字典传递。但是OC对象我们可以通过id来统一表示,基本类型我们却无能为力。所以这里给出两种思路:

  • 思路一:可以在添加观察者方法中的第3步给kvo类重写setter方法,我们通过判断参数类型,来添加不同setter的方法实现。类型的判断这里用到了@encode关键字,不明白的可以看这篇文章
// 3. 检查KVO类是否已重写父类的setter方法,如果没有则为KVO类添加setter方法的实现
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        // 获取参数类型
        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);
        }
    };

但是这种思路的问题就是需要判断的类型太多,除对象类型外都需要实现不同的setter方法的IMP,而且代码内容大致相同,造成代码的重复。这里给出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. 调用父类方法
    struct objc_super superClazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    objc_msgSendSuper(&superClazz, _cmd, newValue);
    
    // 4、获取观察者列表,遍历找出对应的观察者,执行响应的block
    NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
    for (NSDictionary *info in observers) {
        if ([info[@"key"] isEqualToString:getterName]) {
            // gcd异步调用callback
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                ((LLKVOBlock)info[@"callback"])(info[@"observer"], getterName, oldValue, [NSNumber numberWithInt:newValue]);
            });
        }
    }
}
  • 思路二:我们是否可以模仿系统KVC的实现通过[dog setValue:[NSNumber numberWithInteger:5] forKey:@"age"];这样的形式,在给子类添加setter方法前,通过转换成NSNumer类型后,在实现setter方法呢。然并卵,我也没能实现。需请大神支援~~~

到这里本篇文章基本结束,文中所涉及代码都在这里
最后:喜欢我文章的可以多多点赞和关注,您的鼓励是我写作的动力。O(∩_∩)O~


参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 上篇文章讲到了什么是isa指针以及KVO的底层实现,如果对KVO和isa指针不熟悉的需要先看看这篇文章。本篇文章主...
    lilei5阅读 1,160评论 0 13
  • 来自 http://tech.glowing.com/cn/implement-kvo/ 本文是 Objectiv...
    MSG猿阅读 353评论 0 0
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 转自:http://tech.glowing.com/cn/implement-kvo/本文是 Objective...
    反调唱唱阅读 789评论 0 0
  • 我们的挥手是如此美丽 我们的言辞 竟也如此动人 凛冽的肩头上 飞雪正以绝美的姿势 完成终生的意愿 有泪的分手是无味...
    湘中布衣秀才阅读 235评论 0 0