FBKVOController是一個(gè)簡(jiǎn)單易用的鍵值觀察框架,KVOController 對(duì)于 Cocoa 中 KVO 的封裝非常的簡(jiǎn)潔和優(yōu)秀,我們只需要調(diào)用一個(gè)方法就可以完成一個(gè)對(duì)象的鍵值觀測(cè),同時(shí)不需要處理移除觀察者等問(wèn)題,能夠降低我們出錯(cuò)的可能性。
本文參考了:如何優(yōu)雅地使用 KVO。
KVOController的簡(jiǎn)單使用
@interface ViewController ()
@property (nonatomic,assign) NSUInteger index;
@property (nonatomic,strong) dispatch_source_t timer;
@property (nonatomic,strong) FBKVOController *KVOController;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
self.index++;
});
dispatch_resume(_timer);
typeof(self) weakSelf = self;
self.KVOController = [FBKVOController controllerWithObserver:self];
[self.KVOController observe:self keyPath:@"index" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"%lu",(unsigned long)weakSelf.index);
}];
}
打印結(jié)果
2017-11-13 17:36:12.159778+0800 KVOController[14165:1523330] 1
2017-11-13 17:36:13.157070+0800 KVOController[14165:1523330] 2
2017-11-13 17:36:14.156000+0800 KVOController[14165:1523330] 3
2017-11-13 17:36:15.156487+0800 KVOController[14165:1523330] 4
2017-11-13 17:36:16.156946+0800 KVOController[14165:1523330] 5
2017-11-13 17:36:17.156922+0800 KVOController[14165:1523330] 6
FBKVOController和KVO對(duì)比:
KVO
1.KVO需要手動(dòng)移除觀察者,且移除觀察者的時(shí)機(jī)必須合適;
2.注冊(cè)觀察者的代碼和事件發(fā)生處的代碼上下文不同,傳遞上下文是通過(guò)void *指針;
3.需要重寫(xiě)-observeValueForKeyPath:ofObject:change:context:方法,比較麻煩;
4.在復(fù)雜的業(yè)務(wù)邏輯中,準(zhǔn)確判斷被觀察者相對(duì)比較麻煩,有多個(gè)被觀測(cè)的對(duì)象和屬性時(shí),需要在方法中寫(xiě)大量的 if 進(jìn)行判斷;FBKVOController
1.不需要手動(dòng)移除觀察者;
2.實(shí)現(xiàn)KVO與事件發(fā)生處的代碼上下文相同,不需要跨方法傳參數(shù);
3.使用block來(lái)替代方法能夠減少使用的復(fù)雜度,提升使用KVO的體驗(yàn);
4.每一個(gè)keyPath會(huì)對(duì)應(yīng)一個(gè)屬性,不需要在 block 中使用if判斷keyPath;FBKVOController實(shí)現(xiàn)了觀察者和被觀察者的角色反轉(zhuǎn),系統(tǒng)的
KVO是被觀察者添加觀察者,而FBKVOController實(shí)現(xiàn)了觀察者主動(dòng)去添加被觀察者,實(shí)現(xiàn)了角色上的反轉(zhuǎn),其實(shí)就是用的比較方便。
// FBKVOController
typeof(self) weakSelf = self;
self.KVOController = [FBKVOController controllerWithObserver:self];
[self.KVOController observe:self keyPath:@"index" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"%lu",(unsigned long)weakSelf.index);
}];
// KVO
[self addObserver:self
forKeyPath:@"index"
options:NSKeyValueObservingOptionNew
context:nil];
源碼解析
FBKVOController框架有兩個(gè)類(lèi),其實(shí)只是對(duì) Cocoa 中 KVO 的封裝,分別是KVOController和NSObject+FBKVOController。先來(lái)看一下NSObject+FBKVOController的實(shí)現(xiàn)。
NSObject+FBKVOController.h
@interface NSObject (FBKVOController)
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end
從名字可以看出KVOControllerNonRetaining在使用時(shí)并不會(huì)持有被觀察的對(duì)象,與它相比,KVOController 就會(huì)持有該對(duì)象了。
- (FBKVOController *)KVOController {
id controller = objc_getAssociatedObject(self, NSObjectKVOControllerKey);
if (nil == controller) {
controller = [FBKVOController controllerWithObserver:self];
self.KVOController = controller;
}
return controller;
}
- (void)setKVOController:(FBKVOController *)KVOController {
objc_setAssociatedObject(self, NSObjectKVOControllerKey, KVOController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (FBKVOController *)KVOControllerNonRetaining {
id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey);
if (nil == controller) {
controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO];
self.KVOControllerNonRetaining = controller;
}
return controller;
}
- (void)setKVOControllerNonRetaining:(FBKVOController *)KVOControllerNonRetaining {
objc_setAssociatedObject(self, NSObjectKVOControllerNonRetainingKey, KVOControllerNonRetaining, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
兩者的 setter方法都只是使用 objc_setAssociatedObject按照鍵值簡(jiǎn)單地存一下,而 getter中不同的其實(shí)也就是對(duì)于FBKVOController的初始化了。
KVOController 的初始化
KVOController 是整個(gè)框架中提供 KVO 接口的類(lèi),作為 KVO 的管理者,其必須持有當(dāng)前對(duì)象所有與 KVO 有關(guān)的信息,而在 KVOController 中,用于存儲(chǔ)這個(gè)信息的數(shù)據(jù)結(jié)構(gòu)就是 NSMapTable。為了使 KVOController 達(dá)到線程安全,它還必須持有一把 pthread_mutex_t 鎖,用于在操作 _objectInfosMap 時(shí)使用。
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved {
self = [super init];
if (nil != self) {
_observer = observer;
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
pthread_mutex_init(&_lock, NULL);
}
return self;
}
在初始化方法中,根據(jù)retainObserved決定是強(qiáng)引用還是弱引用被觀察者,KVOController 和 KVOControllerNonRetaining 的區(qū)別就體現(xiàn)在生成的 NSMapTable 實(shí)例時(shí)傳入的是 NSPointerFunctionsStrongMemory 還是 NSPointerFunctionsWeakMemory 選項(xiàng)。最后初始化pthread_mutex_t 鎖。
KVO 的過(guò)程
使用KVOController實(shí)現(xiàn)鍵值觀測(cè)時(shí)會(huì)調(diào)用實(shí)例方法-observe:keyPath:options:block來(lái)注冊(cè)成為某個(gè)對(duì)象的觀察者,監(jiān)控屬性的變化:
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block {
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
[self _observe:object info:info];
}
數(shù)據(jù)結(jié)構(gòu) _FBKVOInfo
上面的方法中就涉及到另外一個(gè)私有的數(shù)據(jù)結(jié)構(gòu)_FBKVOInfo,這個(gè)類(lèi)中包含著所有與 KVO 有關(guān)的信息:
{
@public
__weak FBKVOController *_controller;
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
FBKVONotificationBlock _block;
_FBKVOInfoState _state;
}
_FBKVOInfo在KVOController中充當(dāng)?shù)淖饔脙H僅是一個(gè)數(shù)據(jù)結(jié)構(gòu),我們主要用它來(lái)存儲(chǔ)整個(gè) KVO 過(guò)程中所需要的全部信息,_FBKVOInfo重寫(xiě)了-isEqual:方法用于對(duì)象之間的判等以及方便NSMapTable的存儲(chǔ)。其中的state表示當(dāng)前的 KVO 狀態(tài)。
typedef NS_ENUM(uint8_t, _FBKVOInfoState) {
_FBKVOInfoStateInitial = 0,
_FBKVOInfoStateObserving,
_FBKVOInfoStateNotObserving,
};
observe 的過(guò)程
-observer:keyPath:options:block:初始化了一個(gè)名為_FBKVOInfo的對(duì)象:
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block {
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
[self _observe:object info:info];
}
在創(chuàng)建了_FBKVOInfo之后執(zhí)行了另一個(gè)私有方法-_observe:info::
- (void)_observe:(id)object info:(_FBKVOInfo *)info {
pthread_mutex_lock(&_lock);
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
pthread_mutex_unlock(&_lock);
return;
}
if (nil == infos) {
infos = [NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];
}
[infos addObject:info];
pthread_mutex_unlock(&_lock);
[[_FBKVOSharedController sharedController] observe:object info:info];
}
這個(gè)私有方法通過(guò)自身持有_objectInfosMap
來(lái)判斷當(dāng)前對(duì)象、屬性以及各種上下文是否已經(jīng)注冊(cè)在表中存在了,在這個(gè)
_objectInfosMap中保存著對(duì)象以及與對(duì)象有關(guān)的_FBKVOInfo集合:
在操作了當(dāng)前KVOController持有的_objectInfosMap之后,才會(huì)執(zhí)行私有的_FBKVOSharedController類(lèi)的實(shí)例方法-observe:info::
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info {
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
if (info->_state == _FBKVOInfoStateInitial) {
info->_state = _FBKVOInfoStateObserving;
} else if (info->_state == _FBKVOInfoStateNotObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
_FBKVOSharedController才是最終調(diào)用 Cocoa 中的-observe:forKeyPath:options:context:方法開(kāi)始對(duì)屬性的監(jiān)聽(tīng)的地方;同時(shí),在整個(gè)應(yīng)用運(yùn)行時(shí),只會(huì)存在一個(gè)_FBKVOSharedController實(shí)例:
+ (instancetype)sharedController {
static _FBKVOSharedController *_controller = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_controller = [[_FBKVOSharedController alloc] init];
});
return _controller;
}
這個(gè)_FBKVOSharedController單例會(huì)在 KVO 的回調(diào)方法中將事件分發(fā)給 KVO 的觀察者。
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, id> *)change
context:(nullable void *)context {
_FBKVOInfo *info;
pthread_mutex_lock(&_mutex);
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
FBKVOController *controller = info->_controller;
id observer = controller.observer;
if (info->_block) {
NSDictionary<NSString *, id> *changeWithKeyPath = change;
if (keyPath) {
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);
} else if (info->_action) {
[observer performSelector:info->_action withObject:change withObject:object];
} else {
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
在這個(gè)-observeValueForKeyPath:ofObject:change:context:回調(diào)方法中,_FBKVOSharedController會(huì)根據(jù) KVO 的信息_KVOInfo選擇不同的方式分發(fā)事件,如果觀察者沒(méi)有傳入 block 或者選擇子,就會(huì)調(diào)用觀察者 KVO 回調(diào)方法。
如何 removeObserver
在使用 KVOController 時(shí),我們并不需要手動(dòng)去處理 KVO 觀察者的移除,因?yàn)樗械?KVO 事件都由私有的_KVOSharedController來(lái)處理;
當(dāng)每一個(gè)KVOController對(duì)象被釋放時(shí),都會(huì)將它自己持有的所有 KVO 的觀察者交由_KVOSharedController的-unobserve:infos:
- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos {
pthread_mutex_lock(&_mutex);
for (_FBKVOInfo *info in infos) {
[_infos removeObject:info];
}
pthread_mutex_unlock(&_mutex);
for (_FBKVOInfo *info in infos) {
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}
}
該方法會(huì)遍歷所有傳入的_FBKVOInfo,從其中取出keyPath并將_KVOSharedController移除觀察者。
除了在KVOController析構(gòu)時(shí)會(huì)自動(dòng)移除觀察者,我們也可以通過(guò)它的實(shí)例方法-unobserve:keyPath:操作達(dá)到相同的效果;功能的實(shí)現(xiàn)過(guò)程其實(shí)都是類(lèi)似的,都是通過(guò)-removeObserver:forKeyPath:context:方法移除觀察者:
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info {
pthread_mutex_lock(&_mutex);
[_infos removeObject:info];
pthread_mutex_unlock(&_mutex);
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}
不過(guò)由于這個(gè)方法的參數(shù)并不是一個(gè)數(shù)組,所以并不需要使用for循環(huán),而是只需要將該_FBKVOInfo對(duì)應(yīng)的 KVO 事件移除就可以了。
KVO
KVO的使用非常簡(jiǎn)單,使用KVO的要求是對(duì)象必須能支持kvc機(jī)制——所有NSObject的子類(lèi)都支持這個(gè)機(jī)制。拿上面的漸變導(dǎo)航欄做,我們?yōu)閠ableView添加了一個(gè)監(jiān)聽(tīng)者controller,在我們滑動(dòng)列表的時(shí)候,會(huì)計(jì)算當(dāng)前列表的滾動(dòng)偏移量,然后改變導(dǎo)航欄的背景色透明度。
//添加監(jiān)聽(tīng)者
[self.tableView addObserver: self forKeyPath: @"contentOffset" options: NSKeyValueObservingOptionNew context: nil];
/**
* 監(jiān)聽(tīng)屬性值發(fā)生改變時(shí)回調(diào)
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
CGFloat offset = self.tableView.contentOffset.y;
CGFloat delta = offset / 64.f + 1.f;
delta = MAX(0, delta);
[self alphaNavController].barAlpha = MIN(1, delta);
}
毫無(wú)疑問(wèn),kvo是一種非常便捷的回調(diào)方式,但是編譯器是怎么完成監(jiān)聽(tīng)這個(gè)任務(wù)的呢?先來(lái)看看蘋(píng)果文檔對(duì)于KVO的實(shí)現(xiàn)描述
Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ..
簡(jiǎn)要的來(lái)說(shuō),在我們對(duì)某個(gè)對(duì)象完成監(jiān)聽(tīng)的注冊(cè)后,編譯器會(huì)修改監(jiān)聽(tīng)對(duì)象(上文中的tableView)的isa指針,讓這個(gè)指針指向一個(gè)新生成的中間類(lèi)。從某個(gè)意義上來(lái)說(shuō),這是一場(chǎng)騙局。
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
這里要說(shuō)明的是isa這個(gè)指針,isa是一個(gè)Class類(lèi)型的指針,對(duì)象的首地址一般是isa變量,同時(shí)isa又保存了對(duì)象的類(lèi)對(duì)象的首地址。我們通過(guò)object_getClass方法來(lái)獲取這個(gè)對(duì)象的元類(lèi),即是對(duì)象的類(lèi)對(duì)象的類(lèi)型(正常來(lái)說(shuō),class方法內(nèi)部的實(shí)現(xiàn)就是獲取這個(gè)isa保存的對(duì)象的類(lèi)型,在kvo的實(shí)現(xiàn)中蘋(píng)果對(duì)被監(jiān)聽(tīng)對(duì)象的class方法進(jìn)行了重寫(xiě)隱藏了實(shí)現(xiàn))。class方法是獲得對(duì)象的類(lèi)型,雖然這兩個(gè)返回的結(jié)果是一樣的,但是兩個(gè)方法在本質(zhì)上得到的結(jié)果不是同一個(gè)東西
在oc中,規(guī)定了只要擁有isa指針的變量,通通都屬于對(duì)象。上面的objc_object表示的是NSObject這個(gè)類(lèi)的結(jié)構(gòu)體表示,因此oc不允許出現(xiàn)非NSObject子類(lèi)的對(duì)象(block是一個(gè)特殊的例外)*
當(dāng)然了,蘋(píng)果并不想講述更多的實(shí)現(xiàn)細(xì)節(jié),但是我們可以通過(guò)運(yùn)行時(shí)機(jī)制來(lái)完成一些有趣的調(diào)試。
蘋(píng)果的黑魔法
根據(jù)蘋(píng)果的說(shuō)法,在對(duì)象完成監(jiān)聽(tīng)注冊(cè)后,修改了被監(jiān)聽(tīng)對(duì)象的某些屬性,并且改變了isa指針,那么我們可以在監(jiān)聽(tīng)前后輸出被監(jiān)聽(tīng)對(duì)象的相關(guān)屬性來(lái)進(jìn)一步探索kvo的原理。為了保證能夠得到對(duì)象的真實(shí)類(lèi)型,我使用了object_getClass方法,這個(gè)方法在runtime.h頭文件中
NSLog(@"address: %p", self.tableView);
NSLog(@"class method: %@", self.tableView.class);
NSLog(@"description method: %@", self.tableView);
NSLog(@"use runtime to get class: %@", object_getClass(self.tableView));
[self.tableView addObserver: self forKeyPath: @"contentOffset" options: NSKeyValueObservingOptionNew context: nil];
NSLog(@"===================================================");
NSLog(@"address: %p", self.tableView);
NSLog(@"class method: %@", self.tableView.class);
NSLog(@"description method: %@", self.tableView);
NSLog(@"use runtime to get class %@", object_getClass(self.tableView));
在看官們運(yùn)行這段代碼之前,可以先思考一下上面的代碼會(huì)輸出什么。
2015-12-12 23:02:33.216 LXDAlphaNavigationController[1487:63171] address: 0x7f927a81d200
2015-12-12 23:02:33.216 LXDAlphaNavigationController[1487:63171] class method: UITableView
2015-12-12 23:02:33.217 LXDAlphaNavigationController[1487:63171] description method: <UITableView: 0x7f927a81d200; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x7f927971f9a0>; layer = <CALayer: 0x7f9279706f50>; contentOffset: {0, 0}; contentSize: {600, 0}>
2015-12-12 23:02:33.217 LXDAlphaNavigationController[1487:63171] use runtime to get class: UITableView
2015-12-12 23:02:33.217 LXDAlphaNavigationController[1487:63171] ===================================================
2015-12-12 23:02:33.218 LXDAlphaNavigationController[1487:63171] address: 0x7f927a81d200
2015-12-12 23:02:33.218 LXDAlphaNavigationController[1487:63171] class method: UITableView
2015-12-12 23:02:33.218 LXDAlphaNavigationController[1487:63171] description method: <UITableView: 0x7f927a81d200; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x7f927971f9a0>; layer = <CALayer: 0x7f9279706f50>; contentOffset: {0, 0}; contentSize: {600, 0}>
2015-12-12 23:02:33.230 LXDAlphaNavigationController[1487:63171] use runtime to get class NSKVONotifying_UITableView
除了通過(guò)object_getClass獲取的類(lèi)型之外,其他的輸出沒(méi)有任何變化。class方法跟description方法可以重寫(xiě)實(shí)現(xiàn)上面的效果,但是為什么連地址都是一樣的。
這里可以通過(guò)一句小代碼來(lái)說(shuō)明一下:
NSLog(@"%@, %@", self.class, super.class);
上面這段代碼不管你怎么輸出,兩個(gè)結(jié)果都是一樣的。這是由于super本質(zhì)上指向的是父類(lèi)內(nèi)存。這話說(shuō)起來(lái)有點(diǎn)繞口,但是我們可以通過(guò)對(duì)象內(nèi)存圖來(lái)表示:

每一個(gè)對(duì)象占用的內(nèi)存中,一部分是父類(lèi)屬性占用的;在父類(lèi)占用的內(nèi)存中,又有一部分是父類(lèi)的父類(lèi)占用的。前文已經(jīng)說(shuō)過(guò)isa指針指向的是父類(lèi),因此在這個(gè)圖中,Son的地址從Father開(kāi)始,F(xiàn)ather的地址從NSObject開(kāi)始,這三個(gè)對(duì)象內(nèi)存的地址都是一樣的。通過(guò)這個(gè),我們可以猜到蘋(píng)果文檔中所提及的中間類(lèi)就是被監(jiān)聽(tīng)對(duì)象的子類(lèi)。并且為了隱藏實(shí)現(xiàn),蘋(píng)果還重寫(xiě)了這個(gè)子類(lèi)的class方法跟description方法來(lái)掩人耳目。另外,我們還看到了新類(lèi)相對(duì)于父類(lèi)添加了一個(gè)NSKVONotifying_前綴,添加這個(gè)前綴是為了避免多次創(chuàng)建監(jiān)聽(tīng)子類(lèi),節(jié)省資源
怎么實(shí)現(xiàn)類(lèi)似效果
既然知道了蘋(píng)果的實(shí)現(xiàn)過(guò)程,那么我們可以自己動(dòng)手通過(guò)運(yùn)行時(shí)機(jī)制來(lái)實(shí)現(xiàn)KVO。runtime允許我們?cè)诔绦蜻\(yùn)行時(shí)動(dòng)態(tài)的創(chuàng)建新類(lèi)、拓展方法、method-swizzling、綁定屬性等等這些有趣的事情。
在創(chuàng)建新類(lèi)之前,我們應(yīng)該學(xué)習(xí)蘋(píng)果的做法,判斷當(dāng)前是否存在這個(gè)類(lèi),如果不存在我們?cè)龠M(jìn)行創(chuàng)建,并且重新實(shí)現(xiàn)這個(gè)新類(lèi)的class方法來(lái)掩蓋具體實(shí)現(xiàn)?;谶@些原則,我們用下面的方法來(lái)獲取新類(lèi)
- (Class)createKVOClassWithOriginalClassName: (NSString *)className
{
NSString * kvoClassName = [kLXDkvoClassPrefix stringByAppendingString: className];
Class observedClass = NSClassFromString(kvoClassName);
if (observedClass) { return observedClass; }
//創(chuàng)建新類(lèi),并且添加LXDObserver_為類(lèi)名新前綴
Class originalClass = object_getClass(self);
Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0);
//獲取監(jiān)聽(tīng)對(duì)象的class方法實(shí)現(xiàn)代碼,然后替換新建類(lèi)的class實(shí)現(xiàn)
Method classMethod = class_getInstanceMethod(originalClass, @selector(class));
const char * types = method_getTypeEncoding(classMethod);
class_addMethod(kvoClass, @selector(class), (IMP)kvo_Class, types);
objc_registerClassPair(kvoClass);
return kvoClass;
}
另外,在判斷是否需要中間類(lèi)來(lái)完成監(jiān)聽(tīng)的注冊(cè)前,我們還要判斷監(jiān)聽(tīng)的屬性的有效性。通過(guò)獲取變量的setter方法名(將首字母大寫(xiě)并加上前綴set),以此來(lái)獲取setter實(shí)現(xiàn),如果不存在實(shí)現(xiàn)代碼,則拋出異常使程序崩潰。
SEL setterSelector = NSSelectorFromString(setterForGetter(key));
Method setterMethod = class_getInstanceMethod([self class], setterSelector);
if (!setterMethod) {
@throw [NSException exceptionWithName: NSInvalidArgumentException reason: [NSString stringWithFormat: @"unrecognized selector sent to instance %p", self] userInfo: nil];
return;
}
Class observedClass = object_getClass(self);
NSString * className = NSStringFromClass(observedClass);
//如果被監(jiān)聽(tīng)者沒(méi)有LXDObserver_,那么判斷是否需要?jiǎng)?chuàng)建新類(lèi)
if (![className hasPrefix: kLXDkvoClassPrefix]) {
observedClass = [self createKVOClassWithOriginalClassName: className];
object_setClass(self, observedClass);
}
//重新實(shí)現(xiàn)setter方法,使其完成
const char * types = method_getTypeEncoding(setterMethod);
class_addMethod(observedClass, setterSelector, (IMP)KVO_setter, types);
在重新實(shí)現(xiàn)setter方法的時(shí)候,有兩個(gè)重要的方法:willChangeValueForKey和didChangeValueForKey,分別在賦值前后進(jìn)行調(diào)用。此外,還要遍歷所有的回調(diào)監(jiān)聽(tīng)者,然后通知這些監(jiān)聽(tīng)者:
static void KVO_setter(id self, SEL _cmd, id newValue)
{
NSString * setterName = NSStringFromSelector(_cmd);
NSString * getterName = getterForSetter(setterName);
if (!getterName) {
@throw [NSException exceptionWithName: NSInvalidArgumentException reason: [NSString stringWithFormat: @"unrecognized selector sent to instance %p", self] userInfo: nil];
return;
}
id oldValue = [self valueForKey: getterName];
struct objc_super superClass = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
[self willChangeValueForKey: getterName];
void (*objc_msgSendSuperKVO)(void *, SEL, id) = (void *)objc_msgSendSuper;
objc_msgSendSuperKVO(&superClass, _cmd, newValue);
[self didChangeValueForKey: getterName];
//獲取所有監(jiān)聽(tīng)回調(diào)對(duì)象進(jìn)行回調(diào)
NSMutableArray * observers = objc_getAssociatedObject(self, (__bridge const void *)kLXDkvoAssiociateObserver);
for (LXD_ObserverInfo * info in observers) {
if ([info.key isEqualToString: getterName]) {
dispatch_async(dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
info.handler(self, getterName, oldValue, newValue);
});
}
}
}
所有的監(jiān)聽(tīng)者通過(guò)動(dòng)態(tài)綁定的方式將其存儲(chǔ)起來(lái),但這樣也會(huì)產(chǎn)生強(qiáng)引用,所以我們還需要提供釋放監(jiān)聽(tīng)的方法:
- (void)LXD_removeObserver:(NSObject *)object forKey:(NSString *)key
{
NSMutableArray * observers = objc_getAssociatedObject(self, (__bridge void *)kLXDkvoAssiociateObserver);
LXD_ObserverInfo * observerRemoved = nil;
for (LXD_ObserverInfo * observerInfo in observers) {
if (observerInfo.observer == object && [observerInfo.key isEqualToString: key]) {
observerRemoved = observerInfo;
break;
}
}
[observers removeObject: observerRemoved];
}
雖然上面已經(jīng)粗略的實(shí)現(xiàn)了kvo,并且我們還能自定義回調(diào)方式。使用target-action或者block的方式進(jìn)行回調(diào)會(huì)比單一的系統(tǒng)回調(diào)要全面的多。但kvo真正的實(shí)現(xiàn)并沒(méi)有這么簡(jiǎn)單,上述代碼目前只能實(shí)現(xiàn)對(duì)象類(lèi)型的監(jiān)聽(tīng),基本類(lèi)型無(wú)法監(jiān)聽(tīng),況且還有keyPath可以監(jiān)聽(tīng)對(duì)象的成員對(duì)象的屬性這種更強(qiáng)大的功能。
尾言
對(duì)于基本類(lèi)型的監(jiān)聽(tīng),蘋(píng)果可能是通過(guò)void *類(lèi)型對(duì)對(duì)象進(jìn)行橋接轉(zhuǎn)換,然后直接獲取內(nèi)存,通過(guò)type encoding我們可以獲取所有setter對(duì)象的具體類(lèi)型,雖然實(shí)現(xiàn)比較麻煩,但是確實(shí)能夠達(dá)成類(lèi)似的效果。
鉆研kvo的實(shí)現(xiàn)可以讓我們對(duì)蘋(píng)果的代碼實(shí)現(xiàn)有更深層次的了解,這些知識(shí)涉及到了更深層次的技術(shù),探究它們對(duì)我們的開(kāi)發(fā)視野有著很重要的作用。同時(shí),對(duì)比其他的回調(diào)方式,KVO的實(shí)現(xiàn)在創(chuàng)建子類(lèi)、重寫(xiě)方法等等方面的內(nèi)存消耗是很巨大的,因此博主更加推薦使用delegate、block等回調(diào)方式,甚至直接使用method-swizzling來(lái)替換這種重寫(xiě)setter方式也是可行的。
ps:昨天有人問(wèn)我說(shuō)為什么kvo不直接通過(guò)重寫(xiě)setter方法的方式來(lái)進(jìn)行回調(diào),而要?jiǎng)?chuàng)建一個(gè)中間類(lèi)。誠(chéng)然,method_swizzling是一個(gè)很贊的機(jī)制,完全能用它來(lái)滿(mǎn)足監(jiān)聽(tīng)需求。但是,如果我們要監(jiān)聽(tīng)的對(duì)象是tableView呢?正常而言,一款應(yīng)用中遠(yuǎn)不止一個(gè)列表,使用method_swizzling會(huì)導(dǎo)致所有的列表都添加了監(jiān)聽(tīng)回調(diào),先不考慮這可能導(dǎo)致的崩潰風(fēng)險(xiǎn),所有繼承自tableView的視圖(包括自身)的setter都受到了影響。而使用中間類(lèi)卻避免了這個(gè)問(wèn)題