感觉很久都没有写新文章了,还是写一些小文章吧,记录一下也好!
这一次和大家讲解的是KVO -- key value observing
,想必大家也不陌生,项目中也会有用到的地方,网上相关的文章也不少,我在这里的目的就是为了班门弄斧😂,哈哈哈...。但是大家有没有研究过KVO
是怎么实现的呢?我本着厚颜无耻的心态为大家讲解一下。
前言
KVO的监听,实际上是通过运行时runtime
实现的,并且对被监听的对象,动态创建其子类,重写setter、class、
等方法,改变其isa
指针的指向,企图瞒骗我们。想一想都觉得好过分😏
虽然我们不知道其内部的实现,但是通过猜想,说错了,不是猜想,是实践。首先我们来看看添加观察后,过程发生了什么。这里已经有大神做过分析了,可以先看看这里
我也简单的给大家讲解下
@interface TestObject : NSObject
@property (nonatomic,assign) int z; // 被观察的属性z
@end
@implementation TestObject
@end
@interface ViewController ()
@end
//MARK: - 获取方法列表
static NSArray *ClassMethodNames(Class c)
{
NSMutableArray *array = [NSMutableArray array];
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for(i = 0; i < methodCount; i++)
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
free(methodList);
return array;
}
//MARK: - 打印对象信息
static void PrintDescription(NSString *name, id obj)
{
NSString *str = [NSString stringWithFormat:
@"%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
name,
obj,
class_getName([obj class]),
class_getName(object_getClass(obj)),
[ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "]];
printf("%s\n", [str UTF8String]);
}
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建 x、y对象
TestObject *x = [TestObject new];
TestObject *y = [TestObject new];
// 观察 y 对象
[y addObserver:y forKeyPath:@"z" options:0 context:nil];
// 打印x、y对象有什么不同
PrintDescription(@"x", x);
PrintDescription(@"y", y);
}
以下为打印信息
x: <TestObject: 0x604000017b90>
NSObject class TestObject
libobjc class TestObject
implements methods <z, setZ:>
y: <TestObject: 0x604000017c50>
NSObject class TestObject
libobjc class NSKVONotifying_TestObject
implements methods <setZ:, class, dealloc, _isKVOA>
从结果我们可以看出,被观察的对象重写了 setter
class
dealloc
还有一个神秘的_isKVOA
方法,原来的TestObject
类也发生了改变,变成了NSKVONotifying_TestObject
,这样子我们就可以断定对象方法class
被覆盖了override
,但我们可以通过runtime
的object_getClass()
获取其真正的类名,而且[obj class]
其内部实现就是下面的样子👇
- (Class)class {
return object_getClass(self);
}
你还会发现,当你重写TestObject
的class
方法,这方法已经不走了,原因就是被重写了。来,让我们再重写一些方法,看看Apple
还做了什么事情。
@interface TestObject : NSObject
@property (nonatomic,assign) int z;
@end
@implementation TestObject
- (void)setZ:(int)z {
_z = z;
NSLog(@"setter z:%d",z);
}
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"willChangeValueForKey:%@",key);
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey:%@",key);
}
- (Class)class {
NSLog(@"not override:%@",self);
return object_getClass(self);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
TestObject *x = [TestObject new];
TestObject *y = [TestObject new];
[y addObserver:y forKeyPath:@"z" options:0 context:nil];
x.z = 1;
NSLog(@"separation line");
y.z = 1;
[x class];
NSLog(@"separation line1");
[y class];
}
}
2018-07-19 11:17:04.989201+0800 KVO_demo[79056:7685866] setter z:1
2018-07-19 11:17:04.989368+0800 KVO_demo[79056:7685866] separation line
2018-07-19 11:17:04.989613+0800 KVO_demo[79056:7685866] willChangeValueForKey:z
2018-07-19 11:17:04.989775+0800 KVO_demo[79056:7685866] setter z:1
2018-07-19 11:17:04.990139+0800 KVO_demo[79056:7685866] didChangeValueForKey:z
2018-07-19 11:17:04.990517+0800 KVO_demo[79056:7685866] not override:<TestObject: 0x60000001acf0>
2018-07-19 11:17:05.007865+0800 KVO_demo[79056:7685866] separation line1
这次我们重写了4个方法
- (void)setZ:(int)z
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
- (Class)class
通过观察打印信息,我们可以发现被观察的对象,在赋值之前会先走- (void)willChangeValueForKey:(NSString *)key
,然后是- (void)setZ:(int)z
,最后走到- (void)didChangeValueForKey:(NSString *)key
,但并没有走- (Class)class
,而 x 对象只走了 setter
方法和class
方法。这样子我们就可以看出差别了,y 对象的class
方法确实被覆盖了。不过细心的你是不是发现了一些问题,上面不是说过setter
方法也被覆盖重写了吗?那为什么 y 对象的setter
方法还会走,有猫腻啊🐈,这个我们下面会说到,看官别急。
在这里还要提醒一下大家,当你重写override
了 -(void)willChangeValueForKey:(NSString *)key ;- (void)didChangeValueForKey:(NSString *)key
其中的一个方法时,下面监听的回调方法就不会再调用的了。
@interface NSObject(NSKeyValueObserving)
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
@end
而且此回调方法是根据添加的观察者是谁addObserver:observer
,谁就会接收到通知。
小结
根据以上的分析和实践,我们大致了解KVO一个看得见的实现流程,下面我们来模拟一下KVO的实现。还原案发现场罒ω罒。
先上代码
#import <Foundation/Foundation.h>
typedef NSString *CCKeyValueChangeKey NS_STRING_ENUM;
FOUNDATION_EXPORT CCKeyValueChangeKey const CCKeyValueChangeNewKey;
FOUNDATION_EXPORT CCKeyValueChangeKey const CCKeyValueChangeOldKey;
@interface NSObject (Override_KVO)
/**
对象方法,谁调用 表明 谁就是被观察的对象
@param observer 观察者
@param key 被观察对象的属性
@param handler block回调
*/
- (void)cc_addObserver:(id)observer
key:(NSString *)key
handler:(void(^)(id observer, NSString *key, NSDictionary<CCKeyValueChangeKey, id> *dic))handler;
/**
回调方法
@param key 被观察对象的属性
@param object 观察者
@param change 变化前后的值
*/
- (void)cc_observeValueForKey:(NSString *)key object:(id)object change:(NSDictionary<CCKeyValueChangeKey,id> *)change;
/**
移除观察者
@param observer 观察者
@param key 被观察对象的属性
*/
- (void)removeObserver:(id)observer key:(NSString *)key;
@end
我们先来看一下头文件的一些属性和方法
typedef NSString *CCKeyValueChangeKey NS_STRING_ENUM;
FOUNDATION_EXPORT CCKeyValueChangeKey const CCKeyValueChangeNewKey;
FOUNDATION_EXPORT CCKeyValueChangeKey const CCKeyValueChangeOldKey;
这里是参考Apple
的实现
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
/* Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
至于这个typedef NSString *CCKeyValueChangeKey
和这个FOUNDATION_EXPORT
有什么用,请百度一下吧,这里就不细说😀,往下还有很多知识点,根本说不完。简单理解就是自定义字符串。
我们先看一下第一个方法的实现
- (void)cc_addObserver:(id)observer key:(NSString *)key handler:(void (^)(id, NSString *, NSDictionary<CCKeyValueChangeKey,id> *))handler {
SEL setterMethod = NSSelectorFromString(setterNameFunc(key));
Class observerClass = object_getClass(self); // 获取类名
NSString *classString = NSStringFromClass(observerClass); // 转字符串
// 判断是否已创建子类
if (![classString hasPrefix:cc_keyValueNotifiy]) {
Class subClass = [self createSubClass:observerClass];
object_setClass(self, subClass); // 原类转换为子类
}
// 没有 setter 方法 就添加
if (![self isExistSetterFunc:setterMethod]) {
Method method = class_getInstanceMethod(object_getClass(self), setterMethod);
const char *type = method_getTypeEncoding(method);
class_addMethod(object_getClass(self), setterMethod, (IMP)cc_setter, type);
}
// 创建回调对象
CCObserver *temp = [[CCObserver alloc] initObserver:observer key:key handler:handler];
// 关联数组,存储对象
NSMutableArray *array = objc_getAssociatedObject(self, (__bridge void *)cc_associationKey);
if (!array) {
array = [NSMutableArray array];
objc_setAssociatedObject(self, (__bridge void *)cc_associationKey, array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[array addObject:temp];
}
在添加观察者的时候,我们先判断一下是否已经新建了子类,没有的话我们就创建其子类,改变其isa
的指向,下面我们看一看代码
Class subClass = [self createSubClass:observerClass];
//MARK: - 创建子类
- (Class)createSubClass:(Class)obsererClass {
NSString *classString = NSStringFromClass(obsererClass);
NSString *subString = [NSString stringWithFormat:@"%@%@",cc_keyValueNotifiy,classString]; // 拼接
Class subClass = NSClassFromString(subString); // 转换为 class
// 不是nil 证明已存在该类,直接返回
if (subClass) {
return subClass;
}
subClass = objc_allocateClassPair(obsererClass, subString.UTF8String, 0); // 创建新的类
Method method = class_getInstanceMethod(obsererClass, @selector(class));
const char * type = method_getTypeEncoding(method);
class_addMethod(subClass, @selector(class), (IMP)override_classFunc, type); // 添加新的class方法 以重载原class方法
objc_registerClassPair(subClass); // 创建新的类后,注册该类,使添加的方法 属性生效
return subClass;
}
我们通过runtime
的objc_allocateClassPair
动态的创建一个新的类,顺便看一下官方文档的说明,需要什么参数,每个参数的作用。
superclass
: 这个参数将作为新类的父类,如果是nil,那么会创建一个新的根类。
name
:新类的名字
extraBytes
分配字节数,通常为 0
如果已经存在该类,那么新建的类就会失败,返回nil,不过上面的代码已经做了判断了。
/**
* Creates a new class and metaclass.
*
* @param superclass The class to use as the new class's superclass, or \c Nil to create a new root class.
* @param name The string to use as the new class's name. The string will be copied.
* @param extraBytes The number of bytes to allocate for indexed ivars at the end of
* the class and metaclass objects. This should usually be \c 0.
*
* @return The new class, or Nil if the class could not be created (for example, the desired name is already in use).
*
* @note You can get a pointer to the new metaclass by calling \c object_getClass(newClass).
* @note To create a new class, start by calling \c objc_allocateClassPair.
* Then set the class's attributes with functions like \c class_addMethod and \c class_addIvar.
* When you are done building the class, call \c objc_registerClassPair. The new class is now ready for use.
* @note Instance methods and instance variables should be added to the class itself.
* Class methods should be added to the metaclass.
*/
OBJC_EXPORT Class _Nullable
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name,
size_t extraBytes)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
新类已经创建完了,我们继续通过runtime
的class_addMethod
,动态添加新的方法,以覆盖原有的class
方法
Method method = class_getInstanceMethod(obsererClass, @selector(class));
const char * type = method_getTypeEncoding(method);
class_addMethod(subClass, @selector(class), (IMP)override_classFunc, type);
最后注册新的类,上面的文档也提到了,当创建完新的类,为其设置类的属性和函数,最后调用objc_registerClassPair
进行注册,以告诉编译器新的类已经准备好使用。
有getClass
自然就有setClass
,创建完新的类后,没错,要变身了,请后退。object_setClass
可以把对象设置为任何类,很好很强大的。而且你会发现我们很多的类都是这样子设计的,这里先举个🌰NSNumber
,先说这么多,我们继续吧。
object_setClass(self, subClass); // 改变`isa`的指向
新类创建完了,外衣也做好了,接下来我们为新类再动态添加一个方法,以重写父类的setter
方法,并且把相关信息存储到数组里面,继续以runtime
的 objc_setAssociatedObject
关联起来,又是一个知识点。还有我们为什么要新建一个对象来存储信息呢,这里是为了后续的回调使用的,而且这样子包一层,也为了防止强引用observer
,而造成内存无法释放。数组会强引用其对象,所以不宜直接使用。
if (![self isExistSetterFunc:setterMethod]) {
Method method = class_getInstanceMethod(object_getClass(self), setterMethod);
const char *type = method_getTypeEncoding(method);
class_addMethod(object_getClass(self), setterMethod, (IMP)cc_setter, type);
}
// 创建回调对象
CCObserver *temp = [[CCObserver alloc] initObserver:observer key:key handler:handler];
// 关联数组,存储对象
NSMutableArray *array = objc_getAssociatedObject(self, (__bridge void *)cc_associationKey);
if (!array) {
array = [NSMutableArray array];
objc_setAssociatedObject(self, (__bridge void *)cc_associationKey, array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[array addObject:temp];
我们继续看一下重写的setter
又做了什么,来看下面的代码。我们在赋值前先调用其NSObject
分类方法willChangeValueForKey:
,之后获取原来的值,创建一个结构体struct objc_super
,又是一个知识点,这里最好能理解super
跟self
之间的关系,例如:[[super alloc] init]
这段代码,意思是调用父类的方法分配内存、初始化,但是其接收接者是self
。
((void (*)(id, SEL, id)) (void *)objc_msgSendSuper)((__bridge id)(&superClass), _cmd, (id)newValue);
没错,这里的做法是为了让原类TestObject
的setter
方法生效,而这里为什么用objc_msgSendSuper
而不是objc_msgSend
,我想大家应该明白了吧,这里也很好的解释了上文提到的 setter
方法为什么重写了还会走,因为用父类[super setXXX:]
了。
//MARK: - 重载 setter 方法
static void cc_setter(id self, SEL _cmd, id newValue) {
NSString *setterName = NSStringFromSelector(_cmd);
NSString *key = getterName(setterName);
[self willChangeValueForKey:key]; // 赋值前 调用 willChangeValueForKey
id oldValue = [self valueForKey:key]; // 获取 旧值
// 创建结构体
struct objc_super superClass = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
// 使原类 setter 方法生效
((void (*)(id, SEL, id)) (void *)objc_msgSendSuper)((__bridge id)(&superClass), _cmd, (id)newValue);
// 获取存储的数组
NSMutableArray *array = objc_getAssociatedObject(self, (__bridge void *)cc_associationKey);
for (CCObserver *observer in array) {
NSDictionary *dic = @{CCKeyValueChangeOldKey:oldValue, CCKeyValueChangeNewKey:newValue};
// [observer.observer cc_observeValueForKey:key object:observer.observer change:dic];.
!observer.handler ? : observer.handler(observer.observer, key, dic); // 执行回调
}
[self didChangeValueForKey:key]; // 赋值后 调用 didChangeValueForKey
}
谜底基本都差不多揭开了,最后我们来把结果回调一下,把存储起来的对象进行回调。注释掉的那段代码是模仿原有的回调,这里我们以block
形式回调。
// 获取存储的数组
NSMutableArray *array = objc_getAssociatedObject(self, (__bridge void *)cc_associationKey);
for (CCObserver *observer in array) {
NSDictionary *dic = @{CCKeyValueChangeOldKey:oldValue, CCKeyValueChangeNewKey:newValue};
// [observer.observer cc_observeValueForKey:key object:observer.observer change:dic];.
!observer.handler ? : observer.handler(observer.observer, key, dic); // 执行回调
}
总结
KVO涉及到知识点非常多,而且很多是runtime
,对这方面的薄弱的同学很可能看不懂,需要多看多读多写,反复消化,这段代码为什么这么写,而不这么写,都需要认真理解后才知道。这里还可以延伸很多其他的知识。消息的转发、通过关联值为分类添加属性、动态添加方法、类的指向及其结构、继承、分类的作用...
,多得不要不要的。而我们这次的实践也很好的证明了KVO确实是通过runtime
,动态创建一个新类作为原对象的子类,把isa
指针指向新类,并对其重写setter
方法,以监听属性值的变化来通知外界。
下一次分享什么好呢,我自己都很期待😆
本次的demo
示例及demo的参考来源及文章
iOS开发·KVO用法,原理与底层实现
How Key-Value Observing (KVO) is actually implemented at the runtime level