文档章节

Objective-C KVO 中 runtime 探究

aron1992
 aron1992
发布于 2017/04/27 14:00
字数 1409
阅读 20
收藏 0

最近学习Runtime,顺便总结一下在Objective-C中KVO使用到的Runtime机制。

系统的KVO使用

故事还得从OC的KVO说起,一般的我们使用KVO类似的如下所示,创建一个对象,然后调用addObserver方法进行某个属性的监听,有意思的是,我们在创建对象处和调用了addObserver方法处打断点,然后使用po命令打印对象的isa,发现了对象的isa指针在调用了addObserver方法之后变了,明显滴,调用了addObserver方法之后使用了runtime机制动态的修改了对象的isa指针。

对象添加KVO监听之后isa的变换

KVO中runtime的几个概念

大家一定会很好奇,runtime是怎么实现了KVO,那好下面就慢慢的揭开谜底。先了解几个runtime的概念:

  • 动态创建Class
    objc_allocateClassPair可以动态创建Class,objc_registerClassPair进行注册动态创建的Class
  • 修改对象的Class
    object_setClass可以修改对象的Class,也就是修改了isa指针指向的Class对象
  • 动态添加方法
    class_addMethod可以给类添加方法
  • runtime方法调用
    objc_msgSendobjc_msgSendSuper是OC消息发送机制的底层实现

KVO的实现步骤解析

好了,掌握了这几个runtime的概念,以及一开始看到了对象isa指针的变化,我们大概可以猜测KVO的实现了,大概需要以下几个步骤:

  • 动态了创建一个类(objc_allocateClassPair/objc_registerClassPair),该类是我们需要监听的对象的类的子类,然后修改当前对象的Class(object_setClass)。
  • 给类动态的添加方法(object_setClass),比如KVO监听name属相,一般的需要添加一个setName:方法,然后添加方法的实现,在方法实现中,调用父类也就是原始类的setName:方法进行属性的设置(objc_msgSend,亦可以使用objc_msgSendSuper)。
  • 给observer的回调方法发送监听消息(objc_msgSend),最后需要重置当前对象的Class(object_setClass),确保再次监听还会执行同样的流程.

自定义的KVO实现

系统的KVO addObserver方法是NSObject对象的一个类别中NSObject(NSKeyValueObserverRegistration)定义的方法,所以我们也创建一个NSObject的类别NSObject (YTT_KVO),添加一个类似的监听方法。

创建KVO使用的类别

@interface NSObject (YTT_KVO)

- (void)ytt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

@end

类别方法的实现

类别的监听方法,做的事情主要是保存方法参数,动态创建之类以及设置对象的Class到子类,动态的添加子类的方法。

- (void)ytt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
    
    // 保存keypath
    objc_setAssociatedObject(self, "keyPath", keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    // 获取当前类
    Class selfClass = self.class;
    
    // 动态创建KVO类
    const char * className = NSStringFromClass(selfClass).UTF8String;
    char kvoClassName[1000];
    sprintf(kvoClassName, "%s%s", "YTT_KVO_", className);
    Class kvoClass = objc_allocateClassPair(selfClass, kvoClassName, 0);
    if (!kvoClass) {
        // Nil if the class could not be created (for example, the desired name is already in use).
        kvoClass = NSClassFromString([NSString stringWithUTF8String:kvoClassName]);
    }
    objc_registerClassPair(kvoClass);
    
    // 修改当前类指向为动态创建的KVO子类
    object_setClass(self, kvoClass);
    
    // 动态添加一个方法:setXxx()
    SEL sel = NSSelectorFromString([NSString stringWithFormat:@"set%@:", keyPath.capitalizedString]);
    class_addMethod(kvoClass, sel, (IMP)setValue, NULL);
}

动态添加方法的实现

动态添加方法做的事情主要是创建和设置回调参数,包含新值和旧值,调父类的设置方法,给原始类设置值,给observer回调方法发送消息通知,告诉值改变了。

void setValue(id self, SEL _cmd, id value) {
    
    // 保存当前的Class,重置Class使用
    Class selfClass = [self class];
    // 设置Class为原始Class
    object_setClass(self, [self superclass]);
    // 获取keyPath
    NSString* keyPath = objc_getAssociatedObject(self, "keyPath");

    // KVO 回调参数
    NSMutableDictionary* change = [NSMutableDictionary dictionary];
    change[NSKeyValueChangeNewKey] = value;
    
    // 获取旧的值
    SEL getSel = NSSelectorFromString([NSString stringWithFormat:@"%@", keyPath]);
    if ([self respondsToSelector:getSel]) {
        id ret = ((id(*)(id, SEL, id))objc_msgSend)(self, getSel, value);
        if (ret) {
            change[NSKeyValueChangeOldKey] = ret;
        }
    }
    
    // 给原始类设置数据
    SEL setSel = NSSelectorFromString([NSString stringWithFormat:@"set%@:", keyPath.capitalizedString]);
    if ([self respondsToSelector:setSel]) {
        ((void(*)(id, SEL, id))objc_msgSend)(self, setSel, value);
    }
    
    // 发送通知
    id observer = objc_getAssociatedObject(self, "observer");
    SEL observerSel = @selector(ytt_observeValueForKeyPath:ofObject:change:context:);
    if ([observer respondsToSelector:observerSel]) {
        ((void(*) (id, SEL, NSString*, id, id ,id))(void *)objc_msgSend)(observer, observerSel, keyPath, self, change, nil);
    }
    
    // 重置class指针,这样再次调用对象方法会走到这里面
    object_setClass(self, selfClass);
}

KVO通知的接收处理

在监听的对象中定义我们约定好的方法,类似Objective-C中的KVO进行处理

- (void)ytt_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    id new = change[NSKeyValueChangeNewKey];
    id old = change[NSKeyValueChangeOldKey];
    
    NSLog(@"%@-%@", old, new);
}

注意点

  • objc_msgSend 的使用注意

我使用的是xcode8,ENABLE_STRICT_OBJC_MSGSEND开关默认是打开的,需要进行方法的指针强转为对应的方法才能调用

((void(*) (id, SEL, NSString*, id, id ,id))(void *)objc_msgSend)(observer, observerSel, keyPath, self, change, nil);

// 这句代码的意思是把objc_msgSend这个任意类型的指针【(void *)】转换为一个返回值为void的函数指针【void(*)】,并且参数列表为【(id, SEL, NSString*, id, id ,id)】

// void (*) 声明一个函数指针,返回值是void类型
// (void *) 返回的是一个任意类型的指针

// 比如我们需要获取person的name属性,返回值是NSString*类型,那么objc_msgSend方法定义如下
id ret = ((NSString*(*)(id, SEL))objc_msgSend)(self.person, @selector(name));

也可以设置ENABLE_STRICT_OBJC_MSGSEND开关关闭,那么可以不额外的处理objc_msgSend强转的问题,如果是一个库项目,那么不建议手动关闭这个开关,这样不管是ENABLE_STRICT_OBJC_MSGSEND开关打开还是关闭都不会影响。

  • objc_allocateClassPair 为空处理

objc_allocateClassPair 调用可能返回Nil,这种情况在当前的场景第二次调用同一个类对象的ytt_addObserver方法可能发生,这种情况,使用NSClassFromString方法创建Class。

    Class kvoClass = objc_allocateClassPair(selfClass, kvoClassName, 0);
    if (!kvoClass) {
        // Nil if the class could not be created (for example, the desired name is already in use).
        kvoClass = NSClassFromString([NSString stringWithUTF8String:kvoClassName]);
    }
    objc_registerClassPair(kvoClass);

© 著作权归作者所有

共有 人打赏支持
aron1992

aron1992

粉丝 47
博文 80
码字总数 135304
作品 0
厦门
程序员
如何自己动手实现 KVO

本文是 Objective-C Runtime 系列文章的第三篇。如果你对 Objective-C Runtime 还不是很了解,可以先去看看前两篇文章: Objective-C Runtime Method Swizzling 和 AOP 实践 本篇会探究 KVO ...

zh_iOS
2016/08/22
302
0
Objective-C Runtime(四)isa swizzling

Runtime 4 isa swizzling Objective-C Runtime(一) 简介 对象、类的结构 消息传递(Messaging) Objective-C Runtime(二) 动态方法解析和转发 Objective-C Runtime(三) Method Swizzli...

liuyanhongwl
03/27
0
0
详解Objective-C runtime

原文地址:http://blog.securemacprogramming.com/2013/12/by-your-cmd/ 感谢翻译小组成员wingpan热心翻译。本篇文章是我们每周推荐优秀国外的技术类文章的其中一篇。如果您有不错的原创或译...

Michael-W
2014/01/06
0
0
The Objective-C runtime

By your cmd This post is a write-up of a talk I gave at Alt Tech Talks: London on the Objective-C runtime. Seriously though, you should’ve been there. The Objective-C runtime?......

TaciturnKnightYQ
2015/12/19
43
0
swift中KVO和属性观察器

开篇提醒:OC中的KVO及其KVO的基础知识可参见:深入runtime探究KVO Swift中,原本没有KVO模式,为何这么说,请看下文: KVO本质上是基于的动态分发机制,通过来监听的值。 OC能够实现监听因为...

CoderDancer
2017/01/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

JS三元运算示例

1. topFlag=topFlag ==0?1:0; 等于 if(topFlag=00){ topFlag=1; }else if(topFlag == 1){ topFlag=0; } 2. 5>3?alert('5大'):alert('3大'); 即 if(5>3){alert('5大')}else{alert('3大')}; 注......

森火
55分钟前
0
0
利用Slf4j的MDC跟踪方法调用链

why? 一个web项目通常提供很多URL访问地址, 项目一般都是分层处理,例如Controller——>Service——>DAO。 如果想根据日志查看用户一次请求都走了哪些方法(多数是查错误)。 如果系统是多人...

杨春炼
今天
5
0
Maven介绍及安装

Maven介绍及安装 以下内容是本人早期学习时的笔记,可能比较详实繁琐,现在复习一下Maven,顺便将内容抛出来,供大家一起学习进步。 一、Maven简介 Maven是Apache旗下的一款项目管理工具,是...

星汉
今天
0
0
小程序Aes解密

主要步骤: 1、下载AES源码(JS版) 2、在小程序中新建一个公共的文件夹,把AES源码拷贝进去(注意:需要暴露接口 module.exports = CryptoJS;) 3、添加一个用于加密解密的公共JS,可取名为...

Mr_Tea伯奕
今天
0
0
Go实现文件传输(基本传输可用)

发送端 package mainimport ("fmt""os""net""io")func SendFile(path string, connect net.Conn){file, oerr :=os.Open(path)if oerr !=nil{fmt.Println("Open", oerr)......

CHONGCHEN
今天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部