一适袜、VKO 簡(jiǎn)述
KVO 全稱(chēng) Key Value Observing趁尼,俗稱(chēng)“鍵值監(jiān)聽(tīng)”;可以監(jiān)聽(tīng)對(duì)象某個(gè)屬性值的變化
1. KVO 是已什么方式實(shí)現(xiàn)的嚎尤?(底層原理是什么荔仁?)
答:當(dāng)對(duì)一個(gè)對(duì)象添加監(jiān)聽(tīng)(addObserver:forKeyPath: ... ),iOS會(huì)修改該對(duì)象的 isa (isa默認(rèn)指向?qū)ο笏鶎俚念?lèi))芽死。改為指向一個(gè)通過(guò)Runtime動(dòng)態(tài)創(chuàng)建的子類(lèi)乏梁,子類(lèi)擁重寫(xiě) set 方法,并且 set 方法內(nèi)部會(huì)順序調(diào)用 willChangeValueForKey, 原來(lái)的set方法,即:[super set...], didChangeValueForKey关贵。并且會(huì)在 didChangeValueForKey 中調(diào)用KVO的回調(diào)方法:observeValueForKeyPath:ofObject:change:context:
2. 如何手動(dòng)觸發(fā)KVO?
答:已添加監(jiān)聽(tīng)的屬性遇骑,在值發(fā)生變化時(shí),系統(tǒng)會(huì)自動(dòng)觸發(fā)回調(diào)揖曾。如果想要手動(dòng)觸發(fā)落萎,則需我們自己調(diào)用 willChangeValueFor 和 didChallengeValueForKey方法,這兩個(gè)方法缺一不可炭剪。
二练链、KVO 實(shí)現(xiàn)原理探索
1. 話不多說(shuō),上代碼:
- (void)useSystemKVOTest {
// 1. 創(chuàng)建測(cè)試對(duì)象
self.p1 = [Person new];
self.p2 = [Person new];
self.p1.age = 1;
self.p2.age = 2;
// 2. 打印監(jiān)聽(tīng)前p1奴拦、p2 所屬類(lèi)媒鼓、setter 方法實(shí)現(xiàn)地址
NSLog(@"監(jiān)聽(tīng)前 p1 class is : %@, p2 class is : %@", object_getClass(self.p1), object_getClass(self.p2));
// 輸出結(jié)果:監(jiān)聽(tīng)前 p1 class is : Person, p2 class is : Person
NSLog(@"監(jiān)聽(tīng)前 p1-setAage: address is : = %p, p2-setAage: address is : %p", [self.p1 methodForSelector:@selector(setAge:)], [self.p2 methodForSelector:@selector(setAge:)]);
// 輸出結(jié)果:監(jiān)聽(tīng)前 p1-setAage: address is : = 0x102f98ea8, p2-setAage: address is : 0x102f98ea8
// 3. 添加監(jiān)聽(tīng),
[self.p1 addObserver:self forKeyPath:NSStringFromSelector(@selector(age)) options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
// 4. 打印監(jiān)聽(tīng)后p1粱坤、p2 所屬類(lèi)隶糕、setter 方法實(shí)現(xiàn)地址
NSLog(@"監(jiān)聽(tīng)后 p1 class is : %@, p2 class is : %@", object_getClass(self.p1), object_getClass(self.p2));
// 輸出結(jié)果:監(jiān)聽(tīng)后 p1 class is : NSKVONotifying_Person, p2 class is : Person
NSLog(@"監(jiān)聽(tīng)后 p1-setAage: address is : = %p, p2-setAage: address is : %p", [self.p1 methodForSelector:@selector(setAge:)], [self.p2 methodForSelector:@selector(setAge:)]);
// 輸出結(jié)果:監(jiān)聽(tīng)后 p1-setAage: address is : = 0x194c61d54, p2-setAage: address is : 0x102f98ea8
// 5. 改變值
self.p1.age = 10;
self.p2.age = 20;
// 6.移除 p1.age 的監(jiān)聽(tīng)者
[self.p1 removeObserver:self forKeyPath:NSStringFromSelector(@selector(age))];
}
// kvo 回調(diào)方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"監(jiān)聽(tīng)到 %@ 的 %@ 改變了 %@", [object isEqual:self.p1]?@"p1":@"p2", keyPath, change);
/* 輸出結(jié)果:
監(jiān)聽(tīng)到 p1 的 age 改變了 {
kind = 1;
new = 10;
old = 1;
}
*/
}
2. 有以上輸出結(jié)果,我們發(fā)現(xiàn):
- 在添加監(jiān)聽(tīng)后站玄,p1 的 isa 指向了 NSKVONotifying_Person
- NSKVONotifyin_Person其實(shí)是Person的子類(lèi),那么也就是說(shuō)其superclass指針是指向Person類(lèi)對(duì)象的
- NSKVONotifyin_Person 是 runtime 在運(yùn)行時(shí)生成的濒旦。那么 p1 對(duì)象在調(diào)用 setage 方法的時(shí)候株旷,肯定會(huì)根據(jù) p1 的 isa 找到NSKVONotifyin_Person,在 NSKVONotifyin_Person 中找 setage 的方法及實(shí)現(xiàn)。
- p1 的 setAge 方法的實(shí)現(xiàn)由 Person 類(lèi)方法中的 setAge 方法轉(zhuǎn)換為了C語(yǔ)言的 Foundation 框架的 _NSsetIntValueAndNotify 函數(shù)晾剖。
3. NSKVONotifyin_Person 的內(nèi)部結(jié)構(gòu):
首先我們知道锉矢,NSKVONotifyin_Person作為Person的子類(lèi),其superclass指針指向Person類(lèi)齿尽,并且NSKVONotifyin_Person內(nèi)部一定對(duì)setAge方法做了單獨(dú)的實(shí)現(xiàn)沽损,那么NSKVONotifyin_Person同Person類(lèi)的差別可能就在于其內(nèi)存儲(chǔ)的對(duì)象方法及實(shí)現(xiàn)不同。
我們通過(guò)runtime分別打印Person類(lèi)對(duì)象和NSKVONotifyin_Person類(lèi)對(duì)象內(nèi)存儲(chǔ)的對(duì)象方法
- (void)printMethods {
[self printMehtodsOfClass:object_getClass(self.p1)];
[self printMehtodsOfClass:object_getClass(self.p2)];
}
- (void)printMehtodsOfClass:(Class)cls {
unsigned int count = 0;
Method * methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = @"".mutableCopy;
[methodNames appendFormat:@"%@ - ", cls];
for (int i = 0; i < count; i++) {
Method method = methods[i];
NSString * methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString:methodName];
[methodNames appendString:@" "];
}
NSLog(@"%@", methodNames);
free(methods);
}
通過(guò)上述代碼我們發(fā)現(xiàn)NSKVONotifyin_Person中有4個(gè)對(duì)象方法循头。分別為setAge: class dealloc _isKVOA绵估,那么至此我們可以畫(huà)出NSKVONotifyin_Person的內(nèi)存結(jié)構(gòu)以及方法調(diào)用順序。
這里NSKVONotifyin_Person重寫(xiě)class方法是為了隱藏NSKVONotifyin_Person卡骂。不被外界所看到国裳。我們?cè)趐1添加過(guò)KVO監(jiān)聽(tīng)之后,分別打印p1和p2對(duì)象的class可以發(fā)現(xiàn)他們都返回Person全跨。
NSLog(@"%@, %@", [self.p1 class], [self.p2 class]);
// 打印結(jié)果 Person, Person
三. 自定義 KVO 實(shí)現(xiàn)監(jiān)聽(tīng)
1. ViewController 調(diào)用實(shí)現(xiàn):
#import "ViewController.h"
#import "Person.h"
#import "NSObject+YJKVO.h"
@interface ViewController ()
@property (nonatomic, strong) Person * p;
@end
@implementation ViewController
#pragma mark - Life Cycle
- (void)viewDidLoad {
[super viewDidLoad];
[self useCustomKVOTest];
}
#pragma mark - 使用自定義kvo
- (void)useCustomKVOTest {
self.p = [[Person alloc] init];
[self.p yj_addObserver:self forKeyPath:NSStringFromSelector(@selector(name))];
self.p.name = @"張三";
}
#pragma mark - 自定義kvo缝左,回調(diào)
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object newValue:(id)newValue {
NSLog(@"newValue = %@", newValue);
}
2. Person 類(lèi)
- Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString * name;
@end
- Person.m
#import "Person.h"
@implementation Person
- (void)setName:(NSString *)name {
_name = name;
NSLog(@"調(diào)用了");
}
@end
3. 定義一個(gè) NSObject 的分類(lèi) NSObject+YJKVO,實(shí)現(xiàn)KVO監(jiān)聽(tīng)
- NSObject+YJKVO.h
@interface NSObject (YJKVO)
/// 添加觀察者
/// @param observer 觀察者
/// @param keyPath keyPath
- (void)yj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
/// 移除觀察者
/// @param observer 觀察者
/// @param keyPath keyPath
- (void)yj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
/// kvo 回調(diào)方法 (由觀察者實(shí)現(xiàn))
/// @param keyPath keyPath
/// @param object 被觀察對(duì)象
/// @param newValue 新值
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object newValue:(id)newValue;
@end
- NSObject+YJKVO.m
#import "NSObject+YJKVO.h"
#import <objc/message.h>
// 通過(guò) Runtime 動(dòng)態(tài)成子類(lèi)的前綴
static NSString *const YJKVOPrefix = @"YJKVO_";
// 關(guān)聯(lián) 觀察者
static NSString *const YJKVOAssociatedOberverKey = @"YJKVOAssociatedOberverKey";
@implementation NSObject (YJKVO)
#pragma mark - -- public methods
/// 添加觀察者
/// @param observer 觀察者
/// @param keyPath keyPath
- (void)yj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
// 1. 檢查時(shí)候有 set 方法
NSString *setterMethodName = setterForGetter(keyPath);
SEL setterSel = NSSelectorFromString(setterMethodName);
// method
Method method = class_getInstanceMethod(self.class, setterSel);
if (!method) {
@throw [[NSException alloc] initWithName:NSExtensionItemAttachmentsKey reason:@"沒(méi)有setter方法" userInfo:nil];
}
// 2. 動(dòng)態(tài)生成子類(lèi)
Class sub_Class = [self registerSubClassWithKeyPath:keyPath];
if (!sub_Class) {
@throw [[NSException alloc] initWithName:NSExtensionItemAttachmentsKey reason:@"子類(lèi)創(chuàng)建失敗" userInfo:nil];
}
// 3. 消息轉(zhuǎn)發(fā)
// 關(guān)聯(lián) observer
objc_setAssociatedObject(self, (__bridge void const * _Nonnull)YJKVOAssociatedOberverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/// 移除觀察者
/// @param observer 觀察者
/// @param keyPath keyPath
- (void)yj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
objc_removeAssociatedObjects(observer);
}
/// kvo 回調(diào)方法 (由觀察者實(shí)現(xiàn))
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(nonnull id)object newValue:(nonnull id)newValue { }
#pragma mark - -- private methods
#pragma mark - 通過(guò) getter 方法名浓若,獲取 setter 方法名渺杉;例如:age ==> setAge:
static NSString * setterForGetter(NSString *getter) {
if (getter.length < 1) {
return nil;
}
// 獲取第一個(gè)字符,變成打下
NSString *firstString = [[getter substringToIndex:1] uppercaseString]; // substringToIndex:從最前頭一直截取到Index
NSString *otherString = [getter substringFromIndex:1]; // substringFromIndex:從Index開(kāi)始截取到最后
// 拼接 age == > setAag:
return [NSString stringWithFormat:@"set%@%@:", firstString, otherString];
}
#pragma mark - 通過(guò) setter 方法名挪钓,獲取 getter 方法名是越;例如:setAge: ==> age
static NSString * getterForSetter(NSString *setter) {
if (setter.length < 1 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
return nil;
}
NSString *getter = [setter substringFromIndex:3];
getter = [getter substringToIndex:getter.length-1];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
#pragma mark - 動(dòng)態(tài)生成子類(lèi)
/// 運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建子類(lèi)
/// @param keyPath keyPath
- (Class)registerSubClassWithKeyPath:(NSString *)keyPath {
// 子類(lèi)名
NSString *subClsName = [NSString stringWithFormat:@"%@%@", YJKVOPrefix, self.class];
// 子類(lèi),一個(gè) NSObject 默認(rèn)分貝 16 個(gè)字節(jié)
Class subCls = objc_allocateClassPair(self.class, subClsName.UTF8String, 16);
// 注冊(cè)
objc_registerClassPair(subCls);
// 給子類(lèi)動(dòng)態(tài)添加 setter诵原、class 實(shí)現(xiàn)
Method class_method = class_getClassMethod(self.class, @selector(class));
Method setter_method = class_getClassMethod(self.class, NSSelectorFromString(setterForGetter(keyPath)));
class_addMethod(subCls, @selector(class), (IMP)yj_class, method_getTypeEncoding(class_method));
class_addMethod(subCls, NSSelectorFromString(setterForGetter(keyPath)), (IMP)yj_setter, method_getTypeEncoding(setter_method));
// 將父類(lèi)的 isa 指向子類(lèi)
object_setClass(self, subCls);
// 返回
return subCls;
}
#pragma mark - 重寫(xiě) class 方法
static Class yj_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}
#pragma mark - 重寫(xiě) setter 方法
/// 重寫(xiě) setter 方法
/// @param newValue 新值
static void yj_setter(id self, SEL _cmd, id newValue) {
// 1. 調(diào)用 super setter 方法
struct objc_super super_cls = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
// 調(diào)用父類(lèi) setter 方法 設(shè)置新值
((void(*) (id, SEL, id)) (void *)objc_msgSendSuper)((__bridge id)(&super_cls), _cmd, newValue);
// 2. 取出觀察者英妓,調(diào)用kvo 回調(diào)方法
id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(YJKVOAssociatedOberverKey));
//
SEL handleSel = @selector(yj_observeValueForKeyPath:ofObject:newValue:);
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
// Runtime 調(diào)用回到方法
// objc_msgSend() 默認(rèn)的情況下,不支持添加參數(shù)绍赛。
// 解決方案一: Build Setting –> 搜索: Enable Strict Checking of objc_msgSend Calls 改為 NO (我自己試了下蔓纠,無(wú)效 Xcode12.1)
// 解決方案二: 這里通過(guò)(void *)送入5個(gè)參數(shù),你可以根據(jù)自己參數(shù)類(lèi)型強(qiáng)轉(zhuǎn)原本是void()的函數(shù)方法
((void (*) (id, SEL, NSString*, id, id)) (void*)objc_msgSend)(observer, handleSel, keyPath, self, newValue);
}
@end