1. KVC
1.0 KVC的使用
- LGStudent.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int length;
@property (nonatomic, strong) NSMutableArray *penArr;
@end
NS_ASSUME_NONNULL_END
- LGPerson.h
#import <Foundation/Foundation.h>
#import "LGStudent.h"
NS_ASSUME_NONNULL_BEGIN
typedef struct {
float x, y, z;
} ThreeFloats;
@interface LGPerson : NSObject{
@public
NSString *myName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, strong) NSMutableArray *mArray;
@property (nonatomic, assign) int age;
@property (nonatomic) ThreeFloats threeFloats;
@property (nonatomic, strong) LGStudent *student;
@end
NS_ASSUME_NONNULL_END
我們?cè)谄綍r(shí)一般使用LGPerson *person = [[LGPerson alloc] init]; person.name = @"Cooci";
這樣的setter
方法來(lái)對(duì)對(duì)象的某一個(gè)屬性進(jìn)行賦值,但是,我們也可以采用[person setValue:@"KC" forKey:@"name"];
這樣的Key-Value Coding (KVC)
形式進(jìn)行賦值。
- 集合類型
person.array = @[@"1",@"2",@"3"];
// 修改數(shù)組
// 第一種:搞一個(gè)新的數(shù)組 - KVC 賦值就OK
NSArray *array = [person valueForKey:@"array"];
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);
// 第二種
NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
mArray[0] = @"200";
NSLog(@"%@",[person valueForKey:@"array"]);
- 結(jié)構(gòu)體
ThreeFloats floats = {1.,2.,3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *value1 = [person valueForKey:@"threeFloats"];
NSLog(@"%@",value1);
ThreeFloats th;
[value1 getValue:&th];
NSLog(@"%f-%f-%f",th.x,th.y,th.z);
- 通過(guò)keyPath訪問(wèn)
LGStudent *student = [LGStudent alloc];
student.subject = @"大師班";
person.student = student;
[person setValue:@"Swift" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
- 字典
NSDictionary* dict = @{
@"name":@"Cooci",
@"nick":@"KC",
@"subject":@"iOS",
@"age":@18,
@"length":@180
};
LGStudent *p = [[LGStudent alloc] init];
// 字典轉(zhuǎn)模型
[p setValuesForKeysWithDictionary:dict];
NSLog(@"%@",p);
// 鍵數(shù)組轉(zhuǎn)模型到字典
NSArray *array = @[@"name",@"age"];
NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
NSLog(@"%@",dic);
這些類型的使用在apple 官方文檔中都有詳細(xì)介紹。
1.2 KVC原理
如圖上可知锦爵,
KVC
的源碼在Foundation
庫(kù)內(nèi),而Foundation
庫(kù)是未開(kāi)源的,所以我們需要從apple 官方文檔去探索延曙。
-
KVC設(shè)值流程
官方文檔:
KVC - setter.png
根據(jù)文檔提示,第一步先要看是否實(shí)現(xiàn)了setKey
方法或者_setKey
方法亡哄。如果實(shí)現(xiàn)了setKey
或_setKey
枝缔,則看accessInstanceVariablesDirectly
方法中是否返回了YES
,默認(rèn)是返回YES
的蚊惯,如果你重寫(xiě)了此方法并返回NO
愿卸,則代碼會(huì)調(diào)用setValue:forUndefinedKey
并發(fā)生異常。當(dāng)accessInstanceVariablesDirectly
方法返回了YES
截型,則去找當(dāng)前對(duì)象是否有key
,_isKey
趴荸,key
,isKey
宦焦,如果有发钝,則設(shè)值完成,如果沒(méi)有波闹,則調(diào)用setValue:forUndefinedKey
并發(fā)生異常酝豪。
驗(yàn)證: - LGPerson.h
#import <Foundation/Foundation.h>
#import "LGStudent.h"
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
@end
NS_ASSUME_NONNULL_END
- LGPerson.m
#import "LGPerson.h"
@implementation LGPerson
#pragma mark - 關(guān)閉或開(kāi)啟成員變量賦值
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
#pragma MARK:賦值 - setKey. 的流程分析
- (void)setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
// 沒(méi)有調(diào)用
- (void)_setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
@end
- 調(diào)用
LGPerson *person = [[LGPerson alloc] init];
[person setValue:@"jeffery_zc" forKey:@"name"];
首先,按照官方文檔精堕,最先調(diào)用的是_Key
孵淘,為了驗(yàn)證這個(gè)猜想,我們打印一下輸出結(jié)果NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
發(fā)現(xiàn)只有person->_name
有值歹篓,其他為null
,于是瘫证,我們屏蔽LGPerson.h
中_name
,再次輸出打印結(jié)果NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
,發(fā)現(xiàn)person->_isName
有值了滋捶,其他依舊是null
痛悯。依次操作打印輸出結(jié)果,得出結(jié)論與官方文檔一致重窟,設(shè)值流程為_key,_isKey载萌,key,isKey
。
-
KVC取值流程
官方文檔:
KVC取值01.png
根據(jù)官方文檔扭仁,首先查詢是否有
getKey垮衷,key,isKey乖坠,_key
方法搀突,如果有,先判斷當(dāng)前實(shí)例對(duì)象是否為集合類型熊泵,緊接著仰迁,判斷accessInstanceVariablesDirectly
是否返回YES
,如果是顽分,則尋找是否有_key徐许,_isKey,key卒蘸,isKey
的成員變量雌隅,如果有,判斷是否為集合類型并做處理缸沃,如果沒(méi)有恰起,則調(diào)用setValue:forUndefinedKey
并發(fā)生異常。驗(yàn)證:
將
LGPerson.m
中的setter
驗(yàn)證方法屏蔽趾牧,添加如下代碼:
#pragma MARK: 取值 - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>,
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
- 驗(yàn)證
getter
方法取值的調(diào)用順序
NSLog(@"取值:%@",[person valueForKey:@"name"]);
第一次輸出打印為getName
检盼,于是,我們屏蔽getName
輸出打印結(jié)果為name
,依次屏蔽操作武氓,得出官方文檔第一步的結(jié)論梯皿,getter
取值的調(diào)用順序?yàn)?code>getKey,key县恕,isKey东羹,_key。
- 驗(yàn)證成員變量取值順序
屏蔽LGPerson.m
中setter
,getter
方法忠烛,在調(diào)用代碼中加入如下代碼:
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
第一次打印結(jié)果為_name
属提,然后屏蔽成員變量_name
和person->_name = @"_name";
,打印結(jié)果為_isName
美尸,依次操作冤议,打印結(jié)果分別為_name,_isName师坎,name恕酸,isName
,正好與官方文檔第四步一致。
2. KVO
2.1 KVO的使用
KVO(Key-value observing)是一種鍵值觀察機(jī)制胯陋,它允許將其他對(duì)象的指定屬性的更改通知給對(duì)象蕊温。我們通常使用KVO在控制器中觀察某一對(duì)象的屬性變化袱箱。
- 注冊(cè)通知:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
observer
:觀察者,一般寫(xiě)self
义矛;
keyPath
:觀察的鍵值发笔,一般是對(duì)象的成員變量;
options
:一種枚舉值凉翻,比如新值舊值這些了讨;
context
:上下文,用來(lái)保存一些信息制轰,context
為void *
類型前计,當(dāng)只有一個(gè)觀察者時(shí),一般直接傳null
垃杖,當(dāng)有多個(gè)觀察者時(shí)残炮,一般會(huì)定義多個(gè)void *
類型的值用來(lái)區(qū)分。例:static void *PersonNickContext = &PersonNickContext;
- KVO回調(diào)函數(shù)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
//處理事件
}
- 移除觀察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
根據(jù)官方文檔描述缩滨,移除觀察者是必須要進(jìn)行的,如果你沒(méi)有移除觀察者泉瞻,可能會(huì)報(bào)錯(cuò)
NSRangeException
脉漏。
- 手動(dòng)添加觀察
默認(rèn)情況下,自動(dòng)觀察都是打開(kāi)的袖牙,當(dāng)我們需要手動(dòng)進(jìn)行觀察某一個(gè)對(duì)象時(shí)侧巨,可以使用+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey
方法將自動(dòng)觀察關(guān)閉。
關(guān)閉自動(dòng)觀察.png
然后在觀察的屬性的賦值前添加willChangeValueForKey:
方法鞭达,賦值后添加didChangeValueForKey :
方法司忱,這樣,這一屬性就變?yōu)槭謩?dòng)觀察了畴蹭。
手動(dòng)觀察.png
官方文檔還介紹了更多了的使用方法坦仍,這里我就不過(guò)多介紹了。 - 路徑處理
假設(shè)我們需要檢測(cè)當(dāng)前的下載進(jìn)度downloadProgress
,而下載進(jìn)度 = 已下載 / 總下載叨襟,所以我們可以利用集合將totalData
和總下載
合并成一個(gè)downloadProgress
繁扎,這樣,我們就只需要觀察downloadProgress
就可以了糊闽。
// 下載進(jìn)度 -- writtenData/totalData
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (NSString *)downloadProgress{
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
- 數(shù)組觀察
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
當(dāng)我們給數(shù)組賦值時(shí)梳玫,不能直接進(jìn)行addObject
,而是需要valueForKey
做一步轉(zhuǎn)換才能觀察到數(shù)組變化右犹,代碼如下:
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
2.2 KVO原理探究
官方文檔:
根據(jù)文檔提示提澎,KVO是使用了
isa-swizzling
的方法。在為某個(gè)對(duì)象注冊(cè)觀察者時(shí)念链,將這個(gè)對(duì)象的isa
指向了一個(gè)中間類盼忌,而不是原生的類积糯,所以,我們不能依靠isa
確定類碴犬,而是應(yīng)該通過(guò)class
去確定絮宁。
- LGPerson.h
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
NS_ASSUME_NONNULL_END
- LGPerson.m
#import "LGPerson.h"
@implementation LGPerson
- (void)setNickName:(NSString *)nickName{
_nickName = nickName;
}
@end
我們分別對(duì)屬性nickName
和成員變量name
添加觀察者:
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
當(dāng)點(diǎn)擊屏幕時(shí)對(duì)nickName
和name
進(jìn)行賦值,然后進(jìn)行觀察:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"實(shí)際情況:%@-%@",self.person.nickName,self.person->name);
self.person.nickName = @"KC";
self.person->name = @"jeffery";
}
#pragma mark - KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
通過(guò)輸出發(fā)現(xiàn)服协,打印出來(lái)的
change
只有KC
绍昂,也就是說(shuō)只監(jiān)測(cè)到了nickName
,而nickName
與name
的區(qū)別則在于是否實(shí)現(xiàn)了setter
方法偿荷,也就是窘游,KVO實(shí)際上是對(duì)setter
方法的監(jiān)聽(tīng)。由官方文檔可知跳纳,在為對(duì)象注冊(cè)了觀察者時(shí)忍饰,改變了對(duì)象的
isa
指針,并將對(duì)象的isa
指針指向了一個(gè)中間類,這時(shí)寺庄,我們不能通過(guò)isa
去獲取類了艾蓝,而是應(yīng)該通過(guò)class
,此時(shí)斗塘,我們?cè)谧?cè)觀察者后設(shè)置了一個(gè)斷點(diǎn)赢织,打印出當(dāng)前的類:通過(guò)打印可以看到,在添加了觀察者之后馍盟,生成了
LGPerson
的子類NSKVONotifying_LGPerson
于置,正如文檔所說(shuō),self.person
改變了isa
指針指向,接下來(lái)研究這個(gè)中間類實(shí)現(xiàn)了哪些方法:
#pragma mark - 遍歷方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
在添加觀察者后添加方法[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];
將這個(gè)中間類的實(shí)現(xiàn)方法打印出來(lái):
通過(guò)打印可以看到中間類實(shí)現(xiàn)了
setNickName,class乓梨,dealloc,_isKVOA
4個(gè)方法话速。既然setNickName
方法有打印,那就說(shuō)明中間類重寫(xiě)了此方法芯侥。也就是說(shuō)尿孔,我們將nickName
傳給了中間類,但是發(fā)生改變的卻是父類LGPerson
筹麸,所以可能在這里面的某個(gè)時(shí)間有傳值操作活合。因?yàn)樘O(píng)果建議通過(guò)class
來(lái)獲取類,所以這里中間類的重寫(xiě)是獲取原類的class
物赶,dealloc
是釋放方法白指,_isKVOA
則是判斷是否為KVO類。當(dāng)我們?cè)?code>dealloc中移除觀察者后酵紫,通過(guò)打印發(fā)現(xiàn)告嘲,·
isa
指針指向已經(jīng)由中間類NSKVONotifying_LGPerson
只回了LGPerson
错维,那么NSKVONotifying_LGPerson
類是否會(huì)從內(nèi)存中移除呢?總結(jié)
1.當(dāng)對(duì)象在注冊(cè)了觀察者之后,其
isa
指針會(huì)由原生類指向中間類僧界;2.KVO實(shí)際觀察的是
setter
方法侨嘀;3.移除觀察者之后,實(shí)例對(duì)象的
isa
指針會(huì)指回原生類捂襟;4.中間類在創(chuàng)建之后咬腕,會(huì)一直存在于內(nèi)存中。