使用Runtime自定义KVO,原理浅析
一、介绍
什么是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,原理浅析的更多相关文章
- 【OC底层】KVO原理
KVO的原理是什么?底层是如何实现的? KVO是Key-value observing的缩写. KVO是Objective-C是使用观察者设计模式实现的. Apple使用了isa混写(isa-swiz ...
- 老生常谈系列之Aop--Spring Aop原理浅析
老生常谈系列之Aop--Spring Aop原理浅析 概述 上一篇介绍了AspectJ的编译时织入(Complier Time Weaver),其实AspectJ也支持Load Time Weaver ...
- KVO原理解析
KVO在我们项目开发中,经常被用到,但很少会被人关注,但如果面试一些大公司,针对KVO的面试题可能如下: 知道KVO嘛,底层是怎么实现的? 如何动态的生成一个类? 今天我们围绕上面几个问题,我们先看K ...
- KVO的使用三:基于runtime实现KVO
苹果的KVO原理通过isa-swizzling技术实现,本质实现逻辑是在runtime时添加一个子类,重写set方法进行操作,现在我们也基于runtime来实现一个KVO. 首先新建一个Person类 ...
- Runtime - 消息发送原理
Runtime - 消息发送原理. Objective-C运行时的核心就在于消息分派器objc_msgSend,消息分派器把选择器映射为函数指针,并调用被引用的函数. 要想理解objc_msgSend ...
- 沉淀,再出发:docker的原理浅析
沉淀,再出发:docker的原理浅析 一.前言 在我们使用docker的时候,很多情况下我们对于一些概念的理解是停留在名称和用法的地步,如果更进一步理解了docker的本质,我们的技术一定会有质的进步 ...
- HTTP长连接和短连接原理浅析
原文出自:HTTP长连接和短连接原理浅析
- Javascript自执行匿名函数(function() { })()的原理浅析
匿名函数就是没有函数名的函数.这篇文章主要介绍了Javascript自执行匿名函数(function() { })()的原理浅析的相关资料,需要的朋友可以参考下 函数是JavaScript中最灵活的一 ...
- [转帖]Git数据存储的原理浅析
Git数据存储的原理浅析 https://segmentfault.com/a/1190000016320008 写作背景 进来在闲暇的时间里在看一些关系P2P网络的拓扑发现的内容,重点关注了Ma ...
随机推荐
- Windows7安装PowerShell5.1方法(Flutter新版本需要)
Windows7安装PowerShell5.1方法(Flutter新版本需要) 重新安装Windows7系统,在使用Flutter的时候,发现需要PowerShell5.0以上版本,需要升级Win ...
- Matlab线性规划
线性规划 线性规划的标准形式 \[\underset{x}{min}{\ c^Tx}\ s.t.\ Ax \leqslant b\] 例如,线性规划为: \[ \underset{x}{min ...
- layui多个时间选择器出现闪退问题
1.出现问题的代码 laydate.render({ elem: '#startDate' // }); laydate.render({ elem: '#endDate' // }); laydat ...
- 【机器学习基础】交叉熵(cross entropy)损失函数是凸函数吗?
之所以会有这个问题,是因为在学习 logistic regression 时,<统计机器学习>一书说它的负对数似然函数是凸函数,而 logistic regression 的负对数似然函数 ...
- Codechef RIN 「Codechef14DEC」Course Selection 最小割离散变量模型
问题描述 提供中文版本好评,一直以为 Rin 是题目名字... pdf submit 题解 参考了 东营市胜利第一中学姜志豪 的<网络流的一些建模方法>(2016年信息学奥林匹克中国国家队 ...
- C++入门到理解阶段二核心篇(1)——c++面向对象概述、内存分析、引用
1.c++内存分区模型 c++程序在运行的过程中,内存会被划分为以下四个分区 代码区:程序的所有程序的二进制代码,包括注释会被放到此区 全局区:存放静态变量.全局变量.常量(字符串常量和const修饰 ...
- go语言之用户输入&类型别名&类型转换
1.用户输入 package main import "fmt" func main() { //用户输入,程序接受并输出 var v1 int //fmt.Println(&qu ...
- Thinkphp 5.1.24 parseKey缺陷导致聚合注入 分析
测试url: http://127.0.0.1/thinkphp/thinkphp_5.1.24/public/index.php/index/index/sqli2?id=2 控制器是获取id参数作 ...
- C#以对象为成员的例子
using System; using System.Collections.Generic; using System.Text; namespace test { class Program { ...
- centos7.2安装及管理docker
使用的操作系统是是centos7.2,按照官方的推荐的配置,把linux内核升级到3.8以上.安装步骤如下: 1.升级内核版本(包含aufs)cd /etc/yum.repos.dwget http: ...