KVO總結(jié)和FBKVOController

KVO是IOS中一種強(qiáng)大且有效的機(jī)制定罢,當(dāng)一個(gè)對象的屬性發(fā)生變化時(shí)吼鱼,注冊成為這個(gè)對象的觀察者的其他對象可以收到通知韩脑。我們可以使用KVO來觀察對象屬性的變化劫谅。比如见坑,想實(shí)現(xiàn)下拉刷新效果時(shí),可以使用KVO觀察UITableView的contenOffset屬性的變化來實(shí)現(xiàn)的捏检。

In order to be considered KVO-compliant for a specific property, a class must ensure the following:

  • The class must be key-value coding compliant for the property, as specified in Ensuring KVC Compliance.KVO supports the same data types as KVC, including Objective-C objects and the scalars and structures listed in Scalar and Structure Support
  • The class emits KVO change notifications for the property.
  • Dependent keys are registered appropriately (see Registering Dependent Keys).

文檔里提到了鳄梅,要使一個(gè)類的屬性支持KVO,這個(gè)類對于屬性是滿足KVC的未檩,并且這個(gè)類會發(fā)送KVO的通知。

有兩種技術(shù)來確保發(fā)送KVO通知:

  • Automatic support :自動支持是NSObject提供的粟焊,對于支持KVC的屬性默認(rèn)都是可用的冤狡。不需要寫額外的代碼來發(fā)送屬性改變的通知。

  • Manual change notification:手動發(fā)送通知需要額外的代碼项棠。

Automatic Change Notification

NSObject provides a basic implementation of automatic key-value change notification. Automatic key-value change notification informs observers of changes made using key-value compliant accessors, as well as the key-value coding methods.

NSObject實(shí)現(xiàn)了自動改變的通知悲雳。自動通知有兩種,一種是使用屬性的setter方法香追,一種是使用KVC合瓢。

KVO的原理

KVO的實(shí)現(xiàn)依賴于runtime,Apple文檔里提到過KVO的實(shí)現(xiàn)

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

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. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

蘋果使用了 isa-swizzling來實(shí)現(xiàn)KVO透典。當(dāng)給一個(gè)被觀察者的屬性添加觀察者后晴楔,被觀察者的isa指針會被改變,指向一個(gè)中間的class峭咒,而不是原來真正的class税弃。具體來說,會創(chuàng)建一個(gè)新的類凑队,這個(gè)類繼承自被觀察者则果,并且重寫了被觀察的屬性的setter方法,重寫的setter方法會在負(fù)責(zé)調(diào)用原setter方法的前后漩氨,通知所有觀察者值得變化(使用willChangeValueForKeydidChangeValueForKey來通知)西壮,并且把isa的指針指向這個(gè)新創(chuàng)建的子類。

-(void)setName:(NSString *)newName{ 
[self willChangeValueForKey:@"name"];    //KVO在調(diào)用存取方法之前總調(diào)用 
[super setValue:newName forKey:@"name"]; //調(diào)用父類的存取方法 
[self didChangeValueForKey:@"name"];     //KVO在調(diào)用存取方法之后總調(diào)用}

看一下下面的測試代碼:

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Person

@end
#import <objc/runtime.h>
#import "Person.h"
int main(int argc, char * argv[]) {
    Person *p = [[Person alloc] init];
    PrintDescriptionid(p);
    [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    PrintDescriptionid(p);
    return 0;
}

static NSArray *ClassMethodNames(Class c)
{
    NSMutableArray *array = [NSMutableArray array];
    
    unsigned int methodCount = 0;
    Method *methodList = class_copyMethodList(c, &methodCount);
    unsigned int i;
    for(i = 0; i < methodCount; i++)
        [array addObject: NSStringFromSelector(method_getName(methodList[i]))];
    free(methodList);
    
    return array;
}

static void PrintDescriptionid( id obj)
{
    NSString *str = [NSString stringWithFormat:
                     @"NSObject class %s\nLibobjc class %s\nSuper Class %s\nimplements methods <%@>",
                     class_getName([obj class]),
                     class_getName(object_getClass(obj)),
                     class_getName(class_getSuperclass(object_getClass(obj))),
                     [ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "]];
    printf("%s\n", [str UTF8String]);
}

log:

//添加觀察者之前
NSObject class Person
Libobjc class Person
Super Class NSObject
implements methods <.cxx_destruct, name, setName:>

//添加觀察者之后
NSObject class Person
Libobjc class NSKVONotifying_Person
Super Class Person
implements methods <setName:, class, dealloc, _isKVOA>

object_getClass(obj)會獲取obj對象isa指向的類叫惊。從log可以看出款青,添加觀察者后,obj對象isa指針指向NSKVONotifying_Person這個(gè)類赋访,它的父類是Person,并且NSKVONotifying_Person里實(shí)現(xiàn)了<setName:, class, dealloc, _isKVOA>這個(gè)幾個(gè)方法可都。

Manual Change Notification

如果你想完全控制一個(gè)屬性的通知缓待,需要重寫automaticallyNotifiesObserversForKey:

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    BOOL automatic = NO;
    if ([key isEqualToString:@"name"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

在運(yùn)行一下之前的代碼log:

//添加KVO之前
Libobjc class Person
Super Class NSObject
implements methods <.cxx_destruct, name, setName:>
//添加KVO之后
NSObject class Person
Libobjc class Person
Super Class NSObject
implements methods <.cxx_destruct, name, setName:>

此時(shí)也不會創(chuàng)建NSKVONotifying_Person這個(gè)類了。為了實(shí)現(xiàn)手動發(fā)送通知渠牲,這是在改變值之前要調(diào)用 willChangeValueForKey:旋炒,改變值之后調(diào)用didChangeValueForKey:

KVO的使用:

[self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];

這個(gè)方法是給tableView添加了一個(gè)觀察者來監(jiān)測tableView的contentOffset屬性的變化。這個(gè)方法不會增加方法的調(diào)用者(self.tableView)和觀察者(self)的引用計(jì)數(shù)签杈。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"%@", change);
}

在觀察者里實(shí)現(xiàn)這個(gè)方法瘫镇,當(dāng)被觀察者的被觀察的屬性發(fā)生變化時(shí),會調(diào)用這個(gè)方法答姥。

最后铣除,不要忘記移除觀察者:

- (void)dealloc
{
    [self.tableView removeObserver:self forKeyPath:@"contentOffset"];
}

FBKVOController

facebook開源的FBKVOController框架可以很方便地使用KVO。

使用FBKVOController鹦付,上面的代碼可以改寫成:

 [self.KVOController observe:self.tableView keyPath:@"contentOffset" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        NSLog(@"%@", change);
 }];

在FBKVOController尚粘,有一個(gè)NSObject的category,里面給NSObject添加了兩個(gè)屬性

@property (nonatomic, strong) FBKVOController *KVOController;

@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;

使用KVOController時(shí)會對被觀察的對象強(qiáng)引用敲长,使用KVOControllerNonRetaining對被觀察的對象是弱引用郎嫁。

FBKVOController類里有一個(gè)實(shí)例變量:

  NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;

NSMapTable 的key存儲的是被觀察的對象,在初始化方法里可以設(shè)置成強(qiáng)引用或者弱引用的祈噪。它的value存放的是_FBKVOInfo對象泽铛,主要是關(guān)于被觀察者的keyPath等信息。
FBKVOController使用-observer:keyPath:options:block:觀察對象屬性變化時(shí)辑鲤,用到了_FBKVOSharedController這個(gè)類盔腔,這個(gè)類是一個(gè)單例,它的實(shí)例方法添加了觀察者:

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
  if (nil == info) {
    return;
  }

  // register info
  pthread_mutex_lock(&_mutex);
  [_infos addObject:info];
  pthread_mutex_unlock(&_mutex);

  // add observer
  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

  if (info->_state == _FBKVOInfoStateInitial) {
    info->_state = _FBKVOInfoStateObserving;
  } else if (info->_state == _FBKVOInfoStateNotObserving) {
    // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
    // and the observer is unregistered within the callback block.
    // at this time the object has been registered as an observer (in Foundation KVO),
    // so we can safely unobserve it.
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
}

它也實(shí)現(xiàn)了觀察變化的方法:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context
{
  NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

  _FBKVOInfo *info;

  {
    // lookup context in registered infos, taking out a strong reference only if it exists
    pthread_mutex_lock(&_mutex);
    info = [_infos member:(__bridge id)context];
    pthread_mutex_unlock(&_mutex);
  }

  if (nil != info) {

    // take strong reference to controller
    FBKVOController *controller = info->_controller;
    if (nil != controller) {

      // take strong reference to observer
      id observer = controller.observer;
      if (nil != observer) {

        // dispatch custom block or action, fall back to default action
        if (info->_block) {
          NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
          // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
          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) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
          [observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
        } else {
          [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
        }
      }
    }
  }
}

當(dāng)KVOController調(diào)用dealloc時(shí)月褥,會移除觀察者弛随。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市吓坚,隨后出現(xiàn)的幾起案子撵幽,更是在濱河造成了極大的恐慌,老刑警劉巖礁击,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盐杂,死亡現(xiàn)場離奇詭異,居然都是意外死亡哆窿,警方通過查閱死者的電腦和手機(jī)链烈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挚躯,“玉大人强衡,你說我怎么就攤上這事÷肜螅” “怎么了漩勤?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵感挥,是天一觀的道長。 經(jīng)常有香客問我越败,道長触幼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任究飞,我火速辦了婚禮置谦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘亿傅。我一直安慰自己媒峡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布葵擎。 她就那樣靜靜地躺著谅阿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪酬滤。 梳的紋絲不亂的頭發(fā)上奔穿,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天,我揣著相機(jī)與錄音敏晤,去河邊找鬼。 笑死缅茉,一個(gè)胖子當(dāng)著我的面吹牛嘴脾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蔬墩,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼译打,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拇颅?” 一聲冷哼從身側(cè)響起奏司,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎樟插,沒想到半個(gè)月后韵洋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡黄锤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年搪缨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸵熟。...
    茶點(diǎn)故事閱讀 39,953評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡副编,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出流强,到底是詐尸還是另有隱情痹届,我是刑警寧澤呻待,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站队腐,受9級特大地震影響蚕捉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜香到,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一鱼冀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧悠就,春花似錦千绪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至炸茧,卻和暖如春瑞妇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背梭冠。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工辕狰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人控漠。 一個(gè)月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓蔓倍,卻偏偏與公主長得像,于是被迫代替她去往敵國和親盐捷。 傳聞我的和親對象是個(gè)殘疾皇子偶翅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評論 2 355

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