KVO底层原理—利用Runtime自定义KVO

KVO底层原理—利用Runtime自定义KVO

KVO:Key-value observer,也就是键值观察,是Objective-C对观察者模式的实现,每当被观察对象的某个属性值发生改变时,注册的观察者便能得到通知。
当然想了解KVO,还要先对KVC有所了解:KVC底层原理,本文利用Runtime实现自定义KVO,如果对Runtime不熟悉可以先了解下前几篇文章:Runtime底层原理KVO-官网直通车

先简单介绍一下KVO使用:

  • 添加观察:addObserver:self forKeyPath:options:context:
  • 观察回调:observeValueForKeyPath:ofObject :change: context:
  • 移除观察:removeObserver: forKeyPath:

TIP:建议KVO还是手动添加移除。如果没有移除观察,会有隐藏奔溃隐患(单例),比如当观察者析构时不会自动移除,被观察对象继续发送消息, 像发送一个消息给已经释放的对象, 触发exception。

KVO原理:

KVO默认观察setter,使用isa-swizzling来实现自动键值观察,也就是被观察对象的isa会被修改,指向一个动态生成的子类NSKVONotifying_xxxx(isa在移除观察者之后复原,动态生成的类不会被移除),但是通过object_getClass获取的还是原来的类,该子类重写了观察对象的setter方法,还有classdealloc方法和_isKVOA标识,并在重写setter方法中调用– willChangeValueForKey– didChangeValueForKey,然后向父类发送消息。如果automaticallyNotifiesObserversForKey返回NO的时候可以手动观察

  • 动态生成子类: NSKVONotifying_xxxx,用原来的类名做后缀
  • 重写观察对象的setter,classdealloc方法和_isKVOA标识
  • 在重写setter方法中调用 – willChangeValueForKey和 – didChangeValueForKey
  • 向父类发送消息

自定义KVO

知道了KVO的原理后我们利用Runtime进行验证并自定义KVO的实现,在实现了系统KVO的功能基础上还添加了自动移除观察者机制、监听利用block回调等

利用LLDB查看isa的指针,再利用Runtime查看添加观察前后的变化,可以通过下面的方法对原来的类和新增的NSKVONotifying_xxxx类进行对比

// 遍历方法 -- 判断imp指针是否改变也就是重写
- (void)getClassAllMethod:(Class)cls {
    if (!cls) return;
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@ --- %p",NSStringFromSelector(sel), imp);
    }
    free(methodList);
}

// 遍历属性
- (void)getClassProperty:(Class)cls {
    if (!cls) return;
    //获取类中的属性列表
    unsigned int propertyCount = 0;
    objc_property_t * properties = class_copyPropertyList(cls, &propertyCount);
    for (int i = 0; i<propertyCount; i++) {
        NSLog(@"属性的名称为 : %s",property_getName(properties[i]));
        /**
         特性编码 具体含义
         R readonly
         C copy
         & retain
         N nonatomic
         G(name) getter=(name)
         S(name) setter=(name)
         D @dynamic
         W weak
         P 用于垃圾回收机制
         */
        NSLog(@"属性的特性字符串为: %s",property_getAttributes(properties[i]));
    }
    //释放属性列表数组
    free(properties);
}

// 遍历变量
- (void)getClassAllIvar:(Class)cls {
    if (!cls) return;
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(cls, &count);
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivarList[i];
        NSLog(@"%s",ivar_getName(ivar));
    }
    free(ivarList);
}

// 遍历类以及子类
- (void)getClasses:(Class)cls {
    if (!cls) return;
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组,其中包含给定对象
    NSMutableArray *mArr = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class *classes = (Class *)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i < count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArr addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes --- %@", mArr);
}

经过验证后开始自定义KVO实现系统功能,并额外加上自定义的一些功能。先添加用来保存KVO信息的Info类VKVOInfo用来保存信息,还有一个扩展NSObject+VKVO,主要实现系统的原有功能,再添加自定义的一些方法,比如自动移除观察者等。

  • 首先先动态生成子类,并添加setterclassdealloc方法
#pragma mark - 动态生成子类
- (Class)createChildClass:(NSString *)keyPath {
    NSString *oldName = NSStringFromClass([self class]);
    NSString *newName = [NSString stringWithFormat:@"%@%@", kVKVOPrefix, oldName];
    Class newClass = NSClassFromString(newName);
    // 如果内存不存在,创建生成新的类,防止重复创建生成新类
    if (newClass) return newClass;
    
    newClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
    objc_registerClassPair(newClass);
    
    // 添加class方法
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classType = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)v_class, classType);
    
    // 添加setter方法
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterType = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)v_setter, setterType);
    
    // 添加dealloc方法
    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
    const char *deallocType = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSEL, (IMP)v_dealloc, deallocType);
    
    return newClass;
}
  • 把isa指针指向动态生成的KVONotifying子类(Person类会动态生成KVONotifying_Person)
object_setClass(self, newClass);
  • 保存KVO的信息
VKVOInfo *KVOInfo = [[VKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath options:options handleBlock:handleBlock];
    NSMutableArray *infoArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kVKVOAssiociateKey));
    if (!infoArr) {
        infoArr = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kVKVOAssiociateKey), infoArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [infoArr addObject:KVOInfo];

下面是部分重要代码:在setter方法中,先将消息发送给原来的类,再利用block响应回调(这里也可以添加判断,利用block回调或者设置代理),也可以添加一些自定义的方法,比如去掉NSKeyValueObservingOptions参数。

static void v_setter(id self, SEL _cmd, id newValue) {
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    /// Specifies the superclass of an instance.
    struct objc_super v_objc_super = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    // 消息转发给父类
    void (*v_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    v_msgSendSuper(&v_objc_super, _cmd, newValue);
    
    // 响应回调
    NSMutableArray *infoArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kVKVOAssiociateKey));
    for (VKVOInfo *info in infoArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                if (info.options & NSKeyValueObservingOptionNew) {
                    if (info.handleBlock) {
                        info.handleBlock(info.observer, info.keyPath, info.options, newValue, oldValue);
                    }
                }
//                SEL obserSEL = @selector(observeValueForKeyPath:ofObject:change:context:);
//                void (*v_objc_msgSend)(id, SEL, id, id, id, void *) = (void *)objc_msgSend;
//                Class supperClass = (object_getClass(self));
//                v_objc_msgSend(info.observer, obserSEL, keyPath, supperClass, @{keyPath:newValue}, NULL);
            });
        }
    }
}

这里是Demo地址:https://github.com/JBWangWork/VCustomKVO,本Demo已更新,去掉了options和context参数(系统context可以起到快速定位观察键的作用)。本Demo只适用于学习KVO底层原理。

该文章为记录本人的学习路程,也希望能够帮助大家,知识共享,共同成长,共同进步!!!文章地址:https://www.jianshu.com/p/724f6ab39400

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

推荐阅读更多精彩内容

  • 大体思路: 创建一个 A类,这个类中有一个属性 如 age 创建一个 NSObject + KVO 类,这个类中实...
    文瑶906阅读 510评论 0 0
  • 通过在了解KVO的实现原理和实现步骤之后,我们可以手动实现KVO,具体可以看最后的demo,这里只讲实现原理 添加...
    wp_Demo阅读 398评论 0 2
  • KVO是iOS开发中常用的不同类之间通信的技术,叫做键值观察,跟通知NSNotifacation一样是,可以一对多...
    huxinwen阅读 1,228评论 0 2
  • 概念 基本使用 触发模式 属性依赖 容器类的使用 自定义KVO 概念 KVO全称Key-Value Observi...
    it_Xiong阅读 677评论 0 2
  • 知识点概述 1.KVO实现原理2.runtime使用 目的 给NSObject添加一个Category,用于给实例...
    sqatm阅读 836评论 0 4