运行时(Runtime)篇幅五

103 阅读5分钟

如果说Method Swizzling在平时项目中使用率并不高的话,那本篇关于Runtime的作用使用率应该可以说是相当高的。那就是KVC/KVO

KVC/KVO都属于观察者模式的一种实现

观察者模式:定义一种一对多的依赖关系,让多个观察者(订阅者)对象同时监听某一主题对象。当该对象发生改变时,实时通知所有观察者(订阅者),对该值进行更新。

通过观察者模式的定义可以看出,对值进行更新是通过一种依赖关系进行的。而这种依赖关系的底层逻辑和实现过程则是和Runtime有关的。Runtime通过KVC和KVO两种不同的实现方式来进行依赖关系的绑定,因此来进行一一的梳理和讲解。

KVC

Key-Value-coding 俗称键值编码,是一种通过指定方法来改变对象中不能通过直接方式获取的变量的值。指定方法是指KVC中用于变量的赋值取值

赋值:

- (void)setValue:(nullable id)value forKey:(NSString *)key;

取值:

- (nullable id)valueForKey:(NSString *)key;

value : 变化的值
key : 需要改变的变量名称

而对于“不能通过直接方式获取的变量”的理解则是,首先KVC的目的还是比较明确的,修改变量的值。然而对于修改变量值的方式有很多,点语法是最直接获取变量的方式,如果通过点语法能达到修改变量的目的那为什么还是使用KVC呢!所以该变量肯定是在无法直接获取修改的前提下如成员变量

@interface PublicView () {
    NSString *name;
}
@end

比如要使用KVC修改上方PublicView的成员变量name的值如下:

// 赋值
self.publicView = [[PublicView alloc] init];
[self.publicView setValue:@"change" forKey:@"name"];

// 取值
self.publicView.label.text = [self valueForKey:key];

setValue传入的是需要修改的值;forKey则是上方成员变量的名称。

修改和取值变量本质就是通过setter和getter方法来执行。所以当调用setValue:forKey方法和value:forKey:时Runtime首先会先查找类中对应成员变量名称的<Key>is<Key>get<Key>的访问器方法,有则通过该方法返回对应的值。或者通过set<Key>方法进行赋值

当以上方法不可用时,则会去查找是否有_<Key>_is<Key>已经_get<Key>_set<Key>方法;

当无法找到以上方法时,则会去调用accessInstanceVaeibalesDirectly方法,尝试是否可以通过直接访问成员变量的方式,返回的布尔值值为YES时,则会去类中按顺序依次去查找是否存在<key>,_<Key>格式的成员变量方法,存在则赋值。

当返回为NO或则无法查找到对应格式的成员变量,则去调用setValue:forUndefinedKey:方法,并抛出异常。

相对于forKey方法forKeyPath的方法在功能上则更强一步。forKeyPath可以利用点语法来更进一步的访问指定的属性,但实际原理其实都是一样的。当某个类的对象首次被观察时,Runtime系统就会在运行期动态的创建一个该类的派生类,然后在这个派生类中重写基类中任何被观察的属性的setter方法,其目的是实现真正的通知机制。当收到赋值请求时则将这个对象的isa指针指向这个派生类,从而也激活了派生类的setter方法和通知机制。 除此之外派生类还会重写dealloc方法来释放资源。

在派生类中重写setter方法时新增了

-(void)willChangeValueForKey:(NSString *)key;
-(void)didChangeValueForKey:(NSString *)key;

两个方法,willChangeValueForKey用于获取原值(旧值),然后再通过didChangeValueForKey在派生类中进行设值即重写setter方法。

KVO

全称:Key-Value-Observing 俗称键值观察

其功能及原理上与KVC是大同小异的,"同"的是相似的作用和底层实现方式,“异”则由这几个方面,一是KVO具体使用如下方法:

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

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

第一个方法用于设置观察者对象forKeyPath即观察的属性;observeValueForKeyPath用于监听事件,当属性被修改时凡是添加该方法都会返回监听结果;removeObserver删除监听对象。此时相比于KVC,KVO并没有修改属性值的参数,这是因为KVO的最终目的不在于修改属性值,而是在于通过observeValueForKeyPath方法进行属性值修改的监听结果的回调。从而能够实现在同一时间内实时的修改所用到该属性的地方。因此KVO所监听对象的属性一般都是非成员变量,而是能够通过点语法进行直接修改的属性变量。

其二的不同之处在于派生类的具体实现过程。虽然KVC/KVO都会生成派生类,KVC仅仅是通过setter方法对变量值进行了重置,但要实现KVO中实时监听回调机制是不够的,因此KVO通过isa指针的方式把原有类的isa指针重新指向到了派生类,当重写setter方法时派生类就会通过isa指针去通知原有属性的值从而实现通知机制。

其三则是removeObserver的使用。对于为什么要使用removeObserver我的理解一是对资源的释放,二是因为observer在底层是unsafe_unretained的存在当响应如果出现野指针并且向野指针发送了消息则必然引起崩溃,因此对不需要的observer进行remove是很有必要的。还有就是需要注意的是当添加观察者与remove次数不匹配时容易导致Crash

通知

通知是一个专门协助不同对象之间的消息通信,任何一个对象都可以向通知中心发布想发布的内容,凡是感兴趣的对象都可以接收此类通知。NSNotification和KVO的区别在于NSNotification需要被观察者主动发出通知,然后观察者注册监听后再来进行响应,相比KVO多了发送通知的步骤,并且监听范围和回调内容方式都要优于KVO。