FBKVOController & KVO

FBKVOController是一個簡單易用的鍵值觀察框架,KVOController 對于 Cocoa 中 KVO 的封裝非常的簡潔和優(yōu)秀屏鳍,我們只需要調(diào)用一個方法就可以完成一個對象的鍵值觀測勘纯,同時不需要處理移除觀察者等問題,能夠降低我們出錯的可能性钓瞭。
本文參考了:如何優(yōu)雅地使用 KVO驳遵。

KVOController的簡單使用
@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對比:
  • KVO
    1.KVO需要手動移除觀察者,且移除觀察者的時機必須合適山涡;
    2.注冊觀察者的代碼和事件發(fā)生處的代碼上下文不同堤结,傳遞上下文是通過void *指針;
    3.需要重寫-observeValueForKeyPath:ofObject:change:context:方法鸭丛,比較麻煩竞穷;
    4.在復(fù)雜的業(yè)務(wù)邏輯中,準確判斷被觀察者相對比較麻煩鳞溉,有多個被觀測的對象和屬性時瘾带,需要在方法中寫大量的 if 進行判斷;

  • FBKVOController
    1.不需要手動移除觀察者穿挨;
    2.實現(xiàn)KVO 與事件發(fā)生處的代碼上下文相同月弛,不需要跨方法傳參數(shù);
    3.使用 block 來替代方法能夠減少使用的復(fù)雜度科盛,提升使用KVO的體驗帽衙;
    4.每一個keyPath 會對應(yīng)一個屬性,不需要在 block 中使用if判斷keyPath贞绵;

  • FBKVOController實現(xiàn)了觀察者和被觀察者的角色反轉(zhuǎn)厉萝,系統(tǒng)的KVO是被觀察者添加觀察者,而FBKVOController實現(xiàn)了觀察者主動去添加被觀察者,實現(xiàn)了角色上的反轉(zhuǎn)谴垫,其實就是用的比較方便章母。

    // 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框架有兩個類,其實只是對 Cocoa 中 KVO 的封裝翩剪,分別是KVOControllerNSObject+FBKVOController乳怎。先來看一下NSObject+FBKVOController的實現(xiàn)。

NSObject+FBKVOController.h

@interface NSObject (FBKVOController)
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end

從名字可以看出KVOControllerNonRetaining在使用時并不會持有被觀察的對象前弯,與它相比蚪缀,KVOController 就會持有該對象了。

- (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按照鍵值簡單地存一下恕出,而 getter中不同的其實也就是對于FBKVOController的初始化了询枚。

KVOController 的初始化

KVOController 是整個框架中提供 KVO 接口的類,作為 KVO 的管理者浙巫,其必須持有當前對象所有與 KVO 有關(guān)的信息金蜀,而在 KVOController 中,用于存儲這個信息的數(shù)據(jù)結(jié)構(gòu)就是 NSMapTable的畴。為了使 KVOController 達到線程安全渊抄,它還必須持有一把 pthread_mutex_t 鎖,用于在操作 _objectInfosMap 時使用苗傅。

- (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決定是強引用還是弱引用被觀察者,KVOController 和 KVOControllerNonRetaining 的區(qū)別就體現(xiàn)在生成的 NSMapTable 實例時傳入的是 NSPointerFunctionsStrongMemory 還是 NSPointerFunctionsWeakMemory 選項渣慕。最后初始化pthread_mutex_t 鎖。

KVO 的過程

使用KVOController實現(xiàn)鍵值觀測時會調(diào)用實例方法-observe:keyPath:options:block來注冊成為某個對象的觀察者抱慌,監(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

上面的方法中就涉及到另外一個私有的數(shù)據(jù)結(jié)構(gòu)_FBKVOInfo逊桦,這個類中包含著所有與 KVO 有關(guān)的信息:

{
@public
  __weak FBKVOController *_controller;
  NSString *_keyPath;
  NSKeyValueObservingOptions _options;
  SEL _action;
  void *_context;
  FBKVONotificationBlock _block;
  _FBKVOInfoState _state;
}

_FBKVOInfoKVOController中充當?shù)淖饔脙H僅是一個數(shù)據(jù)結(jié)構(gòu),我們主要用它來存儲整個 KVO 過程中所需要的全部信息抑进,_FBKVOInfo重寫了-isEqual:方法用于對象之間的判等以及方便NSMapTable的存儲强经。其中的state表示當前的 KVO 狀態(tài)。

typedef NS_ENUM(uint8_t, _FBKVOInfoState) {
  _FBKVOInfoStateInitial = 0,
  _FBKVOInfoStateObserving,
  _FBKVOInfoStateNotObserving,
};

observe 的過程

-observer:keyPath:options:block:初始化了一個名為_FBKVOInfo的對象:

- (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í)行了另一個私有方法-_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];
}

這個私有方法通過自身持有_objectInfosMap
來判斷當前對象寺渗、屬性以及各種上下文是否已經(jīng)注冊在表中存在了匿情,在這個
_objectInfosMap中保存著對象以及與對象有關(guān)的_FBKVOInfo集合:
在操作了當前KVOController持有的_objectInfosMap之后,才會執(zhí)行私有的_FBKVOSharedController類的實例方法-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:方法開始對屬性的監(jiān)聽的地方信殊;同時炬称,在整個應(yīng)用運行時,只會存在一個_FBKVOSharedController實例:

+ (instancetype)sharedController {
  static _FBKVOSharedController *_controller = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    _controller = [[_FBKVOSharedController alloc] init];
  });
  return _controller;
}

這個_FBKVOSharedController單例會在 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];
    }
}

在這個-observeValueForKeyPath:ofObject:change:context:回調(diào)方法中玲躯,_FBKVOSharedController會根據(jù) KVO 的信息_KVOInfo選擇不同的方式分發(fā)事件,如果觀察者沒有傳入 block 或者選擇子,就會調(diào)用觀察者 KVO 回調(diào)方法跷车。

如何 removeObserver

在使用 KVOController 時棘利,我們并不需要手動去處理 KVO 觀察者的移除,因為所有的 KVO 事件都由私有的_KVOSharedController來處理朽缴;
當每一個KVOController對象被釋放時善玫,都會將它自己持有的所有 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;
  }
}

該方法會遍歷所有傳入的_FBKVOInfo,從其中取出keyPath并將_KVOSharedController移除觀察者密强。
除了在KVOController析構(gòu)時會自動移除觀察者蝌焚,我們也可以通過它的實例方法-unobserve:keyPath:操作達到相同的效果;功能的實現(xiàn)過程其實都是類似的誓斥,都是通過-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;
}

不過由于這個方法的參數(shù)并不是一個數(shù)組只洒,所以并不需要使用for循環(huán),而是只需要將該_FBKVOInfo對應(yīng)的 KVO 事件移除就可以了劳坑。

KVO

轉(zhuǎn)載:KVO的奧秘 Demo鏈接

KVO的使用非常簡單毕谴,使用KVO的要求是對象必須能支持kvc機制——所有NSObject的子類都支持這個機制。拿上面的漸變導(dǎo)航欄做距芬,我們?yōu)閠ableView添加了一個監(jiān)聽者controller涝开,在我們滑動列表的時候,會計算當前列表的滾動偏移量框仔,然后改變導(dǎo)航欄的背景色透明度舀武。

//添加監(jiān)聽者
[self.tableView addObserver: self forKeyPath: @"contentOffset" options: NSKeyValueObservingOptionNew context: nil];
/**
 *  監(jiān)聽屬性值發(fā)生改變時回調(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);
}

毫無疑問,kvo是一種非常便捷的回調(diào)方式离斩,但是編譯器是怎么完成監(jiān)聽這個任務(wù)的呢银舱?先來看看蘋果文檔對于KVO的實現(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)聽的注冊后跛梗,編譯器會修改監(jiān)聽對象(上文中的tableView)的isa指針寻馏,讓這個指針指向一個新生成的中間類。從某個意義上來說核偿,這是一場騙局诚欠。

typedef struct objc_class *Class;
typedef struct objc_object {
   Class isa;
} *id;

這里要說明的是isa這個指針,isa是一個Class類型的指針漾岳,對象的首地址一般是isa變量轰绵,同時isa又保存了對象的類對象的首地址。我們通過object_getClass方法來獲取這個對象的元類尼荆,即是對象的類對象的類型(正常來說左腔,class方法內(nèi)部的實現(xiàn)就是獲取這個isa保存的對象的類型,在kvo的實現(xiàn)中蘋果對被監(jiān)聽對象的class方法進行了重寫隱藏了實現(xiàn))耀找。class方法是獲得對象的類型翔悠,雖然這兩個返回的結(jié)果是一樣的业崖,但是兩個方法在本質(zhì)上得到的結(jié)果不是同一個東西
在oc中,規(guī)定了只要擁有isa指針的變量蓄愁,通通都屬于對象双炕。上面的objc_object表示的是NSObject這個類的結(jié)構(gòu)體表示,因此oc不允許出現(xiàn)非NSObject子類的對象
(block是一個特殊的例外)*
當然了撮抓,蘋果并不想講述更多的實現(xiàn)細節(jié)妇斤,但是我們可以通過運行時機制來完成一些有趣的調(diào)試。

蘋果的黑魔法

根據(jù)蘋果的說法丹拯,在對象完成監(jiān)聽注冊后站超,修改了被監(jiān)聽對象的某些屬性,并且改變了isa指針乖酬,那么我們可以在監(jiān)聽前后輸出被監(jiān)聽對象的相關(guān)屬性來進一步探索kvo的原理死相。為了保證能夠得到對象的真實類型,我使用了object_getClass方法咬像,這個方法在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));

在看官們運行這段代碼之前算撮,可以先思考一下上面的代碼會輸出什么。

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

除了通過object_getClass獲取的類型之外县昂,其他的輸出沒有任何變化肮柜。class方法跟description方法可以重寫實現(xiàn)上面的效果,但是為什么連地址都是一樣的倒彰。
這里可以通過一句小代碼來說明一下:

NSLog(@"%@, %@", self.class, super.class);

上面這段代碼不管你怎么輸出审洞,兩個結(jié)果都是一樣的。這是由于super本質(zhì)上指向的是父類內(nèi)存待讳。這話說起來有點繞口芒澜,但是我們可以通過對象內(nèi)存圖來表示:

image

每一個對象占用的內(nèi)存中,一部分是父類屬性占用的耙箍;在父類占用的內(nèi)存中撰糠,又有一部分是父類的父類占用的。前文已經(jīng)說過isa指針指向的是父類辩昆,因此在這個圖中,Son的地址從Father開始旨袒,F(xiàn)ather的地址從NSObject開始汁针,這三個對象內(nèi)存的地址都是一樣的。通過這個砚尽,我們可以猜到蘋果文檔中所提及的中間類就是被監(jiān)聽對象的子類施无。并且為了隱藏實現(xiàn),蘋果還重寫了這個子類的class方法跟description方法來掩人耳目必孤。另外猾骡,我們還看到了新類相對于父類添加了一個NSKVONotifying_前綴瑞躺,添加這個前綴是為了避免多次創(chuàng)建監(jiān)聽子類,節(jié)省資源

怎么實現(xiàn)類似效果

既然知道了蘋果的實現(xiàn)過程兴想,那么我們可以自己動手通過運行時機制來實現(xiàn)KVO幢哨。runtime允許我們在程序運行時動態(tài)的創(chuàng)建新類、拓展方法嫂便、method-swizzling捞镰、綁定屬性等等這些有趣的事情。
在創(chuàng)建新類之前毙替,我們應(yīng)該學(xué)習(xí)蘋果的做法岸售,判斷當前是否存在這個類,如果不存在我們再進行創(chuàng)建厂画,并且重新實現(xiàn)這個新類的class方法來掩蓋具體實現(xiàn)凸丸。基于這些原則袱院,我們用下面的方法來獲取新類

- (Class)createKVOClassWithOriginalClassName: (NSString *)className
{
    NSString * kvoClassName = [kLXDkvoClassPrefix stringByAppendingString: className];
    Class observedClass = NSClassFromString(kvoClassName);
    if (observedClass) { return observedClass; }

    //創(chuàng)建新類屎慢,并且添加LXDObserver_為類名新前綴
    Class originalClass = object_getClass(self);
    Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0);

    //獲取監(jiān)聽對象的class方法實現(xiàn)代碼,然后替換新建類的class實現(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;
}

另外坑填,在判斷是否需要中間類來完成監(jiān)聽的注冊前抛人,我們還要判斷監(jiān)聽的屬性的有效性。通過獲取變量的setter方法名(將首字母大寫并加上前綴set)脐瑰,以此來獲取setter實現(xiàn)妖枚,如果不存在實現(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)聽者沒有LXDObserver_绝页,那么判斷是否需要創(chuàng)建新類
if (![className hasPrefix: kLXDkvoClassPrefix]) {
    observedClass = [self createKVOClassWithOriginalClassName: className];
    object_setClass(self, observedClass);
}
//重新實現(xiàn)setter方法,使其完成
const char * types = method_getTypeEncoding(setterMethod);
class_addMethod(observedClass, setterSelector, (IMP)KVO_setter, types);

在重新實現(xiàn)setter方法的時候寂恬,有兩個重要的方法:willChangeValueForKeydidChangeValueForKey续誉,分別在賦值前后進行調(diào)用。此外初肉,還要遍歷所有的回調(diào)監(jiān)聽者酷鸦,然后通知這些監(jiān)聽者:

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)聽回調(diào)對象進行回調(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ài)綁定的方式將其存儲起來,但這樣也會產(chǎn)生強引用牙咏,所以我們還需要提供釋放監(jiān)聽的方法:

- (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)粗略的實現(xiàn)了kvo臼隔,并且我們還能自定義回調(diào)方式。使用target-action或者block的方式進行回調(diào)會比單一的系統(tǒng)回調(diào)要全面的多妄壶。但kvo真正的實現(xiàn)并沒有這么簡單摔握,上述代碼目前只能實現(xiàn)對象類型的監(jiān)聽,基本類型無法監(jiān)聽丁寄,況且還有keyPath可以監(jiān)聽對象的成員對象的屬性這種更強大的功能氨淌。

尾言

對于基本類型的監(jiān)聽泊愧,蘋果可能是通過void *類型對對象進行橋接轉(zhuǎn)換,然后直接獲取內(nèi)存盛正,通過type encoding我們可以獲取所有setter對象的具體類型删咱,雖然實現(xiàn)比較麻煩,但是確實能夠達成類似的效果蛮艰。
鉆研kvo的實現(xiàn)可以讓我們對蘋果的代碼實現(xiàn)有更深層次的了解腋腮,這些知識涉及到了更深層次的技術(shù),探究它們對我們的開發(fā)視野有著很重要的作用壤蚜。同時即寡,對比其他的回調(diào)方式,KVO的實現(xiàn)在創(chuàng)建子類袜刷、重寫方法等等方面的內(nèi)存消耗是很巨大的聪富,因此博主更加推薦使用delegate、block等回調(diào)方式著蟹,甚至直接使用method-swizzling來替換這種重寫setter方式也是可行的墩蔓。
ps:昨天有人問我說為什么kvo不直接通過重寫setter方法的方式來進行回調(diào),而要創(chuàng)建一個中間類萧豆。誠然奸披,method_swizzling是一個很贊的機制五慈,完全能用它來滿足監(jiān)聽需求搞莺。但是,如果我們要監(jiān)聽的對象是tableView呢誊涯?正常而言洪鸭,一款應(yīng)用中遠不止一個列表样刷,使用method_swizzling會導(dǎo)致所有的列表都添加了監(jiān)聽回調(diào),先不考慮這可能導(dǎo)致的崩潰風險览爵,所有繼承自tableView的視圖(包括自身)的setter都受到了影響置鼻。而使用中間類卻避免了這個問題

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蜓竹,隨后出現(xiàn)的幾起案子箕母,更是在濱河造成了極大的恐慌,老刑警劉巖俱济,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件司蔬,死亡現(xiàn)場離奇詭異,居然都是意外死亡姨蝴,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門肺缕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來左医,“玉大人授帕,你說我怎么就攤上這事「∩遥” “怎么了跛十?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長秕硝。 經(jīng)常有香客問我芥映,道長,這世上最難降的妖魔是什么远豺? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任奈偏,我火速辦了婚禮,結(jié)果婚禮上躯护,老公的妹妹穿的比我還像新娘惊来。我一直安慰自己,他們只是感情好棺滞,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布裁蚁。 她就那樣靜靜地躺著,像睡著了一般继准。 火紅的嫁衣襯著肌膚如雪枉证。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天移必,我揣著相機與錄音室谚,去河邊找鬼。 笑死避凝,一個胖子當著我的面吹牛舞萄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播管削,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼倒脓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了含思?” 一聲冷哼從身側(cè)響起崎弃,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎含潘,沒想到半個月后饲做,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡遏弱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年盆均,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片漱逸。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡泪姨,死狀恐怖游沿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肮砾,我是刑警寧澤诀黍,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站仗处,受9級特大地震影響眯勾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜婆誓,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一吃环、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧旷档,春花似錦模叙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至厂庇,卻和暖如春渠啊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背权旷。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工替蛉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拄氯。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓躲查,卻偏偏與公主長得像,于是被迫代替她去往敵國和親译柏。 傳聞我的和親對象是個殘疾皇子镣煮,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,681評論 0 9
  • 上半年有段時間做了一個項目鄙麦,項目中聊天界面用到了音頻播放典唇,涉及到進度條,當時做android時候處理的不太好胯府,由于...
    DaZenD閱讀 3,013評論 0 26
  • KVO 作為 iOS 中一種強大并且有效的機制介衔,為 iOS 開發(fā)者們提供了很多的便利;我們可以使用 KVO 來檢測...
    Draveness閱讀 6,885評論 11 59
  • KVO 作為 iOS 中一種強大并且有效的機制骂因,為 iOS 開發(fā)者們提供了很多的便利炎咖;我們可以使用 KVO 來檢測...
    JzRo閱讀 925評論 0 2
  • 一、概述 KVO,即:Key-Value Observing塘装,它提供一種機制急迂,當指定的對象的屬性被修改后,則其觀察...
    DeerRun閱讀 10,038評論 11 33