一猴蹂、關(guān)于KVO
1. KVO實現(xiàn)原理
實現(xiàn)kvo監(jiān)聽某一屬性值變化的相關(guān)代碼:
- (void)viewDidLoad {
[super viewDidLoad];
People *p = [[People alloc] init];
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
p.name = @"123";
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
當一個對象的屬性被觀察時瓶摆,系統(tǒng)會動態(tài)創(chuàng)建了一個子類榆鼠;
并且改變了原有對象的isa指針指向遥昧,指向動態(tài)創(chuàng)建的子類;
子類中重寫了被觀察屬性的set方法润歉,在使用點方法和set方法給屬性賦值時模狭,最終調(diào)用的是子類中的set方法。
在addObserver處設(shè)置斷點觀察對象isa指針變化踩衩,被觀察前isa指針指向的是原始類如圖:
而執(zhí)行代碼被觀察后嚼鹉,指針指向的是NSKVONotifying_People類,可自行實驗驱富。
2. 自定義KVO
創(chuàng)建一個分類新增一個方法HBaddObserver锚赤,在方法中創(chuàng)建子類注冊并指向子類,再為子類添加set方法既可褐鸥。自定義kvo過程中线脚,主要使用到的系統(tǒng)方法:
//// 1.創(chuàng)建一個子類
/**
superclass:設(shè)置新類的父類
name:新類名稱
extraBytes:額外字節(jié)數(shù)設(shè)置為0
*/
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes)
//// 2.注冊該類
/**
cls:當前要注冊的類,注冊后才可以使用
*/
objc_registerClassPair(Class _Nonnull cls)
//// 3.設(shè)置當前對象指向其他類
/**
obj:要設(shè)置的對象
cls:指向的類
*/
object_setClass(id _Nullable obj, Class _Nonnull cls)
//// 4.動態(tài)添加一個方法
/**
cls:設(shè)置添加方法對應的類
name:選擇子(選擇器)名稱叫榕,描述了方法的格式浑侥,并不會指向方法
imp:函數(shù)名稱(函數(shù)指針),和選擇子一一對應晰绎,指向方法實現(xiàn)的地址
*/
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
代碼示例:
- 主控制器代碼:
#import "ViewController.h"
#import <objc/message.h>
#import "Person.h"
#import "NSObject+HBKVO.h"
@interface ViewController (){
Person *p;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
p = [[Person alloc] init];
[p HBaddObserver:self forKeyPath:@"Name" options:NSKeyValueObservingOptionNew context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"change:%@",change);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
static int num = 0;
p.Name = [NSString stringWithFormat:@"%d",num++];
}
@end
- person類頭文件代碼(.m中無相關(guān)代碼):
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic,strong)NSString *Name;
@end
NS_ASSUME_NONNULL_END
- 自定義KVO相關(guān)代碼(NSObject分類):
NSObject+HBKVO.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (HBKVO)
- (void)HBaddObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
NS_ASSUME_NONNULL_END
NSObject+HBKVO.m
#import "NSObject+HBKVO.h"
#import <objc/message.h>
@implementation NSObject (HBKVO)
-(void)HBaddObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
NSString *oldName = NSStringFromClass(self.class);
NSString *newName = [@"HBKVO_" stringByAppendingString:oldName];
//1寓落、創(chuàng)建一個子類
Class newClass = objc_allocateClassPair(self.class, newName.UTF8String, 0);
//2、注冊該類
objc_registerClassPair(newClass);
//3荞下、指向子類
object_setClass(self, newClass);
//4伶选、動態(tài)添加一個方法
NSString *first = [keyPath substringWithRange:NSMakeRange(0, 1)];
NSString *other = [keyPath substringFromIndex:1];
NSString *setName = [NSString stringWithFormat:@"set%@%@:",first.uppercaseString,other];//設(shè)置一個屬性名首字母大寫的方法
Method method = class_getInstanceMethod(self.class, sel_registerName(setName.UTF8String));
const char *types = method_getTypeEncoding(method);
class_addMethod(newClass, sel_registerName(setName.UTF8String), (IMP)setValue, types);
//class_addMethod(newClass, sel_registerName(setMethod.UTF8String), (IMP)setName, "v@:@");
//設(shè)置關(guān)聯(lián)數(shù)據(jù)
//獲取元類舊值使用
objc_setAssociatedObject(self, "keyPath", keyPath, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//設(shè)置新值的時候使用
objc_setAssociatedObject(self, "setName", setName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//通知值變化
objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//傳進來的內(nèi)容需要回傳
objc_setAssociatedObject(self, "context", (__bridge id _Nullable)(context), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
void setValue(id self,SEL _cmd,NSString *newValue){
NSLog(@"newValue:%@",newValue);
NSString *keyPath = objc_getAssociatedObject(self, "keyPath");
NSString *setName = objc_getAssociatedObject(self, "setName");
id observer = objc_getAssociatedObject(self, "observer");
id context = objc_getAssociatedObject(self, "context");
//存儲新類
Class newClass = [self class];
//指向父類獲取舊值
object_setClass(self, class_getSuperclass(newClass));
NSString *oldValue = objc_msgSend(self,sel_registerName(keyPath.UTF8String));
//對原始類屬性或成員變量復制
objc_msgSend(self, sel_registerName(setName.UTF8String),newValue);
NSMutableDictionary *change = [NSMutableDictionary dictionary];
if (oldValue) {
change[NSKeyValueChangeOldKey] = oldValue;
}
if (newValue) {
change[NSKeyValueChangeNewKey] = newValue;
}
//調(diào)用observer的回調(diào)方法
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),keyPath,observer,change,context);
//操作完成后指回動態(tài)創(chuàng)建的新類
object_setClass(self, newClass);
}
@end
一、關(guān)于KVC
KVC的全稱為KeyValueCoding尖昏,是對NSObjcet的擴展仰税,分類名為 : NSKeyValueCoding。我們經(jīng)常用KVC或者setter方法來觸發(fā)KVO抽诉,實現(xiàn)鍵值變化監(jiān)聽陨簇,實現(xiàn)一些功能。
1. KVC常用的方法說明
// 1迹淌、將鍵字符串key所對應的屬性的值設(shè)置為value河绽。不能設(shè)定屬性值時,將會引起接收器調(diào)用方法2
- (void)setValue:(nullable id)value forKey:(NSString *)key
// 2巍沙、當屬性值設(shè)置失敗,調(diào)用此方法
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key
// 3荷鼠、返回標識屬性的鍵字符串所對應的值句携。如果獲取失敗,將會引起接收器調(diào)用方法4
- (nullable id)valueForKey:(NSString *)key
// 4允乐、取值失敗矮嫉,調(diào)用此方法
- (nullable id)valueForUndefinedKey:(NSString *)key
// 5削咆、在鍵字符串key所對應的"標量"型屬性值設(shè)為nil,調(diào)用此方法蠢笋,并拋出NSInvalidArgumentException異常(可demo測試)
- (void)setNilValueForKey:(NSString *)key
// 6拨齐、默認返回值YES,代表如果沒有找到Set方法的話昨寞,會按照_key瞻惋,_iskey,key援岩,iskey的順序搜索成員歼狼,設(shè)置成NO就不這樣搜索
+ (BOOL)accessInstanceVariablesDirectly
標量 : 屬性中的單純的數(shù)值(整數(shù)、實數(shù)享怀、布爾值等)
在賦值的時候羽峰,如果是結(jié)構(gòu)體,必須包裝成NSValue實例添瓷;
如果是標量型屬性梅屉,必須包裝成NSNumber實例。
2. KVC賦值的實現(xiàn)原理
- 查找是否實現(xiàn)setter鳞贷、_setter 方法坯汤,如果有,優(yōu)先調(diào)用setter方法完成賦值(注意:set后面的鍵的第一字字母必須是大寫G幕巍玫霎!)
- 當沒找到setter方法,調(diào)用accessInstanceVariablesDirectly詢問妈橄。如果返回YES庶近,順序匹配變量名與 _<key>,_is<Key>,<key>,is<Key>,匹配到則設(shè)定其值眷蚓;如果返回NO,結(jié)束查找鼻种。并調(diào)用 setValue:forUndefinedKey:報異常
- 如果既沒有setter也沒有實例變量時,調(diào)用 setValue:forUndefinedKey:
結(jié)合demo沙热,寫下基本實現(xiàn)原理
// .h文件
#import <Foundation/Foundation.h>
@interface Peson : NSObject {
//_<key>, _is<Key>, <key>, or is<Key> 注意順序!!!
//NSString *_name;
//NSString *_isName;
// NSString *name;
NSString *isName;
}
@end
// .m文件
#import "Peson.h"
#import <objc/runtime.h>
@implementation Peson
- (void)setValue:(id)value forKey:(NSString *)key
{
NSString *setter = [[@"set" stringByAppendingString:[key capitalizedString]] stringByAppendingString:@":"];
// 1叉钥、檢查是否存在setter方法
if ([self respondsToSelector:NSSelectorFromString(setter)]) {
// 1.1 如果是標量型屬性賦值,且值為nil篙贸,賦值失敗
if (![value isKindOfClass:[NSObject class]] && value == nil)
{
[self setNilValueForKey:key];
// 1.2 如果是對象指針類型投队,直接進行賦值操作
}else{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(setter) withObject:value];
#pragma clang diagnostic pop
}
// 2、詢問 accessInstanceVariablesDirectly,默認YES,繼續(xù)往下查找
}else{
//獲取所有屬性列表
unsigned int count = 0;
Ivar *ivar = class_copyIvarList([self class], &count);
//_<key>, _is<Key>, <key>, or is<Key> 注意順序!!!
NSArray *searchPropretys = @[@"_name",@"_isName",@"name",@"isName"];
// 是否找到變量名的標志位爵川,判斷是否需要拋出異常
BOOL flag = false;
//遍歷屬性,依次匹配
for (int i = 0; i < count; i++) {
//如果找到了敷鸦,結(jié)束循環(huán)
if (flag) {
break;
}else{
Ivar var = ivar[i];
NSString *name = [NSString stringWithUTF8String:ivar_getName(var)];
for (int j = 0; j < searchPropretys.count; j++) {
//找到了,結(jié)束循環(huán)
if ([name isEqualToString:searchPropretys[j]]) {
flag = YES;
object_setIvar(self, var, value);
break;
}
}
}
}
//記得釋放
free(ivar);
//如果沒找到扒披,調(diào)用setValue: forUndefinedKey: 拋出異常
if (!flag) {
[self setValue:value forUndefinedKey:key];
}
}
}
+(BOOL)accessInstanceVariablesDirectly {
return YES;
}
注意: 上面有一個細節(jié)需要說下值依,對于標量型屬性賦值,如果是純數(shù)值碟案,需要使用包裝類NSNumber愿险,對于結(jié)構(gòu)體,需要用NSValue實例包裝价说。
通過上面我們也可以發(fā)現(xiàn)辆亏,為什么KVC和setter方法都可以觸發(fā)KVO 。
3. KVC取值的實現(xiàn)原理
- 查找是否實現(xiàn)getter方法熔任,依次匹配
-get<Key>
和-<key>
和is<Key>
褒链,如果找到,直接返回疑苔。需要注意的是 :如果返回的是對象指針類型甫匹,則返回結(jié)果;如果返回的是NSNumber轉(zhuǎn)換所支持的標量類型之一惦费,則返回一個NSNumber兵迅,否則,將返回一個NSValue- 當沒有找到getter方法薪贫,調(diào)用accessInstanceVariablesDirectly詢問恍箭,如果返回YES, _<key>瞧省,_is<Key>,<key>,is<Key>扯夭,找到了返回對應的值;如果返回NO鞍匾,結(jié)束查找交洗。并調(diào)用 valueForUndefinedKey: 報異常
- 如果沒找到getter方法和屬性值,調(diào)用 valueForUndefinedKey: 報異常橡淑。
緊接著實現(xiàn)上面demo的取值方法:
- (id)valueForKey:(NSString *)key
{
// 1. 查找getter方法
// -get<Key>
NSString *getKey = [@"get" stringByAppendingString:[key capitalizedString]];
NSString *isKey = [@"is" stringByAppendingString:[key capitalizedString]];
// 1.1 優(yōu)先查找 -get<Key>
if ([self respondsToSelector:NSSelectorFromString(getKey)])
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(getKey)];
#pragma clang diagnostic pop
// 1.2 查找 -<key>
}else if ([self respondsToSelector:NSSelectorFromString(key)])
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(key)];
#pragma clang diagnostic pop
// 1.3 查找 is<Key>
}else if ([self respondsToSelector:NSSelectorFromString(isKey)])
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(isKey)];
#pragma clang diagnostic pop
// 2. 詢問 accessInstanceVariablesDirectly构拳,是否繼續(xù)查找屬性,默認返回YES
}else{
//_<key>, _is<Key>, <key>, or is<Key> 注意順序!!!
//獲取所有屬性列表
unsigned int count = 0;
Ivar *ivar = class_copyIvarList([self class], &count);
//_<key>, _is<Key>, <key>, or is<Key> 注意順序!!!
NSArray *searchPropretys = @[@"_name",@"_isName",@"name",@"isName"];
BOOL flag = false;
//遍歷屬性,依次匹配
for (int i = 0; i < count; i++) {
//如果找到了梁棠,跳出外重循環(huán)
if (flag) {
break;
}else{
Ivar var = ivar[i];
NSString *name = [NSString stringWithUTF8String:ivar_getName(var)];
for (int j = 0; j < searchPropretys.count; j++) {
//找到了置森,結(jié)束循環(huán)
if ([name isEqualToString:searchPropretys[j]]) {
flag = YES;
return object_getIvar(self, var);
break;
}
}
}
}
free(ivar);
//如果沒找到,調(diào)用
if (!flag) {
[self valueForUndefinedKey:key];
}
}
return nil;
}
參考鏈接:
KVO實現(xiàn)原理
KVC和字典
iOS KVC實現(xiàn)原理
KVC的底層原理符糊,及自定義KVC
KVC的運用場景
1.動態(tài)的取值和設(shè)值
2.用KVC來訪問和修改私有變量
3.Model和字典轉(zhuǎn)換
4.修改一些控件的內(nèi)部屬性
最常用的是個性化UITextField中的placeHolderText
這里的關(guān)鍵點是如何獲取你要修改的樣式屬性名也就是key or keyPath名
KVC 能夠觸發(fā) KVO凫海,在 KVC 底層有手動觸發(fā) KVO的代碼,監(jiān)聽willChangeValueForKey 和 didChangeValueForKey可得到驗證男娄。
三行贪、KVO/KVC圖示
以Person類為示例把兔,在添加addObserver方法之前,實例對象瓮顽、類對象的關(guān)系:
當對一個對象進行kvo監(jiān)聽的時候围橡,會生成一個NSKVONotifying_
前綴的類暖混,然后我們實際的操作是對這個類進行的。某對象在addObserver:之后翁授,這個對象的isa指針已經(jīng)指向了NSKVONotifying_
前綴的類拣播,其父類被設(shè)置為Person類。
//// 驗證1
// 如果在原有工程中收擦,創(chuàng)建NSKVONotifying_Person類贮配,運行代碼會報 KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class 錯誤,
//因為原有工程中已經(jīng)存在該類塞赂,故無法運行時生成該類泪勒。
//// 驗證2
// 我們可以在addObserver:前后斷點打印對象的isa指針,會發(fā)現(xiàn)兩個實例對應的打印結(jié)果不同宴猾。
NSLog(@"KVO之前 - %@", object_getClass(self.person));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"keyOfMy"];
NSLog(@"KVO之后 - %@", object_getClass(self.person));
//log
//2021-06-22 20:02:02.101462+0800 TestProj[81369:8704592] KVO之前 - Person
//2021-06-22 20:02:07.003729+0800 TestProj[81369:8704592] KVO之后 - NSKVONotifying_Person
//// 驗證3
//在 person 對象調(diào)用 addObserver: forKeyPath: options: context: 方法前后添加如下代碼圆存,打印結(jié)果不同 。
NSLog(@"KVO之前 - %p", [self.person methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"keyOfMy"];
NSLog(@"KVO之后 - %p", [self.person methodForSelector:@selector(setAge:)]);
//log
2021-06-22 20:12:15.245515+0800 TestProj[81577:8714132] KVO之前 - 0x10414be60
2021-06-22 20:12:23.842112+0800 TestProj[81577:8714132] KVO之后 - 0x7fff207bf79f
在addObserver:后調(diào)用setage方法沦辙,會根據(jù)對象的isa找到NSKVONotifying_Person,然后在類的方法列表中找到setage讹剔。
通過下面方法油讯,查看NSKVONotifying_Person 的內(nèi)部結(jié)構(gòu):
- (void)printMethodNamesOfClass:(Class)cls{
unsigned int count;
// 獲得方法數(shù)組
Method *methodList = class_copyMethodList(cls, &count);
// 存儲方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍歷所有的方法
for (int i = 0; i < count; i++) {
// 獲得方法
Method method = methodList[I];
// 獲得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 釋放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
// log,在kvo監(jiān)聽下包含了四個方法:
// NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
可見Apple是不希望暴露NSKVONotifyin_Person延欠,重寫了class方法陌兑,大概實現(xiàn)如下:
- (Class) class {
// 得到類對象,在找到類對象父類
return class_getSuperclass(object_getClass(self));
}
重寫setName: 的內(nèi)部實現(xiàn)衫冻,其中調(diào)用了"_NSSetObjectValueAndNotify()" :
- (void)setName:(NSString *)name {
_NSSetObjectValueAndNotify()
}
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
}
- (void)didChangeValueForKey:(NSString *)key {
[super didChangeValueForKey:key];
[observer observeValueForKeyPath:@"name"];
}
void _NSSetObjectValueAndNotify() {
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
手動觸發(fā)KVO
因為 KVO 的本質(zhì)是重寫了 set 方法诀紊, set 方法內(nèi)部調(diào)用了willChangeValueForKey 和 didChangeValueForKey 方法,直接修改成員變量并不會調(diào)用 set 方法隅俘。由此可知邻奠,KVO 的觸發(fā)條件一般是修改監(jiān)聽對象屬性值,但也可在不修改被監(jiān)聽屬性值的情況下觸發(fā) KVO 監(jiān)聽回調(diào)为居。
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
注:直接修改成員變量不會觸發(fā) KVO 監(jiān)聽方法碌宴,
KVC觸發(fā)KVO
kvc常見的API有:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
setValue:forKey:的原理,
方法accessInstanceVariablesDirectly:(是否允許訪問成員變量)蒙畴,默認返回YES贰镣。該方法有個應用場景就是如果你自己寫框架呜象,你的一些私有的變量不想被外部通過KVC的方式去修改,就可以重寫這個方法碑隆,返回 NO 即可恭陡!
valueForKey:的原理,
KVC修改屬性值上煤,是會觸發(fā)KVO的休玩,原因是系統(tǒng)自動實現(xiàn)了set方法,并且底層都會調(diào)用 willChangeValueForKey和 didChangeValueForKey劫狠。