KVO
,全稱為Key-Value observing
钓葫,中文名為鍵值觀察
悄蕾,KVO是一種機制,它允許將其他對象的指定屬性的更改通知給對象
番刊。
在Key-Value Observing Programming Guide官方文檔中芹务,又這么一句話:理解KVO之前,必須先理解KVC
(即KVO是基于KVC基礎之上)
In order to understand key-value observing, you must first understand key-value coding.
KVC是鍵值編碼
沃但,在對象創(chuàng)建完成后,可以動態(tài)的給對象屬性賦值
淤刃,而KVO是鍵值觀察
,提供了一種監(jiān)聽機制铝侵,當指定的對象的屬性被修改后,則對象會收到通知疟丙,所以可以看出KVO是基于KVC的基礎上對屬性動態(tài)變化的監(jiān)聽
在iOS日常開發(fā)中览祖,經(jīng)常使用KVO來監(jiān)聽對象屬性的變化
,并及時做出響應玄货,即當指定的被觀察的對象的屬性被修改后夹界,KVO會自動通知相應的觀察者
鸠踪,那么KVO
與NSNotificatioCenter
有什么區(qū)別呢?
- 相同點
1评汰、兩者的實現(xiàn)原理
都是觀察者模式
,都是用于監(jiān)聽
2、都能
實現(xiàn)一對多
的操作
- 不同點
1坯墨、
KVO只能用于監(jiān)聽對象屬性的變化
,并且屬性名都是通過NSString來查找液斜,編譯器不會幫你檢測對錯和補全,純手敲會比較容易出錯2臼膏、
NSNotification
的發(fā)送監(jiān)聽
(post)的操作我們可以控制,kvo
由系統(tǒng)
控制始鱼。3、
KVO
可以記錄新舊值變化
KVO 使用注意事項
1会烙、基本使用
KVO的基本使用主要分為3步:
- 注冊觀察者
addObserver:forKeyPath:options:context
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
- 實現(xiàn)KVO回調(diào)
observeValueForKeyPath:ofObject:change:context
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"%@",change);
}
}
- 移除觀察者
removeObserver:forKeyPath:context
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
2系吭、context使用
在官方文檔中沃缘,針對參數(shù)context
有如下說明:
大致含義就是:addObserver:forKeyPath:options:context:
方法中的上下文context
指針包含任意數(shù)據(jù),這些數(shù)據(jù)將在相應的更改通知中傳遞回觀察者〖パ玻可以通過指定context為NULL
,從而依靠keyPath
即鍵路徑字符串
傳來確定更改通知的來源抬驴,但是這種方法可能會導致對象的父類由于不同的原因也觀察到相同的鍵路徑而導致問題豌拙。所以可以為每個觀察到的keyPath創(chuàng)建一個不同的context,從而完全不需要進行字符串比較
唯绍,從而可以更有效地進行通知解析
通俗的講,context上下文主要是用于區(qū)分不同對象的同名屬性
牛柒,從而在KVO回調(diào)方法中可以直接使用context進行區(qū)分椭更,可以大大提升性能湿滓,以及代碼的可讀性
context使用總結
- 不使用context,使用keyPath區(qū)分通知來源
//context的類型是 nullable void *朝氓,應該是NULL,而不是nil
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
- 使用context區(qū)分通知來源
//定義context
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
//注冊觀察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
//KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNickContext) {
NSLog(@"%@",change);
}else if (context == PersonNameContext){
NSLog(@"%@",change);
}
}
3、移除KVO通知的必要性
在官方文檔中橡庞,針對KVO的移除
有以下幾點說明
刪除觀察者時,請記住以下幾點:
要求被移除為觀察者(如果尚未注冊為觀察者)會導致
NSRangeException
确封。您可以對removeObserver:forKeyPath:context:
進行一次調(diào)用,以對應對addObserver:forKeyPath:options:context:
的調(diào)用秉剑,或者,如果在您的應用中不可行略水,則將removeObserver:forKeyPath:context:
調(diào)用在try / catch塊
內(nèi)處理潛在的異常。釋放后跨释,觀察者不會自動將其自身移除
。被觀察對象繼續(xù)發(fā)送通知,而忽略了觀察者的狀態(tài)龄恋。但是,與發(fā)送到已釋放對象的任何其他消息一樣显押,更改通知會觸發(fā)內(nèi)存訪問異常挖息。因此,您可以確保觀察者在從內(nèi)存中消失之前將自己刪除
电禀。該協(xié)議無法詢問對象是觀察者還是被觀察者。構造代碼以避免發(fā)布相關的錯誤政基。一種典型的模式是在觀察者初始化期間(例如,
在init或viewDidLoad中)注冊為觀察者
珊擂,并在釋放過程中(通常在dealloc中)注銷
圣贸,以確保成對和有序地添加和刪除消息
滑负,并確保觀察者在注冊之前被取消注冊,從內(nèi)存中釋放出來
痴鳄。
所以螺句,總的來說,KVO注冊觀察者 和移除觀察者是需要成對出現(xiàn)的
取劫,如果只注冊勇凭,不移除,會出現(xiàn)類似野指針的崩潰
,如下圖所示
崩潰的原因是蘸吓,由于第一次注冊KVO觀察者后沒有移除
,再次進入界面宪萄,會導致第二次注冊KVO觀察者,導致KVO觀察的重復注冊
居凶,而且第一次的通知對象還在內(nèi)存中侠碧,沒有進行釋放,此時接收到屬性值變化的通知谷暮,會出現(xiàn)找不到原有的通知對象,只能找到現(xiàn)有的通知對象
颊埃,即第二次KVO注冊的觀察者班利,所以導致了類似野指針的崩潰
,即一直保持著一個野通知闯割,且一直在監(jiān)聽
注:這里的崩潰案例是通過單例對象
實現(xiàn)(崩潰有很大的幾率,不是每次必現(xiàn)),因為單例對象在內(nèi)存是常駐的御板,針對一般的類對象缎谷,貌似不移除也是可以的瑞你,但是為了防止線上意外,建議還是移除比較好
4鲫懒、KVO的自動觸發(fā)與手動觸發(fā)
KVO觀察的開啟和關閉有兩種方式甲献,自動
和手動
- 自動開關,返回
NO
球及,就監(jiān)聽不到,返回YES
际歼,表示監(jiān)聽
// 自動開關
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return YES;
}
- 自動開關關閉的時候,可以通過
手動開關監(jiān)聽
- (void)setName:(NSString *)name{
//手動開關
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
使用手動開關的好處
就是你想監(jiān)聽就監(jiān)聽旭愧,不想監(jiān)聽關閉即可,比自動觸發(fā)更方便靈活
5桃熄、KVO觀察:一對多
KVO觀察中的一對多
,意思是通過注冊一個KVO觀察者,可以監(jiān)聽多個屬性的變化
以下載進度為例界弧,比如目前有一個需求划栓,需要根據(jù)總的下載量totalData
和當前下載量currentData
來計算當前的下載進度currentProcess
,實現(xiàn)有兩種方式
分別觀察
總的下載量totalData
和當前下載量currentData
兩個屬性,當其中一個發(fā)生變化計算 當前下載進度currentProcess
實現(xiàn)
keyPathsForValuesAffectingValueForKey
方法素标,將兩個觀察合為一個觀察,即觀察當前下載進度currentProcess
//1、合二為一的觀察方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"currentProcess"]) {
NSArray *affectingKeys = @[@"totalData", @"currentData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
//2、注冊KVO觀察
[self.person addObserver:self forKeyPath:@"currentProcess" options:(NSKeyValueObservingOptionNew) context:NULL];
//3欠母、觸發(fā)屬性值變化
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.currentData += 10;
self.person.totalData += 1;
}
//4、移除觀察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"currentProcess"];
}
6、KVO觀察 可變數(shù)組
KVO是基于KVC基礎之上的掷贾,所以可變數(shù)組如果直接添加數(shù)據(jù),是不會調(diào)用setter方法的添寺,所有對可變數(shù)組
的KVO觀察下面這種方式不生效
的,即直接通過[self.person.dateArray addObject:@"1"];
向數(shù)組添加元素胯盯,是不會觸發(fā)kvo通知回調(diào)的
//1、注冊可變數(shù)組KVO觀察者
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
//2计露、KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
//3、移除觀察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"dateArray"];
}
//4、觸發(fā)數(shù)組添加數(shù)據(jù)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person.dateArray addObject:@"1"];
}
在KVC官方文檔中票罐,針對可變數(shù)組的集合
類型蚕礼,有如下說明冀痕,即訪問集合對象需要需要通過mutableArrayValueForKey
方法满哪,這樣才能將元素添加到可變數(shù)組
中
修改
將4中的代碼修改如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// KVC 集合 array
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}
運行結果如下华望,可以看到,元素被添加到可變數(shù)組了
其中的kind
表示鍵值變化的類型
,是一個枚舉枷畏,主要有以下4種
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//設值
NSKeyValueChangeInsertion = 2,//插入
NSKeyValueChangeRemoval = 3,//移除
NSKeyValueChangeReplacement = 4,//替換
};
一般的屬性
與集合
的KVO觀察
是有區(qū)別的,其kind不同
嚣艇,以屬性name
和 可變數(shù)組
為例
-
屬性
的kind
一般是設值
-
可變數(shù)組
的kind
一般是插入
屬性與集合的kind區(qū)別
KVO 底層原理探索
官方文檔說明
在KVO的官方使用指南中考廉,有如下說明
KVO
是使用isa-swizzling
的技術實現(xiàn)的怎炊。顧名思義久橙,
isa指針指向維護分配表的對象的類
康嘉。該分派表實質(zhì)上包含指向該類實現(xiàn)的方法的指針以及其他數(shù)據(jù)纱控。當為對象的屬性
注冊觀察者時
情竹,將修改
觀察對象的isa指針
蒲赂,指向中間類
而不是真實類互广。結果氢妈,isa指針的值不一定反映實例的實際類宋下。您永遠不應依靠isa指針來確定類成員身份洒缀。相反,您應該
使用class方法來確定對象實例的類
。
代碼調(diào)試探索
1壹将、KVO只對屬性觀察
在LGPerson中有一個成員變量name
和 屬性nickName
,分別注冊KVO觀察,觸發(fā)屬性變化時胯甩,會有什么現(xiàn)象拓哟?
- 分別為
成員變量name
和屬性nickName
注冊KVO觀察
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];
- KVO通知觸發(fā)操作
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"實際情況:%@-%@",self.person.nickName,self.person->name);
self.person.nickName = @"KC";
self.person->name = @"Cooci";
}
運行結果如下
結論:KVO對成員變量不觀察
,只對屬性觀察
鳍侣,屬性和成員變量的區(qū)別在于屬性多一個 setter 方法
惑折,而KVO恰好觀察的是setter 方法
2、中間類
根據(jù)官方文檔所述,在注冊KVO觀察者后仑荐,觀察對象的isa指針指向會發(fā)生改變
- 注冊觀察者之前:實例對象
person
的isa
指針指向LGPerson
注冊觀察者之前
- 注冊觀察者之后:實例對象
person
的isa
指針指向NSKVONotifying_LGPerson
注冊觀察者之后
綜上所述,在注冊觀察者后二汛,實例對象的isa指針指向由LGPerson
類變?yōu)榱?code>NSKVONotifying_LGPerson中間類匪蝙,即實例對象的isa
指針指向發(fā)生了變化
2-1、判斷中間類是否是派生類 即子類习贫?
那么這個動態(tài)生成的中間類NSKVONotifying_LGPerson
和LGPerson
類 有什么關系?下面通過代碼來驗證
可以通過下面封裝的方法千元,獲取LGPerson的相關類
#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
// 注冊類的總數(shù)
int count = objc_getClassList(NULL, 0);
// 創(chuàng)建一個數(shù)組苫昌, 其中包含給定對象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 獲取所有已注冊的類
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
//********調(diào)用********
[self printClasses:[LGPerson class]];
打印結果如下所示
從結果中可以說明NSKVONotifying_LGPerson
是LGPerson的子類
2-2、中間類中有什么幸海?
可以通過下面的方法獲取NSKVONotifying_LGPerson
類中的所有方法
#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);
}
//********調(diào)用********
[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];
輸出結果如下
從結果中可以看出有四個方法祟身,分別是setNickName 、 class 物独、 dealloc 袜硫、 _isKVOA
,這些方法是繼承還是重寫
挡篓?
- 在
LGStudent
中重寫setNickName
方法婉陷,獲取LGStudent
類的所有方法
LGStudent方法打印
與中間類的方法進行的對比說明只有重寫
的方法帚称,才會在子類的方法列表中遍歷打印出來,而繼承
的不會在子類遍歷出來
- 獲取
LGPerson
和NSKVONotifying_LGPerson
的方法列表進行對比
對比
綜上所述秽澳,有如下結論:
-
NSKVONotifying_LGPerson
中間類重寫
了父類LGPerson
的setNickName
方法 -
NSKVONotifying_LGPerson
中間類重寫
了基類NSObject
的class 闯睹、 dealloc 、 _isKVOA
方法- 其中
dealloc
是釋放方法 -
_isKVOA
判斷當前是否是kvo類
- 其中
-
2-3担神、dealloc中移除觀察者后楼吃,isa指向是誰,以及中間類是否會銷毀妄讯?
- 移除觀察者之前:實例對象的isa指向仍是
NSKVONotifying_LGPerson
中間類
移除觀察者之前
- 移除觀察者之后:實例對象的isa指向更改為
LGPerson
類
移除觀察者之后
所以孩锡,在移除kvo觀察者后
,isa
的指向由NSKVONotifying_LGPerson
變成了LGPerson
那么中間類從創(chuàng)建后亥贸,到dealloc方法中移除觀察者之后躬窜,是否還存在?
- 在上一級界面打印
LGPerson
的子類情況砌函,用于判斷中間類是否銷毀
中間類未銷毀
通過子類的打印結果可以看出斩披,中間類一旦生成,沒有移除讹俊,沒有銷毀垦沉,還在內(nèi)存中 -- 主要是考慮重用
的想法,即中間類注冊到內(nèi)存中仍劈,為了考慮后續(xù)的重用問題厕倍,所以中間類一直存在
總結
綜上所述,關于中間類
贩疙,有如下說明:
實例對象
isa
的指向在注冊KVO觀察者之后
讹弯,由原有類
更改為指向中間類
中間類
重寫了觀察屬性的setter方法
、class
这溅、dealloc
组民、_isKVOA
方法dealloc方法中,移除KVO觀察者之后悲靴,實例對象
isa
指向由中間類
更改為原有類
中間類
從創(chuàng)建后臭胜,就一直存在內(nèi)存中,不會被銷毀
自定義KVO
自定KVO的流程癞尚,跟系統(tǒng)一致耸三,只是在系統(tǒng)的基礎上針對其中的部分做了一些優(yōu)化處理。
- 1浇揩、將
注冊和響應
通過函數(shù)式編程仪壮,即block
的方法結合在一起 - 2、去掉系統(tǒng)繁瑣的三部曲胳徽,實現(xiàn)
KVO自動銷毀機制
在系統(tǒng)中积锅,注冊觀察者和KVO響應屬于響應式編程
爽彤,是分開寫的,在自定義為了代碼更好的協(xié)調(diào)乏沸,使用block
的形式淫茵,將注冊和回調(diào)的邏輯組合在一起,即采用函數(shù)式編程
方式蹬跃,還是分為三部分
注冊觀察者
//*********定義block*********
typedef void(^LGKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
//*********注冊觀察者*********
- (void)cjl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block;
KVO響應
這部分主要是通過重寫setter
方法匙瘪,在中間類的setter方法中,通過block
的方式傳遞給外部進行響應移除觀察者
//*********移除觀察者*********
- (void)cjl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
準備條件:創(chuàng)建NSObject
類的分類CJLJVO
注冊觀察者
在注冊觀察者
方法中蝶缀,主要有以下幾部分操作:
- 1丹喻、判斷當前觀察值keyPath的setter方法是否存在
#pragma mark - 驗證是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath
{
Class superClass = object_getClass(self);
SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSelector);
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"CJLKVO - 沒有當前%@的setter方法", keyPath] userInfo:nil];
}
}
- 2、
動態(tài)生成子類
翁都,將需要重寫的class
方法添加到中間類中
#pragma mark - 動態(tài)生成子類
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
//獲取原本的類名
NSString *oldClassName = NSStringFromClass([self class]);
//拼接新的類名
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kCJLKVOPrefix,oldClassName];
//獲取新類
Class newClass = NSClassFromString(newClassName);
//如果子類存在碍论,則直接返回
if (newClass) return newClass;
//2.1 申請類
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
//2.2 注冊
objc_registerClassPair(newClass);
//2.3 添加方法
SEL classSel = @selector(class);
Method classMethod = class_getInstanceMethod([self class], classSel);
const char *classType = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSel, (IMP)cjl_class, classType);
return newClass;
}
//*********class方法*********
#pragma mark - 重寫class方法,為了與系統(tǒng)類對外保持一致
Class cjl_class(id self, SEL _cmd){
//在外界調(diào)用class返回CJLPerson類
return class_getSuperclass(object_getClass(self));//通過[self class]獲取會造成死循環(huán)
}
- 3柄慰、
isa指向
由原有類平挑,改為指向中間類
object_setClass(self, newClass);
- 4魔种、
保存信息
:這里用的數(shù)組,也可以使用map,需要創(chuàng)建信息的model
模型類
//*********KVO信息的模型類/*********
#pragma mark 信息model類
@interface CJLKVOInfo : NSObject
@property(nonatomic, weak) NSObject *observer;
@property(nonatomic, copy) NSString *keyPath;
@property(nonatomic, copy) LGKVOBlock handleBlock;
- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block;
@end
@implementation CJLKVOInfo
- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block{
if (self = [super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end
//*********保存信息*********
//- 保存多個信息
CJLKVOInfo *info = [[CJLKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
//使用數(shù)組存儲 -- 也可以使用map
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
if (!mArray) {//如果mArray不存在亡驰,則重新創(chuàng)建
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
完整的注冊觀察者代碼如下
#pragma mark - 注冊觀察者 - 函數(shù)式編程
- (void)cjl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block{
//1背率、驗證是否存在setter方法
[self judgeSetterMethodFromKeyPath:keyPath];
//保存信息
//- 保存多個信息
CJLKVOInfo *info = [[CJLKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
//使用數(shù)組存儲 -- 也可以使用map
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
if (!mArray) {//如果mArray不存在细层,則重新創(chuàng)建
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
//判斷automaticallyNotifiesObserversForKey方法返回的布爾值
BOOL isAutomatically = [self cjl_performSelectorWithMethodName:@"automaticallyNotifiesObserversForKey:" keyPath:keyPath];
if (!isAutomatically) return;
//2儒鹿、動態(tài)生成子類、
/*
2.1 申請類
2.2 注冊
2.3 添加方法
*/
Class newClass = [self createChildClassWithKeyPath:keyPath];
//3凳忙、isa指向
object_setClass(self, newClass);
//獲取sel
SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
//獲取setter實例方法
Method method = class_getInstanceMethod([self class], setterSel);
//方法簽名
const char *type = method_getTypeEncoding(method);
//添加一個setter方法
class_addMethod(newClass, setterSel, (IMP)cjl_setter, type);
}
注意點
- 關于
objc_msgSend
的檢查關閉:target -> Build Setting -> Enable Strict Checking of objc_msgSend Calls
設置為NO
設置
-
class
方法必須重寫
业踏,其目的是為了與系統(tǒng)一樣,對外的類保持一致涧卵,如下所示-
系統(tǒng)的KVO
勤家,在添加觀察者前后,實例對象person的類一直都是CJLPerson
系統(tǒng)KVO-注冊前后對比 -
如果
沒有重寫class
方法柳恐,自定的KVO在注冊前后的實例對象person的class就會看到是不一致的,返回的isa更改后的類却紧,即中間類
自定義KVO-未重寫class方法的注冊前后對比 重寫后class方法后的自定義KVO,在注冊觀察者前后其實例對象類的顯示胎撤,與系統(tǒng)的顯示是一致的
-
KVO響應
主要是給子類
動態(tài)添加setter
方法,其目的是為了在setter方法中向父類發(fā)送消息断凶,告知其屬性值的變化
- 5伤提、將setter方法重寫添加到子類中(主要是在注冊觀察者方法中添加)
//獲取sel
SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
//獲取setter實例方法
Method method = class_getInstanceMethod([self class], setterSel);
//方法簽名
const char *type = method_getTypeEncoding(method);
//添加一個setter方法
class_addMethod(newClass, setterSel, (IMP)cjl_setter, type);
- 6、通過將系統(tǒng)的
objc_msgSendSuper
強制類型轉換自定義的消息發(fā)送cjl_msgSendSuper
//往父類LGPerson發(fā)消息 - 通過objc_msgSendSuper
//通過系統(tǒng)強制類型轉換自定義objc_msgSendSuper
void (*cjl_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
//定義一個結構體
struct objc_super superStruct = {
.receiver = self, //消息接收者 為 當前的self
.super_class = class_getSuperclass(object_getClass(self)), //第一次快捷查找的類 為 父類
};
//調(diào)用自定義的發(fā)送消息函數(shù)
cjl_msgSendSuper(&superStruct, _cmd, newValue);
- 7认烁、告知vc去響應:獲取信息肿男,通過block傳遞
/*---函數(shù)式編程*/
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
for (CJLKVOInfo *info in mArray) {
NSMutableDictionary<NSKeyValueChangeKey, id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
完整的setter方法代碼如下
static void cjl_setter(id self, SEL _cmd, id newValue){
NSLog(@"來了:%@",newValue);
//此時應該有willChange的代碼
//往父類LGPerson發(fā)消息 - 通過objc_msgSendSuper
//通過系統(tǒng)強制類型轉換自定義objc_msgSendSuper
void (*cjl_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
//定義一個結構體
struct objc_super superStruct = {
.receiver = self, //消息接收者 為 當前的self
.super_class = class_getSuperclass(object_getClass(self)), //第一次快捷查找的類 為 父類
};
//調(diào)用自定義的發(fā)送消息函數(shù)
cjl_msgSendSuper(&superStruct, _cmd, newValue);
//此時應該有didChange的代碼
//讓vc去響應
/*---函數(shù)式編程*/
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
for (CJLKVOInfo *info in mArray) {
NSMutableDictionary<NSKeyValueChangeKey, id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
}
移除觀察者
為了避免在外界不斷的調(diào)用removeObserver
方法介汹,在自定義KVO中實現(xiàn)自動移除觀察者
- 8、實現(xiàn)
cjl_removeObserver:forKeyPath:
方法舶沛,主要是清空數(shù)組嘹承,以及isa指向更改
- (void)cjl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
//清空數(shù)組
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
if (mArray.count <= 0) {
return;
}
for (CJLKVOInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath]) {
[mArray removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
if (mArray.count <= 0) {
//isa指回父類
Class superClass = [self class];
object_setClass(self, superClass);
}
}
- 9、在子類中重寫dealloc方法如庭,當子類銷毀時叹卷,會自動調(diào)用
dealloc
方法(在動態(tài)生成子類的方法中添加)
#pragma mark - 動態(tài)生成子類
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
//...
//添加dealloc 方法
SEL deallocSel = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
const char *deallocType = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSel, (IMP)cjl_dealloc, deallocType);
return newClass;
}
//************重寫dealloc方法*************
void cjl_dealloc(id self, SEL _cmd){
NSLog(@"來了");
Class superClass = [self class];
object_setClass(self, superClass);
}
其原理主要是:CJLPerson
發(fā)送消息釋放即dealloc
了,就會自動走到重寫的cjl_dealloc
方法中(原因是因為person
對象的isa
指向變了坪它,指向中間類
骤竹,但是實例對象的地址是不變
的,所以子類的釋放往毡,相當于釋放了外界的person
蒙揣,而重寫的cjl_dealloc
相當于是重寫了CJLPerson的dealloc
方法,所以會走到cjl_dealloc
方法中)开瞭,達到自動移除觀察者
的目的
總結
綜上所述懒震,自定義KVO大致分為以下幾步
- 注冊觀察者 & 響應
1、驗證是否存在setter方法
2嗤详、保存信息
3个扰、動態(tài)生成子類,需要重寫
class
断楷、setter
方法4锨匆、在子類的setter方法中向父類發(fā)消息,即自定義消息發(fā)送
5冬筒、讓觀察者響應
- 移除觀察者
1恐锣、更改
isa指向
為原有類2、重寫子類的
dealloc
方法
拓展
以上自定義的邏輯并不完善舞痰,只是闡述了KVO底層原來實現(xiàn)的大致邏輯土榴,具體的可以參考facebook
的KVO三方框架KVOController
自定義KVO的完整代碼見Github-CustomKVC_KVO,喜歡的可以點個?