最近學(xué)習(xí)Runtime释漆,順便總結(jié)一下在Objective-C中KVO使用到的Runtime機(jī)制。
系統(tǒng)的KVO使用
故事還得從OC的KVO說(shuō)起涎拉,一般的我們使用KVO類似的如下所示荡短,創(chuàng)建一個(gè)對(duì)象标沪,然后調(diào)用addObserver
方法進(jìn)行某個(gè)屬性的監(jiān)聽(tīng),有意思的是苞轿,我們?cè)趧?chuàng)建對(duì)象處和調(diào)用了addObserver
方法處打斷點(diǎn)茅诱,然后使用po命令打印對(duì)象的isa,發(fā)現(xiàn)了對(duì)象的isa指針在調(diào)用了addObserver
方法之后變了呕屎,明顯滴让簿,調(diào)用了addObserver
方法之后使用了runtime機(jī)制動(dòng)態(tài)的修改了對(duì)象的isa指針。
KVO中runtime的幾個(gè)概念
大家一定會(huì)很好奇秀睛,runtime是怎么實(shí)現(xiàn)了KVO尔当,那好下面就慢慢的揭開謎底。先了解幾個(gè)runtime的概念:
- 動(dòng)態(tài)創(chuàng)建Class
objc_allocateClassPair
可以動(dòng)態(tài)創(chuàng)建Class,objc_registerClassPair
進(jìn)行注冊(cè)動(dòng)態(tài)創(chuàng)建的Class - 修改對(duì)象的Class
object_setClass
可以修改對(duì)象的Class蹂安,也就是修改了isa指針指向的Class對(duì)象 - 動(dòng)態(tài)添加方法
class_addMethod
可以給類添加方法 - runtime方法調(diào)用
objc_msgSend
和objc_msgSendSuper
是OC消息發(fā)送機(jī)制的底層實(shí)現(xiàn)
KVO的實(shí)現(xiàn)步驟解析
好了椭迎,掌握了這幾個(gè)runtime的概念,以及一開始看到了對(duì)象isa指針的變化田盈,我們大概可以猜測(cè)KVO的實(shí)現(xiàn)了畜号,大概需要以下幾個(gè)步驟:
- 動(dòng)態(tài)了創(chuàng)建一個(gè)類(objc_allocateClassPair/objc_registerClassPair),該類是我們需要監(jiān)聽(tīng)的對(duì)象的類的子類允瞧,然后修改當(dāng)前對(duì)象的Class(object_setClass)简软。
- 給類動(dòng)態(tài)的添加方法(object_setClass),比如KVO監(jiān)聽(tīng)name屬相述暂,一般的需要添加一個(gè)setName:方法痹升,然后添加方法的實(shí)現(xiàn),在方法實(shí)現(xiàn)中畦韭,調(diào)用父類也就是原始類的setName:方法進(jìn)行屬性的設(shè)置(objc_msgSend疼蛾,亦可以使用objc_msgSendSuper)。
- 給observer的回調(diào)方法發(fā)送監(jiān)聽(tīng)消息(objc_msgSend),最后需要重置當(dāng)前對(duì)象的Class(object_setClass)艺配,確保再次監(jiān)聽(tīng)還會(huì)執(zhí)行同樣的流程.
自定義的KVO實(shí)現(xiàn)
系統(tǒng)的KVO addObserver
方法是NSObject對(duì)象的一個(gè)類別中NSObject(NSKeyValueObserverRegistration)
定義的方法察郁,所以我們也創(chuàng)建一個(gè)NSObject的類別NSObject (YTT_KVO)
,添加一個(gè)類似的監(jiān)聽(tīng)方法转唉。
創(chuàng)建KVO使用的類別
@interface NSObject (YTT_KVO)
- (void)ytt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
類別方法的實(shí)現(xiàn)
類別的監(jiān)聽(tīng)方法皮钠,做的事情主要是保存方法參數(shù),動(dòng)態(tài)創(chuàng)建之類以及設(shè)置對(duì)象的Class到子類赠法,動(dòng)態(tài)的添加子類的方法鳞芙。
- (void)ytt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
// 保存keypath
objc_setAssociatedObject(self, "keyPath", keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 獲取當(dāng)前類
Class selfClass = self.class;
// 動(dòng)態(tài)創(chuàng)建KVO類
const char * className = NSStringFromClass(selfClass).UTF8String;
char kvoClassName[1000];
sprintf(kvoClassName, "%s%s", "YTT_KVO_", className);
Class kvoClass = objc_allocateClassPair(selfClass, kvoClassName, 0);
if (!kvoClass) {
// Nil if the class could not be created (for example, the desired name is already in use).
kvoClass = NSClassFromString([NSString stringWithUTF8String:kvoClassName]);
}
objc_registerClassPair(kvoClass);
// 修改當(dāng)前類指向?yàn)閯?dòng)態(tài)創(chuàng)建的KVO子類
object_setClass(self, kvoClass);
// 動(dòng)態(tài)添加一個(gè)方法:setXxx()
SEL sel = NSSelectorFromString([NSString stringWithFormat:@"set%@:", keyPath.capitalizedString]);
class_addMethod(kvoClass, sel, (IMP)setValue, NULL);
}
動(dòng)態(tài)添加方法的實(shí)現(xiàn)
動(dòng)態(tài)添加方法做的事情主要是創(chuàng)建和設(shè)置回調(diào)參數(shù),包含新值和舊值,調(diào)父類的設(shè)置方法原朝,給原始類設(shè)置值驯嘱,給observer回調(diào)方法發(fā)送消息通知,告訴值改變了喳坠。
void setValue(id self, SEL _cmd, id value) {
// 保存當(dāng)前的Class鞠评,重置Class使用
Class selfClass = [self class];
// 設(shè)置Class為原始Class
object_setClass(self, [self superclass]);
// 獲取keyPath
NSString* keyPath = objc_getAssociatedObject(self, "keyPath");
// KVO 回調(diào)參數(shù)
NSMutableDictionary* change = [NSMutableDictionary dictionary];
change[NSKeyValueChangeNewKey] = value;
// 獲取舊的值
SEL getSel = NSSelectorFromString([NSString stringWithFormat:@"%@", keyPath]);
if ([self respondsToSelector:getSel]) {
id ret = ((id(*)(id, SEL, id))objc_msgSend)(self, getSel, value);
if (ret) {
change[NSKeyValueChangeOldKey] = ret;
}
}
// 給原始類設(shè)置數(shù)據(jù)
SEL setSel = NSSelectorFromString([NSString stringWithFormat:@"set%@:", keyPath.capitalizedString]);
if ([self respondsToSelector:setSel]) {
((void(*)(id, SEL, id))objc_msgSend)(self, setSel, value);
}
// 發(fā)送通知
id observer = objc_getAssociatedObject(self, "observer");
SEL observerSel = @selector(ytt_observeValueForKeyPath:ofObject:change:context:);
if ([observer respondsToSelector:observerSel]) {
((void(*) (id, SEL, NSString*, id, id ,id))(void *)objc_msgSend)(observer, observerSel, keyPath, self, change, nil);
}
// 重置class指針,這樣再次調(diào)用對(duì)象方法會(huì)走到這里面
object_setClass(self, selfClass);
}
KVO通知的接收處理
在監(jiān)聽(tīng)的對(duì)象中定義我們約定好的方法壕鹉,類似Objective-C中的KVO進(jìn)行處理
- (void)ytt_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
id new = change[NSKeyValueChangeNewKey];
id old = change[NSKeyValueChangeOldKey];
NSLog(@"%@-%@", old, new);
}
注意點(diǎn)
- objc_msgSend 的使用注意
我使用的是xcode8剃幌,ENABLE_STRICT_OBJC_MSGSEND開關(guān)默認(rèn)是打開的,需要進(jìn)行方法的指針強(qiáng)轉(zhuǎn)為對(duì)應(yīng)的方法才能調(diào)用
((void(*) (id, SEL, NSString*, id, id ,id))(void *)objc_msgSend)(observer, observerSel, keyPath, self, change, nil);
// 這句代碼的意思是把objc_msgSend這個(gè)任意類型的指針【(void *)】轉(zhuǎn)換為一個(gè)返回值為void的函數(shù)指針【void(*)】晾浴,并且參數(shù)列表為【(id, SEL, NSString*, id, id ,id)】
// void (*) 聲明一個(gè)函數(shù)指針负乡,返回值是void類型
// (void *) 返回的是一個(gè)任意類型的指針
// 比如我們需要獲取person的name屬性,返回值是NSString*類型脊凰,那么objc_msgSend方法定義如下
id ret = ((NSString*(*)(id, SEL))objc_msgSend)(self.person, @selector(name));
也可以設(shè)置ENABLE_STRICT_OBJC_MSGSEND開關(guān)關(guān)閉抖棘,那么可以不額外的處理objc_msgSend強(qiáng)轉(zhuǎn)的問(wèn)題,如果是一個(gè)庫(kù)項(xiàng)目狸涌,那么不建議手動(dòng)關(guān)閉這個(gè)開關(guān)切省,這樣不管是ENABLE_STRICT_OBJC_MSGSEND開關(guān)打開還是關(guān)閉都不會(huì)影響。
- objc_allocateClassPair 為空處理
objc_allocateClassPair 調(diào)用可能返回Nil帕胆,這種情況在當(dāng)前的場(chǎng)景第二次調(diào)用同一個(gè)類對(duì)象的ytt_addObserver
方法可能發(fā)生朝捆,這種情況,使用NSClassFromString方法創(chuàng)建Class懒豹。
Class kvoClass = objc_allocateClassPair(selfClass, kvoClassName, 0);
if (!kvoClass) {
// Nil if the class could not be created (for example, the desired name is already in use).
kvoClass = NSClassFromString([NSString stringWithUTF8String:kvoClassName]);
}
objc_registerClassPair(kvoClass);
```最近學(xué)習(xí)Runtime芙盘,順便總結(jié)一下在Objective-C中KVO使用到的Runtime機(jī)制。
### 系統(tǒng)的KVO使用
故事還得從OC的KVO說(shuō)起脸秽,一般的我們使用KVO類似的如下所示何陆,創(chuàng)建一個(gè)對(duì)象,然后調(diào)用`addObserver`方法進(jìn)行某個(gè)屬性的監(jiān)聽(tīng)豹储,有意思的是,我們?cè)趧?chuàng)建對(duì)象處和調(diào)用了`addObserver`方法處打斷點(diǎn)淘这,然后使用po命令打印對(duì)象的isa剥扣,發(fā)現(xiàn)了對(duì)象的isa指針在調(diào)用了`addObserver`方法之后變了,明顯滴铝穷,調(diào)用了`addObserver`方法之后使用了runtime機(jī)制動(dòng)態(tài)的修改了對(duì)象的isa指針钠怯。
象添加KVO監(jiān)聽(tīng)之后isa的變換")
### KVO中runtime的幾個(gè)概念
大家一定會(huì)很好奇,runtime是怎么實(shí)現(xiàn)了KVO曙聂,那好下面就慢慢的揭開謎底晦炊。先了解幾個(gè)runtime的概念:
- 動(dòng)態(tài)創(chuàng)建Class
`objc_allocateClassPair`可以動(dòng)態(tài)創(chuàng)建Class,`objc_registerClassPair`進(jìn)行注冊(cè)動(dòng)態(tài)創(chuàng)建的Class
- 修改對(duì)象的Class
`object_setClass`可以修改對(duì)象的Class,也就是修改了isa指針指向的Class對(duì)象
- 動(dòng)態(tài)添加方法
`class_addMethod`可以給類添加方法
- runtime方法調(diào)用
`objc_msgSend`和`objc_msgSendSuper`是OC消息發(fā)送機(jī)制的底層實(shí)現(xiàn)
### KVO的實(shí)現(xiàn)步驟解析
好了,掌握了這幾個(gè)runtime的概念断国,以及一開始看到了對(duì)象isa指針的變化,我們大概可以猜測(cè)KVO的實(shí)現(xiàn)了,大概需要以下幾個(gè)步驟:
- 動(dòng)態(tài)了創(chuàng)建一個(gè)類(objc_allocateClassPair/objc_registerClassPair)色迂,該類是我們需要監(jiān)聽(tīng)的對(duì)象的類的子類蚀狰,然后修改當(dāng)前對(duì)象的Class(object_setClass)。
- 給類動(dòng)態(tài)的添加方法(object_setClass)薄疚,比如KVO監(jiān)聽(tīng)name屬相碧信,一般的需要添加一個(gè)setName:方法,然后添加方法的實(shí)現(xiàn)街夭,在方法實(shí)現(xiàn)中砰碴,調(diào)用父類也就是原始類的setName:方法進(jìn)行屬性的設(shè)置(objc_msgSend,亦可以使用objc_msgSendSuper)板丽。
- 給observer的回調(diào)方法發(fā)送監(jiān)聽(tīng)消息(objc_msgSend),最后需要重置當(dāng)前對(duì)象的Class(object_setClass)呈枉,確保再次監(jiān)聽(tīng)還會(huì)執(zhí)行同樣的流程.
### 自定義的KVO實(shí)現(xiàn)
系統(tǒng)的KVO `addObserver`方法是NSObject對(duì)象的一個(gè)類別中`NSObject(NSKeyValueObserverRegistration)`定義的方法,所以我們也創(chuàng)建一個(gè)NSObject的類別`NSObject (YTT_KVO)`檐什,添加一個(gè)類似的監(jiān)聽(tīng)方法碴卧。
#### 創(chuàng)建KVO使用的類別
@interface NSObject (YTT_KVO)
- (void)ytt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
#### 類別方法的實(shí)現(xiàn)
類別的監(jiān)聽(tīng)方法,做的事情主要是保存方法參數(shù)乃正,動(dòng)態(tài)創(chuàng)建之類以及設(shè)置對(duì)象的Class到子類住册,動(dòng)態(tài)的添加子類的方法。
-
(void)ytt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
// 保存keypath
objc_setAssociatedObject(self, "keyPath", keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);// 獲取當(dāng)前類
Class selfClass = self.class;// 動(dòng)態(tài)創(chuàng)建KVO類
const char * className = NSStringFromClass(selfClass).UTF8String;
char kvoClassName[1000];
sprintf(kvoClassName, "%s%s", "YTT_KVO_", className);
Class kvoClass = objc_allocateClassPair(selfClass, kvoClassName, 0);
if (!kvoClass) {
// Nil if the class could not be created (for example, the desired name is already in use).
kvoClass = NSClassFromString([NSString stringWithUTF8String:kvoClassName]);
}
objc_registerClassPair(kvoClass);// 修改當(dāng)前類指向?yàn)閯?dòng)態(tài)創(chuàng)建的KVO子類
object_setClass(self, kvoClass);// 動(dòng)態(tài)添加一個(gè)方法:setXxx()
SEL sel = NSSelectorFromString([NSString stringWithFormat:@"set%@:", keyPath.capitalizedString]);
class_addMethod(kvoClass, sel, (IMP)setValue, NULL);
}
#### 動(dòng)態(tài)添加方法的實(shí)現(xiàn)
動(dòng)態(tài)添加方法做的事情主要是創(chuàng)建和設(shè)置回調(diào)參數(shù)瓮具,包含新值和舊值荧飞,調(diào)父類的設(shè)置方法,給原始類設(shè)置值名党,給observer回調(diào)方法發(fā)送消息通知叹阔,告訴值改變了。
void setValue(id self, SEL _cmd, id value) {
// 保存當(dāng)前的Class传睹,重置Class使用
Class selfClass = [self class];
// 設(shè)置Class為原始Class
object_setClass(self, [self superclass]);
// 獲取keyPath
NSString* keyPath = objc_getAssociatedObject(self, "keyPath");
// KVO 回調(diào)參數(shù)
NSMutableDictionary* change = [NSMutableDictionary dictionary];
change[NSKeyValueChangeNewKey] = value;
// 獲取舊的值
SEL getSel = NSSelectorFromString([NSString stringWithFormat:@"%@", keyPath]);
if ([self respondsToSelector:getSel]) {
id ret = ((id(*)(id, SEL, id))objc_msgSend)(self, getSel, value);
if (ret) {
change[NSKeyValueChangeOldKey] = ret;
}
}
// 給原始類設(shè)置數(shù)據(jù)
SEL setSel = NSSelectorFromString([NSString stringWithFormat:@"set%@:", keyPath.capitalizedString]);
if ([self respondsToSelector:setSel]) {
((void(*)(id, SEL, id))objc_msgSend)(self, setSel, value);
}
// 發(fā)送通知
id observer = objc_getAssociatedObject(self, "observer");
SEL observerSel = @selector(ytt_observeValueForKeyPath:ofObject:change:context:);
if ([observer respondsToSelector:observerSel]) {
((void(*) (id, SEL, NSString*, id, id ,id))(void *)objc_msgSend)(observer, observerSel, keyPath, self, change, nil);
}
// 重置class指針耳幢,這樣再次調(diào)用對(duì)象方法會(huì)走到這里面
object_setClass(self, selfClass);
}
### KVO通知的接收處理
在監(jiān)聽(tīng)的對(duì)象中定義我們約定好的方法,類似Objective-C中的KVO進(jìn)行處理
-
(void)ytt_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
id new = change[NSKeyValueChangeNewKey];
id old = change[NSKeyValueChangeOldKey];NSLog(@"%@-%@", old, new);
}
---
#### 注意點(diǎn)
- objc_msgSend 的使用注意
我使用的是xcode8欧啤,ENABLE_STRICT_OBJC_MSGSEND開關(guān)默認(rèn)是打開的睛藻,需要進(jìn)行方法的指針強(qiáng)轉(zhuǎn)為對(duì)應(yīng)的方法才能調(diào)用
```objc
((void(*) (id, SEL, NSString*, id, id ,id))(void *)objc_msgSend)(observer, observerSel, keyPath, self, change, nil);
// 這句代碼的意思是把objc_msgSend這個(gè)任意類型的指針【(void *)】轉(zhuǎn)換為一個(gè)返回值為void的函數(shù)指針【void(*)】,并且參數(shù)列表為【(id, SEL, NSString*, id, id ,id)】
// void (*) 聲明一個(gè)函數(shù)指針邢隧,返回值是void類型
// (void *) 返回的是一個(gè)任意類型的指針
// 比如我們需要獲取person的name屬性店印,返回值是NSString*類型,那么objc_msgSend方法定義如下
id ret = ((NSString*(*)(id, SEL))objc_msgSend)(self.person, @selector(name));
也可以設(shè)置ENABLE_STRICT_OBJC_MSGSEND開關(guān)關(guān)閉倒慧,那么可以不額外的處理objc_msgSend強(qiáng)轉(zhuǎn)的問(wèn)題按摘,如果是一個(gè)庫(kù)項(xiàng)目包券,那么不建議手動(dòng)關(guān)閉這個(gè)開關(guān),這樣不管是ENABLE_STRICT_OBJC_MSGSEND開關(guān)打開還是關(guān)閉都不會(huì)影響炫贤。
- objc_allocateClassPair 為空處理
objc_allocateClassPair 調(diào)用可能返回Nil溅固,這種情況在當(dāng)前的場(chǎng)景第二次調(diào)用同一個(gè)類對(duì)象的ytt_addObserver
方法可能發(fā)生,這種情況照激,使用NSClassFromString方法創(chuàng)建Class发魄。
Class kvoClass = objc_allocateClassPair(selfClass, kvoClassName, 0);
if (!kvoClass) {
// Nil if the class could not be created (for example, the desired name is already in use).
kvoClass = NSClassFromString([NSString stringWithUTF8String:kvoClassName]);
}
objc_registerClassPair(kvoClass);