iOS KVO原理的探究实现

感觉很久都没有写新文章了,还是写一些小文章吧,记录一下也好!
这一次和大家讲解的是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,但我们可以通过runtimeobject_getClass()获取其真正的类名,而且[obj class]其内部实现就是下面的样子👇

- (Class)class {
    return object_getClass(self);
}

你还会发现,当你重写TestObjectclass方法,这方法已经不走了,原因就是被重写了。来,让我们再重写一些方法,看看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;
}

我们通过runtimeobjc_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);

新类已经创建完了,我们继续通过runtimeclass_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方法,并且把相关信息存储到数组里面,继续以runtimeobjc_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,又是一个知识点,这里最好能理解superself之间的关系,例如:[[super alloc] init]这段代码,意思是调用父类的方法分配内存、初始化,但是其接收接者是self
((void (*)(id, SEL, id)) (void *)objc_msgSendSuper)((__bridge id)(&superClass), _cmd, (id)newValue);没错,这里的做法是为了让原类TestObjectsetter方法生效,而这里为什么用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

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

推荐阅读更多精彩内容