2.RAC解析 - 自定义KVO

知识点概述

1.KVO实现原理
2.runtime使用

目的

给NSObject添加一个Category,用于给实例对象添加观察者,当该实例对象的某个属性发生变化的时候通知观察者。

大体思路

添加观察者的方法中

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

会用runtime的方式手动创建一个其子类,并且将该对象变为该子类。该子类会复写观察方法中keyPath的setter方法,使这个setter被调用时利用runtime去调用observer的回调方法

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

实现

这里只做KVO的基本功能,当被观察者改变属性的时候通知观察者,所以定义如下方法

NSObject+SQKVO.h

/**
 添加观察者

 @param observer 观察者
 @param keyPath 被观察的属性名
 */
- (void)SQ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;


/**
 当被观察的观察属性改变的时候的回调函数

 @param keyPath 所观察被观察者的属性名
 @param object 被观察者
 @param value 被观察的属性的新值
 */
- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value;

@end

因为这里要用到runtime所以需要添加runtime的头文件

#import <objc/message.h>

而且因为用到objc_msgSend所以要改变一下工程的环境变量


##

一.动态生成子类

在被观察者调用- SQ_addObserver:forKeyPath:时首先动态生成一个其子类。

    // 1.生成子类
    // 1.1获取名称
    Class selfClass = [self class];
    NSString *className = NSStringFromClass(selfClass);
    NSString *KVOClassName = [className stringByAppendingString:@"_SQKVO"];
    const char *KVOClassNameChar = [KVOClassName UTF8String];
    // 1.2创建子类
    Class KVOClass = objc_allocateClassPair(selfClass, KVOClassNameChar, 0);
    // 1.3注册
    objc_registerClassPair(KVOClass);

这里可以看到,我们将子类的类名命名为“类名”+“SQKVO”,譬如类名为“Person”,这个子类是“Person_SQKVO”。
这里有个注意点,一般为动态创建的类名应尽量复杂一些避免重复。最好加上“
”。

二.根据KeyPath动态添加对应的setter

1 确定setter的名字

举个例子,如果用户给的keyPath是name,应该动态添加一个-setName:的方法。而这个setter的名字是 "set" + "把keyPath变为首字母大写" + ":"
所以可以得出

NSString *setterString =
    [NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]];
    SEL setter =  NSSelectorFromString(setterString);
2 利用class_addMethod()给子类动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
  • cls:
    给哪个类添加方法。即新生成的子类,上面生成的 KVOClass。
  • name:
    所添加方法的名称。即上一步生成的字符串 setterString。
  • imp:
    所添加方法的实现。即这个方法的C语言实现,首先在下面先写一个C语言的方法。稍后会讲具体实现。
void setValue(id self, SEL _cmd, id newVale) {
}
  • types:
    所添加方法的编码类型。setter的返回值是void,参数是一个对象(id)。void用"v"表示,返回值和参数之间用“@:”隔开,对象用"@"表示。最后我们可以得出结果"v@:@"。
    具体其他的编码类型可以参考苹果文档

ps: 这里说下为什么返回值和参数之间用“@:”隔开。“:”代表字符串,所有的OC方法都有两个隐藏参数在参数列表的最前面,“发起者”和 “方法描述符”,“@”就是这个发起者,“:”是方法描述符。而这个types其实是imp返回值和参数的编码。因为OC方法中返回值和参数之间必然有“发起者”和“SEL”隔着,所以“@:”自然而然就成了返回值和参数之间的分隔符。
当然我们还可以用@encode来得到我们想要的编码类型

NSString *encodeString =
[NSString stringWithFormat:@"%s%s%s%s",
@encode(void), 
@encode(id), 
@encode(SEL), 
@encode(id)];
3 将当前对象的类变为我们所创建的子类的类型,即更改isa指针
object_setClass(self, KVOClass);
4 将keyPath和观察者关联(associate)到我们的对象上

用下面这个函数可以很方便的将一个对象用键值对的方式绑定到一个目标对象上。
*如果想了解跟多可以查找《Effective Objective-C》的第10条

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
  • object
    目标对象

  • key
    绑定对象的键,相当于NSDictionary的key
    这里的key一般采用下面的方式声明:

static const void *SQKVOObserverKey = &SQKVOObserverKey;
static const void *SQKVOKeyPathKey = &SQKVOKeyPathKey;

这样做是因为若想令两个键匹配到同一个值,则两者必须是完全相同的指针才行。

  • value
    绑定对象,相当于NSDictionary的value

  • policy
    绑定对象的缓存策略
    @property (nonatomic, weak) :OBJC_ASSOCIATION_ASSIGN
    @property (nonatomic, strong) :OBJC_ASSOCIATION_RETAIN_NONATOMIC
    @property (nonatomic, copy) :OBJC_ASSOCIATION_COPY_NONATOMIC
    @property (atomic, strong) :OBJC_ASSOCIATION_RETAIN
    @property (atomic, weak) :OBJC_ASSOCIATION_COPY

最后关联的代码:

objc_setAssociatedObject(self, SQKVOObserverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, SQKVOKeyPathKey, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);

三.setValue()的实现

这个函数的目的主要是:
1.利用objc_msgSend触发原先类的setter
2.利用objc_msgSend触发观察者的回调方法

1. 触发原先的setter方法
    // 保存子类
    Class KVOClass = [self class];
    
    // 变回原先的类型,去触发setter
    object_setClass(self, class_getSuperclass(KVOClass));
    NSString *keyPath = objc_getAssociatedObject(self, SQKVOKeyPathKey);
    NSString *setterString = [NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]];
    SEL setter = NSSelectorFromString(setterString);
    objc_msgSend(self, setter, newVale);
2. 调用观察者的回调方法
id observer = objc_getAssociatedObject(self, SQKVOObserverKey);
    objc_msgSend(observer, @selector(SQ_observeValueForKeyPath:ofObject:changeValue:), keyPath, self, newVale);
3.改回KVO类
object_setClass(self, KVOClass);

四.实现空的回调方法

- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value {
    
}

五.调用自定义的KVO

恭喜你看到这里,并且恭喜你已经成功了!

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.name = @"A";
    [self SQ_addObserver:self forKeyPath:@"name"];
    
    self.name = @"B";
}

- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value {
    NSLog(@"%@.%@=%@", object, keyPath, value);
}

六.代码

代码下载地址

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,698评论 0 9
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • 萧凌风微微闭眼,思绪回到了25年前。 那年,江湖正邪一战十分轰动,子月派与韶日教于乐山之巅,两败俱伤。子月...
    筱辞1988阅读 292评论 0 0
  • 没有故事 没有酒 没有你
    欹芸阅读 137评论 0 0
  • 有些人,腹中有诗气自华,饱读诗书,内有乾坤,阅历的沉淀使得双眼炯炯有神,无形的气场令人折服! 当然,一...
    紫川曾来阅读 61评论 1 2