一、介绍

什么是KVO?全称key-value-observer,键值观察,观察者设计模式的另一种实现。其作用是通过观察者监听属性值的变化而做出函数回调。

二、原理

KVO基于Runtime机制实现,使用了isa的混写技术

监听者监听类A的某一个属性的变化,系统会动态为类A创建一个子类NSKVONotifying_A,并将类A的isa指针重新指向该子类

系统会重写类A的setter方法。( 赋值前后分别调用willChangeValueForKey和didChangeValueForKey跟踪新旧值 )

当类A的属性发生改变时,系统通知监听者,调用observeValueForKey:ofObject:change:context方法即可

三、图示

三、基本实现

Student类

//  Created by 夏远全 on 2019/10/12.

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@end

ViewController类

-(void)test_objc_KVO {

    //创建对象
self.stu = [[Student alloc] init];
self.stu.name = @"张三"; //注册观察者
[self.stu addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL]; //修改值
self.stu.name = @"李四";
} -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"被观测对象:%@, 被观测的属性:%@, 值的改变: %@\n", object, keyPath, change); }

断点结果:监听前后,stu对象的类类型发生了变化,Student -> NSKVONotifying_Student

打印结果

-- ::01.251412+ 运行时[:] 被观测对象:<Student: 0x600003d9f3c0>, 被观测的属性:name, 值的改变: {
kind = ;
new = "\U674e\U56db";
}

四、自定义

(1)思想:

  • 添加监听方法
  • 创建一个子类
  • 改写父类isa指针
  • 关联观察者
  • 重写setter方法
  • 给父类发送setter消息
  • 给观察者发送observeValueForKeyPath:ofObject:change:context:消息

(2)实现

Student类

//  Created by 夏远全 on 2019/10/12.

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Student : NSObject
@property (nonatomic, copy) NSString *name; ///自定义的监听方法
-(void)xyq_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; @end NS_ASSUME_NONNULL_END
//
// Student.m
// 运行时
//
// Created by 夏远全 on 2019/10/12.
// #import "Student.h"
#import <objc/message.h> @implementation Student void setterMethod(id self, SEL _cmd, NSString *name) { //5、调用父类的方法
struct objc_super superClass = {
self,
class_getSuperclass([self class])
};
objc_msgSendSuper(&superClass, _cmd, name); //6、通知观察者调用observeValueForKeyPath:ofObject:change:context:
id observer = objc_getAssociatedObject(self, (__bridge const void *)@"observer");
NSString *methodName = NSStringFromSelector(_cmd);
NSString *key = getValueKey(methodName);
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), key, self, @{key:name}, nil); } /// 通过setter方法截取属性名
NSString *getValueKey(NSString *setter){ //去掉set
if ([setter hasPrefix:@"set"]) {
setter = [setter stringByReplacingOccurrencesOfString:@"set" withString:@""];
}
//去掉:
if ([setter hasSuffix:@":"]) {
setter = [setter stringByReplacingOccurrencesOfString:@":" withString:@""];
}
//首字母小写
NSString *key = [setter stringByReplacingOccurrencesOfString:[setter substringToIndex:] withString:[[setter substringToIndex:] lowercaseString] options: range:NSMakeRange(,)];
return key;
} - (void)xyq_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context { //1、生成子类
const char * clazz = class_getName([self class]);
NSString *className = [NSString stringWithCString:clazz encoding:NSUTF8StringEncoding];
NSString *subClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
Class subClass = objc_getClass(subClassName.UTF8String);
subClass = objc_allocateClassPair([self class], [subClassName UTF8String], );
objc_registerClassPair(subClass); //2、isa指向子类
object_setClass(self, subClass); //3、关联观察者
objc_setAssociatedObject(self, (__bridge const void *)@"observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); //4、重写set方法
NSString *setNameStr = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];
SEL setSelector = NSSelectorFromString(setNameStr);
class_addMethod(subClass, setSelector, (IMP)setterMethod, "v@:@");
} @end

ViewController类

-(void)test_objc_KVO {

    //创建对象
self.stu = [[Student alloc] init];
self.stu.name = @"张三"; //注册观察者
[self.stu xyq_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL]; //修改值
self.stu.name = @"李四";
} -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"被观测对象:%@, 被观测的属性:%@, 值的改变: %@\n", object, keyPath, change); }

断点结果:监听前后,stu对象的类类型发生了变化,Student -> NSKVONotifying_Student

打印结果

-- ::17.929772+ 运行时[:] 被观测对象:<NSKVONotifying_Student: 0x6000002e0420>, 被观测的属性:name, 值的改变: {
name = "\U674e\U56db";
}

五、扩展

这个案例只是浅浅的探究了一下实现原理,其他这个还有更大的应用。

我们可以给NSObject创建一个分类NSObject(KVO),实现各种属性的监听实现。

具体操作自己动手去吧,不在本文做演示。

使用Runtime自定义KVO,原理浅析的更多相关文章

  1. 【OC底层】KVO原理

    KVO的原理是什么?底层是如何实现的? KVO是Key-value observing的缩写. KVO是Objective-C是使用观察者设计模式实现的. Apple使用了isa混写(isa-swiz ...

  2. 老生常谈系列之Aop--Spring Aop原理浅析

    老生常谈系列之Aop--Spring Aop原理浅析 概述 上一篇介绍了AspectJ的编译时织入(Complier Time Weaver),其实AspectJ也支持Load Time Weaver ...

  3. KVO原理解析

    KVO在我们项目开发中,经常被用到,但很少会被人关注,但如果面试一些大公司,针对KVO的面试题可能如下: 知道KVO嘛,底层是怎么实现的? 如何动态的生成一个类? 今天我们围绕上面几个问题,我们先看K ...

  4. KVO的使用三:基于runtime实现KVO

    苹果的KVO原理通过isa-swizzling技术实现,本质实现逻辑是在runtime时添加一个子类,重写set方法进行操作,现在我们也基于runtime来实现一个KVO. 首先新建一个Person类 ...

  5. Runtime - 消息发送原理

    Runtime - 消息发送原理. Objective-C运行时的核心就在于消息分派器objc_msgSend,消息分派器把选择器映射为函数指针,并调用被引用的函数. 要想理解objc_msgSend ...

  6. 沉淀,再出发:docker的原理浅析

    沉淀,再出发:docker的原理浅析 一.前言 在我们使用docker的时候,很多情况下我们对于一些概念的理解是停留在名称和用法的地步,如果更进一步理解了docker的本质,我们的技术一定会有质的进步 ...

  7. HTTP长连接和短连接原理浅析

    原文出自:HTTP长连接和短连接原理浅析

  8. Javascript自执行匿名函数(function() { })()的原理浅析

    匿名函数就是没有函数名的函数.这篇文章主要介绍了Javascript自执行匿名函数(function() { })()的原理浅析的相关资料,需要的朋友可以参考下 函数是JavaScript中最灵活的一 ...

  9. [转帖]Git数据存储的原理浅析

    Git数据存储的原理浅析 https://segmentfault.com/a/1190000016320008   写作背景 进来在闲暇的时间里在看一些关系P2P网络的拓扑发现的内容,重点关注了Ma ...

随机推荐

  1. 《精通Python爬虫框架Scrapy》学习资料

    <精通Python爬虫框架Scrapy>学习资料 百度网盘:https://pan.baidu.com/s/1ACOYulLLpp9J7Q7src2rVA

  2. Android Studio的安装及第一次启动时的配置

    Android Studio的安装及第一次启动时的配置 一.下载Android Studio 百度搜索“Android Studio" 点击中文社区进入,选择最新版本下载. 下载后双击安装包 ...

  3. Python装饰器(Decorators )

    http://book.pythontips.com/en/latest/decorators.html 在<Built-in Functions(3.6)>和<Python上下文管 ...

  4. steamdb cookie

    steamdb cookie import requests, re, os, pymysql, time from lxml import etree from steamdb.YDM import ...

  5. Windows10安装ubuntu18.04双系统教程

    写在前面:本教程为windows10安装ubuntu18.04(64位)双系统教程,是我多次安装双系统的经验总结,安装方法同样适用于ubuntu16.04(64位).为了直观和易于理解,我会尽量图文并 ...

  6. [译]Vulkan教程(11)Image Views

    Image views To use any VkImage, including those in the swap chain, in the render pipeline we have to ...

  7. HBase删除数据的原理

    转自:https://blog.csdn.net/cenjianteng/article/details/96645447 -------------------------------------- ...

  8. Python 从入门到进阶之路(四)

    之前的文章我们简单介绍了一下 Python 的几种变量类型,本篇文章我们来看一下 Python 中的函数. 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段. 函数能提高应用的模块性, ...

  9. Java入门系列之StringBuilder、StringBuffer(三)

    前言 上一节我们讲解了字符串的特性,除了字符串类外,还有两个我们也会经常用到的类,那就是StringBuffer和StringBuilder.因为字符串不可变,所以我们每次对字符串的修改比如通过连接c ...

  10. javaWeb核心技术第七篇之HTTP、Tomcat、Servlet、Request和Response

    - Web服务器 - 概念: - web资源: "英文直译"网"的意思 资源:一切数据文件 web资源:通过网络可以访问到的资源,通常指的是一切放在服务器上的文件&quo ...