詳解:RxSwift-KVO底層探索(上)

收錄:原文地址

KVO在我們實際開發(fā)之中運用非常之多,很多開發(fā)者都知道原理鳄炉!但是這些原理是如何來的搜骡,一般都是淺嘗輒止记靡。這個篇章我會從 Swift 入手分析,探索KVO底層源碼.希望讓讀者真正掌握這一塊底層空凸,知其然而知其所以然呀洲!

KVO簡介

首先我們從KVO的三部曲開始

// 1: 添加觀察
person.addObserver(self, forKeyPath: "name", options: .new, context: nil)
// 2: 觀察響應(yīng)回調(diào)
override func observeValue(forKeyPath keyPath:, of object:, change: , context:){}
// 3: 移除觀察
person.removeObserver(self, forKeyPath: "name")

其實我們也知道两嘴,就是平時在開發(fā)的時候族壳,我們也可以通過計算型屬性也可以直接觀察

var name: String = ""{
    willSet{
        print(newValue)
    }
    didSet{
        print(oldValue)
    }
}

問題來了:這兩者有什么關(guān)系仿荆?

KVO與計算型屬性的關(guān)系

下面我們開始分析拢操,首先感謝蘋果開源精神,在Github可以直接下載杠园,我們通過 Swift 源碼展開分析

public func willChangeValue<Value>(for keyPath: __owned KeyPath<Self, Value>) {
    (self as! NSObject).willChangeValue(forKey: _bridgeKeyPathToString(keyPath))
}

public func willChange<Value>(_ changeKind: NSKeyValueChange, valuesAt indexes: IndexSet, for keyPath: __owned KeyPath<Self, Value>) {
    (self as! NSObject).willChange(changeKind, valuesAt: indexes, forKey: _bridgeKeyPathToString(keyPath))
}

public func willChangeValue<Value>(for keyPath: __owned KeyPath<Self, Value>, withSetMutation mutation: NSKeyValueSetMutationKind, using set: Set<Value>) -> Void {
    (self as! NSObject).willChangeValue(forKey: _bridgeKeyPathToString(keyPath), withSetMutation: mutation, using: set)
}

public func didChangeValue<Value>(for keyPath: __owned KeyPath<Self, Value>) {
    (self as! NSObject).didChangeValue(forKey: _bridgeKeyPathToString(keyPath))
}

public func didChange<Value>(_ changeKind: NSKeyValueChange, valuesAt indexes: IndexSet, for keyPath: __owned KeyPath<Self, Value>) {
    (self as! NSObject).didChange(changeKind, valuesAt: indexes, forKey: _bridgeKeyPathToString(keyPath))
}

public func didChangeValue<Value>(for keyPath: __owned KeyPath<Self, Value>, withSetMutation mutation: NSKeyValueSetMutationKind, using set: Set<Value>) -> Void {
    (self as! NSObject).didChangeValue(forKey: _bridgeKeyPathToString(keyPath), withSetMutation: mutation, using: set)
}

  • willChangeValuedidChangeValue 作為數(shù)據(jù)改變的兩個重要的方法
  • 我們通過這兩個方法繼續(xù)展開分析
class Target : NSObject, NSKeyValueObservingCustomization {
    // This dynamic property is observed by KVO
    @objc dynamic var objcValue: String
    @objc dynamic var objcValue2: String {
        willSet {
            willChangeValue(for: \.objcValue2)
        }
        didSet {
            didChangeValue(for: \.objcValue2)
        }
    }
}

  • 很明顯的繼承關(guān)系來自NSObject
  • 實現(xiàn)了 NSKeyValueObservingCustomization 的協(xié)議
public protocol NSKeyValueObservingCustomization : NSObjectProtocol {
    static func keyPathsAffectingValue(for key: AnyKeyPath) -> Set<AnyKeyPath>
    static func automaticallyNotifiesObservers(for key: AnyKeyPath) -> Bool
}

  • 這也是我們兩個非常重要的方法惕橙,平時在開發(fā)也是很有利的方法,keyPathsAffectingValue能夠建立 keyPath 的依賴肚逸,例如兩個屬性的變化同時影響一個重要屬性的改變:進度 = 下載量 / 總量
  • automaticallyNotifiesObservers 自動開關(guān)
  • 很明顯我們的計算型屬性在willSet里面就調(diào)用willChangeValue朦促,didSet調(diào)用didChangeValue务冕,的確我們計算型屬性是和我們KVO相關(guān)方法是有所關(guān)聯(lián)混滔,這里也直接證明坯屿!
  • OK领跛,我們探索完這個問題,我們摸著這條線繼續(xù)探索KVO底層喊括!

KVO底層

這里說明一下郑什,本篇章的貼出的源碼沒有給大家省略,目的是想讓大家認真閱讀钝满,自己對照學習弯蚜。當然可能中間我也忽略過一些細節(jié)剃法,源碼直接貼出來方便自己理解

添加觀察

person.addObserver(self, forKeyPath: "name", options: .new, context: nil)

現(xiàn)在我們開始探索底層源碼:

- (void) addObserver: (NSObject*)anObserver
      forKeyPath: (NSString*)aPath
         options: (NSKeyValueObservingOptions)options
         context: (void*)aContext
{
  GSKVOInfo             *info;
  GSKVOReplacement      *r;
  NSKeyValueObservationForwarder *forwarder;
  NSRange               dot;

  setup();
  [kvoLock lock];

  // Use the original class
  r = replacementForClass([self class]);

  /*
   * Get the existing observation information, creating it (and changing
   * the receiver to start key-value-observing by switching its class)
   * if necessary.
   */
  info = (GSKVOInfo*)[self observationInfo];
  if (info == nil)
    {
      info = [[GSKVOInfo alloc] initWithInstance: self];
      [self setObservationInfo: info];
      object_setClass(self, [r replacement]);
    }

  /*
   * Now add the observer.
   */
  dot = [aPath rangeOfString:@"."];
  if (dot.location != NSNotFound)
    {
      forwarder = [[NSKeyValueObservationForwarder alloc]
        initWithKeyPath: aPath
           ofObject: self
         withTarget: anObserver
        context: aContext];
      [info addObserver: anObserver
             forKeyPath: aPath
                options: options
                context: forwarder];
    }
  else
    {
      [r overrideSetterFor: aPath];
      [info addObserver: anObserver
             forKeyPath: aPath
                options: options
                context: aContext];
    }

  [kvoLock unlock];
}

  • 中間replacementForClass做了一些處理:

    • 創(chuàng)建了一個動態(tài)子類名字:"NSKVONotifing_原類的名字"
    • 添加了class收厨、set帽氓、dealloc方法
    • 原類的isa與動態(tài)isa切換
  • 由原來的觀察者進行遷移到 GSKVOInfo

- (void) addObserver: (NSObject*)anObserver
      forKeyPath: (NSString*)aPath
         options: (NSKeyValueObservingOptions)options
         context: (void*)aContext
{
  GSKVOPathInfo         *pathInfo;
  GSKVOObservation      *observation;
  unsigned              count;

  if ([anObserver respondsToSelector:
    @selector(observeValueForKeyPath:ofObject:change:context:)] == NO)
    {
      return;
    }
  [iLock lock];
  pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
  if (pathInfo == nil)
    {
      pathInfo = [GSKVOPathInfo new];
      // use immutable object for map key
      aPath = [aPath copy];
      NSMapInsert(paths, (void*)aPath, (void*)pathInfo);
      [pathInfo release];
      [aPath release];
    }

  observation = nil;
  pathInfo->allOptions = 0;
  count = [pathInfo->observations count];
  while (count-- > 0)
    {
      GSKVOObservation      *o;

      o = [pathInfo->observations objectAtIndex: count];
      if (o->observer == anObserver)
        {
          o->context = aContext;
          o->options = options;
          observation = o;
        }
      pathInfo->allOptions |= o->options;
    }
  if (observation == nil)
    {
      observation = [GSKVOObservation new];
      GSAssignZeroingWeakPointer((void**)&observation->observer,
    (void*)anObserver);
      observation->context = aContext;
      observation->options = options;
      [pathInfo->observations addObject: observation];
      [observation release];
      pathInfo->allOptions |= options;
    }

  if (options & NSKeyValueObservingOptionInitial)
    {
      /* If the NSKeyValueObservingOptionInitial option is set,
       * we must send an immediate notification containing the
       * existing value in the NSKeyValueChangeNewKey
       */
      [pathInfo->change setObject: [NSNumber numberWithInt: 1]
                           forKey:  NSKeyValueChangeKindKey];
      if (options & NSKeyValueObservingOptionNew)
        {
          id    value;

          value = [instance valueForKeyPath: aPath];
          if (value == nil)
            {
              value = null;
            }
          [pathInfo->change setObject: value
                               forKey: NSKeyValueChangeNewKey];
        }
      [anObserver observeValueForKeyPath: aPath
                                ofObject: instance
                                  change: pathInfo->change
                                 context: aContext];
    }
  [iLock unlock];
}

  • 判斷我們的觀察者是否能夠響應(yīng):observeValueForKeyPath:ofObject:change:context:方法。常規(guī)操作俩块,沒有回調(diào)黎休,響應(yīng)就沒有什么意義了!
  • 通過獲取pathInfo來保存KVO信息
  • 中間對context&options的處理數(shù)據(jù)
  • NSKeyValueObservingOptionInitial就會主動發(fā)起一次KVO響應(yīng):observeValueForKeyPath

觀察屬性變化的時候

- (void) willChangeValueForKey: (NSString*)aKey
{
  GSKVOPathInfo *pathInfo;
  GSKVOInfo     *info;

  info = (GSKVOInfo *)[self observationInfo];
  if (info == nil)
    {
      return;
    }

  pathInfo = [info lockReturningPathInfoForKey: aKey];
  if (pathInfo != nil)
    {
      if (pathInfo->recursion++ == 0)
        {
          id    old = [pathInfo->change objectForKey: NSKeyValueChangeNewKey];

          if (old != nil)
            {
              /* We have set a value for this key already, so the value
               * we set must now be the old value and we don't need to
               * refetch it.
               */
              [pathInfo->change setObject: old
                                   forKey: NSKeyValueChangeOldKey];
              [pathInfo->change removeObjectForKey: NSKeyValueChangeNewKey];
            }
          else if (pathInfo->allOptions & NSKeyValueObservingOptionOld)
            {
              /* We don't have an old value set, so we must fetch the
               * existing value because at least one observation wants it.
               */
              old = [self valueForKey: aKey];
              if (old == nil)
                {
                  old = null;
                }
              [pathInfo->change setObject: old
                                   forKey: NSKeyValueChangeOldKey];
            }
          [pathInfo->change setValue:
            [NSNumber numberWithInt: NSKeyValueChangeSetting]
            forKey: NSKeyValueChangeKindKey];

          [pathInfo notifyForKey: aKey ofInstance: [info instance] prior: YES];
        }
      [info unlock];
    }
  [self willChangeValueForDependentsOfKey: aKey];
}

  • 通過pathInfo獲取回之前的舊值
  • pathInfo->change 里面的數(shù)據(jù)處理
  • 重點:[pathInfo notifyForKey: aKey ofInstance: [info instance] prior: YES];開始發(fā)起響應(yīng)通知
- (void) notifyForKey: (NSString *)aKey ofInstance: (id)instance prior: (BOOL)f
{
  unsigned      count;
  id            oldValue;
  id            newValue;

  if (f == YES)
    {
      if ((allOptions & NSKeyValueObservingOptionPrior) == 0)
        {
          return;   // Nothing to do.
        }
      [change setObject: [NSNumber numberWithBool: YES]
                 forKey: NSKeyValueChangeNotificationIsPriorKey];
    }
  else
    {
      [change removeObjectForKey: NSKeyValueChangeNotificationIsPriorKey];
    }

  oldValue = [[change objectForKey: NSKeyValueChangeOldKey] retain];
  if (oldValue == nil)
    {
      oldValue = null;
    }
  newValue = [[change objectForKey: NSKeyValueChangeNewKey] retain];
  if (newValue == nil)
    {
      newValue = null;
    }

  /* Retain self so that we won't be deallocated during the
   * notification process.
   */
  [self retain];
  count = [observations count];
  while (count-- > 0)
    {
      GSKVOObservation  *o = [observations objectAtIndex: count];

      if (f == YES)
        {
          if ((o->options & NSKeyValueObservingOptionPrior) == 0)
            {
              continue;
            }
        }
      else
        {
          if (o->options & NSKeyValueObservingOptionNew)
            {
              [change setObject: newValue
                         forKey: NSKeyValueChangeNewKey];
            }
        }

      if (o->options & NSKeyValueObservingOptionOld)
        {
          [change setObject: oldValue
                     forKey: NSKeyValueChangeOldKey];
        }

      [o->observer observeValueForKeyPath: aKey
                                 ofObject: instance
                                   change: change
                                  context: o->context];
    }

  [change setObject: oldValue forKey: NSKeyValueChangeOldKey];
  [oldValue release];
  [change setObject: newValue forKey: NSKeyValueChangeNewKey];
  [newValue release];
  [self release];
}

  • change里面值的處理完畢之后
  • 讓我們的觀察者響應(yīng)實現(xiàn)的KVO回調(diào)方法: [o->observer observeValueForKeyPath: aKey ofObject: instance change: change context: o->context];
  • 完美看到響應(yīng)回調(diào)玉凯,舒服

移除觀察者

移除觀察的流程相對來說势腮,比較簡單了,但是優(yōu)秀的我還是愿意和大家一起探索

- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
{
  GSKVOInfo *info;
  id            forwarder;

  /*
   * Get the observation information and remove this observation.
   */
  info = (GSKVOInfo*)[self observationInfo];
  forwarder = [info contextForObserver: anObserver ofKeyPath: aPath];
  [info removeObserver: anObserver forKeyPath: aPath];
  if ([info isUnobserved] == YES)
    {
      /*
       * The instance is no longer being observed ... so we can
       * turn off key-value-observing for it.
       */
      object_setClass(self, [self class]);
      IF_NO_GC(AUTORELEASE(info);)
      [self setObservationInfo: nil];
    }
  if ([aPath rangeOfString:@"."].location != NSNotFound)
    [forwarder finalize];
}

  • 拿回我們observationInfo就是我們信息收集者
  • 利用NSMapRemove(paths, (void*)aPath)移除
  • 動態(tài)子類的isa和原類的isa切換回來
  • 把當前設(shè)置的info置空

OK 完美解析了KVO底層源碼捎拯!我們在探索完KVO底層實現(xiàn)才能說是真正的掌握了,而不是通過面試寶典背下結(jié)論盲厌,那是沒有什么意義! 在真正的高手對決間一眼就能看出署照,中間忽略了一些小細節(jié),比如set的多種情況吗浩,setNumber類型建芙,setInt類型, setLong類型....我相信聰明的你一樣可以解析讀懂!


對于以上技術(shù)點懂扼,想要更好的探討禁荸,可以進入iOS技術(shù)圈右蒲,一起探討學習

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赶熟,隨后出現(xiàn)的幾起案子瑰妄,更是在濱河造成了極大的恐慌,老刑警劉巖映砖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件间坐,死亡現(xiàn)場離奇詭異,居然都是意外死亡啊央,警方通過查閱死者的電腦和手機眶诈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瓜饥,“玉大人,你說我怎么就攤上這事浴骂∨彝粒” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵溯警,是天一觀的道長趣苏。 經(jīng)常有香客問我,道長梯轻,這世上最難降的妖魔是什么食磕? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮喳挑,結(jié)果婚禮上彬伦,老公的妹妹穿的比我還像新娘。我一直安慰自己伊诵,他們只是感情好单绑,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著曹宴,像睡著了一般搂橙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上笛坦,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天区转,我揣著相機與錄音,去河邊找鬼版扩。 笑死废离,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的资厉。 我是一名探鬼主播厅缺,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了湘捎?” 一聲冷哼從身側(cè)響起诀豁,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎窥妇,沒想到半個月后舷胜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡活翩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年烹骨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片材泄。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡沮焕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拉宗,到底是詐尸還是另有隱情峦树,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布旦事,位于F島的核電站魁巩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏姐浮。R本人自食惡果不足惜谷遂,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望卖鲤。 院中可真熱鬧肾扰,春花似錦、人聲如沸扫尖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽换怖。三九已至甩恼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間沉颂,已是汗流浹背条摸。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留铸屉,地道東北人钉蒲。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像彻坛,于是被迫代替她去往敵國和親顷啼。 傳聞我的和親對象是個殘疾皇子踏枣,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

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