你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> KVO進階 —— 源碼實現探究

KVO進階 —— 源碼實現探究

編輯:IOS開發基礎

本篇會對KVO的實現進行探究,不涉及太多KVO的使用方法,但是會有一些使用時的思考。

一、使用上的疑問

1.keyPath是什麼

當我們使用@property時候,keyPath是指的是我們的屬性名,實例變量或者是存取方法?

???? 對一個屬性值使用@synthesize重新定義了存儲變量

# import "Person.h"

@interface Student : Person 

@property (nonatomic, strong) NSString* mark;

@end
@implementation Student

@synthesize mark = abc;

- (void)setMark:(NSString *)newMark {

    abc = newMark;
    
}
- (NSString *)mark {
    
    return abc;
}

main() {
    Student *stu = [[Student alloc] init];
    
    stu.mark = @"65";
    
    StudentKvoObserver *stuObserver = [[StudentKvoObserver alloc] initWithStudent:stu];
    
    [stuObserver addObserverForKeyPath:@"mark"];   // 重命名get方法
    
    stu.mark = @"85";
}

實際結果是,能夠監聽到mark值的變化,反之,我將mark替換成真正的實例變量abc時,無法獲取狀態。

現在想想其實答案早就存在了,我們不做顯示的@synthesize的指定時,其實等價於@synthesize mark = _mark;,由此看來keyPath實際指的並不是真正存儲你數據的變量。

2.KVO是否能夠繼承

我是否能夠監聽我父類裡的屬性,哪怕他並沒有暴露出來?通過某些手段得(猜)到了keyPath,然後去監聽它甚至是KVC修改他的值。

子類繼承父類的一個屬性,當這個屬性被改變時,KVO能否觀察到?

子類繼承父類的一個未暴露的屬性,當這個屬性被改變時,KVO能否觀察到?

子類繼承父類屬性並重寫了它的setter方法,當這個屬性被改變時,KVO能否觀察到?

// Person類
@interface Person : NSObject

@property (nonatomic, strong) NSString *firstName;

@property (nonatomic, strong) NSString *lastName;

@property (nonatomic, strong, readonly) NSString *fullName;

- (void)setNewInnerName:(NSString *)str;

@end

@interface Person ()

@property (nonatomic,strong) NSString *innerName;

@end

@implementation Person

- (void)setNewInnerName:(NSString *)str {

      self.innerName = str;// 通過get、set訪問  觸發KVO
      
//    [self setValue:str forKey:@"innerName"];// KVC方式,其實調用的也是setter方法 觸發KVO

//    _innerName = str;// 直接訪問成員變量,不觸發KVO
}
// Student類
@interface Student : Person

@end

@implementation Student

- (void)setFirstName:(NSString *)firstName {

    NSLog(@"重寫的setFirstName方法");
}

@end

// 執行文件
main() {
    Person *p = [[Person alloc] init];
    
    p.firstName = @"zhao";
    
    p.lastName = @"zhiyu";
    
    PersonKvoObserver *personKvoObserver = [[PersonKvoObserver alloc] initWithPerson:p];
    
    [personKvoObserver addObserverForKeyPath:@"fullName"];  // 屬性關聯
    
    [personKvoObserver addObserverForKeyPath:@"innerName"]; // 內部屬性
    
    p.firstName = @"zhao1";
    
    [p setNewInnerName:@"newInnerNmame"];// 沒有暴露的屬性的get、set方法被調用時,也會發送通知
    
    // 子類的屬性監聽
    Student *stu = [[Student alloc] init];
    
    stu.firstName = @"stu";
    
    stu.lastName = @"dent";
    
    StudentKvoObserver *stuObserver = [[StudentKvoObserver alloc] initWithStudent:stu];
    
    [stuObserver addObserverForKeyPath:@"fullName"];// 子類繼承屬性依舊被監聽
    
    [stuObserver addObserverForKeyPath:@"firstName"];   // 重寫方法,不加super,依舊會監聽kvo
    
    [stuObserver addObserverForKeyPath:@"innerName"]; 
    
    stu.firstName = @"stu1";
    
    stu.lastName = @"dent1";
    
    [stu setNewInnerName:@"newInnerNmame"];// 沒有暴露的屬性的get、set方法被調用時,也會發送通知
}

通過上面的例子,我們能看出幾點:

①通過KVO,能觀察父類的屬性值。

②只要知道了keyPath,不管有沒有暴露方法,依舊可以通過KVO方式觀察值的變化,而且同屬性一樣,可以被繼承。

③子類重寫父類的set方法,也並不會影響KVO的觀察。

從這兒開始就有點好奇了,這個KVO是否通過子類化的方法實現?那如何讓子類的繼承屬性也能被監聽到?了解到KVO依賴setter方法的重寫,那我子類重寫的setter方法之後,為什麼子類繼承屬性的監聽依然生效?

3.跨線程的監聽

我們知道使用Notification時,跨線程發送通知是無法被接受到的,那麼現在看看KVO在多線程中的表現。

 //  在兩個線程定義目標和觀察者
    dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    
//    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    __block Student *stu1 = nil;
    
    dispatch_async(concurrentQueue, ^{
        // 對象屬性
        stu1 = [[Student alloc] init];
        
        NSLog(@"Student %@",[NSDate new]);
        
        stu1.lastName = @"yyyyyyy";
    });
    
    __block StudentKvoObserver *stuObserver1;
    
    dispatch_async(concurrentQueue, ^{
    
        sleep(2);
        
        stuObserver1 = [[StudentKvoObserver alloc] initWithStudent:stu1];
        
        [stuObserver1 addObserverForKeyPath:@"fullName"];// 子類繼承屬性依舊被監聽
        
        NSLog(@" StudentKvoObserver %@",[NSDate new]);
        
    });
    
    
    dispatch_barrier_async(concurrentQueue, ^{
    
        NSLog(@"dispatch_barrier_async %@",[NSDate new]);
        
        NSLog(@"zzzzzz start%@",[NSDate new]);
        
        stu1.lastName = @"zzzzzz";
        
        NSLog(@"zzzzzz end%@",[NSDate new]);
        
    });

輸出結果

2016-10-11 10:46:53.319 KVCLearn[3364:331572] Student 2016-10-11 02:46:53 +0000
2016-10-11 10:46:55.324 KVCLearn[3364:331578] StudentKvoObserver 2016-10-11 02:46:55 +0000
2016-10-11 10:46:55.325 KVCLearn[3364:331578] dispatch_barrier_async 2016-10-11 02:46:55 +0000
2016-10-11 10:46:55.325 KVCLearn[3364:331578] zzzzzz start2016-10-11 02:46:55 +0000
2016-10-11 10:46:55.326 KVCLearn[3364:331578] fullName
{
kind = 1;
new = “(null)zzzzzz”;
old = “(null)yyyyyyy”;
}
2016-10-11 10:46:55.326 KVCLearn[3364:331578] zzzzzz end2016-10-11 02:46:55 +0000

可以看到在兩個不同的線程裡創建的Observer和Target,觀察變化也是能夠生效的。

這裡有一個關於GCD的問題,這裡我使用了dispatch_barrier_async,分發到自定義的並發隊列上,這時barrier是正常工作的,保證了第三個task在前兩個執行完之後執行。但是當我直接使用系統全局的並發隊列時,barrier不起作用,不能保證他們的執行順序。這裡希望有高人看見了能解答下。

二、實現探究

1.API接口

Foundation裡關於KVO的部分都定義在NSKeyValueObserving.h中,KVO通過以下三個NSObject分類實現。

  • NSObject(NSKeyValueObserving)

  • NSObject(NSKeyValueObserverRegistration)

  • NSObject(NSKeyValueObservingCustomization)

這裡會從NSObject (NSKeyValueObserverRegistration) 的 - addObserver:forKeyPath:options:context: 為入口,去一步步分析如何整個KVO的實現方式。

2.先說結論

實現方式:

一個對象在被調用addObserver方法時,會動態創建一個KVO前綴的原類的子類,用來重寫所有的setter方法,並且該子類的- (Class) class和- (Class) superclass方法會被重寫,返回父類(原始類)的Class。最後會將當前對象的類改為這個KVO前綴的子類。

比較繞,讓我們來看個例子。比如說類Person的實例person調用了addObserver方法時,addObserver方法內部給你創建了一個KVOPerson類,KVOPerson的所有的setter方法會被重寫,它的class和superClass方法會被改寫成返回Person和NSObject,之後使用object_setClass將KVOPerson設置成person的class。

當我們調用person的setName方法時,實際是調用的一個KVOPerson實例的setName方法,但由於重寫了class,在外部看不出來其中的差別。在setter方法中,我們在實際值被改變的前後回調用- (void)willChangeValueForKey:(NSString *)key;和- (void)didChangeValueForKey:(NSString *)key;方法,通知觀察者值的變化。

3.代碼

源碼是來自GNUSetup裡的Foundation,據說和apple的實現類似,只是相關API的版本會比較老一些。我們先從addObserver方法開始。

@implementation NSObject (NSKeyValueObserverRegistration)

- (void) addObserver: (NSObject*)anObserver
      forKeyPath: (NSString*)aPath
         options: (NSKeyValueObservingOptions)options
         context: (void*)aContext
{
  ....
  // 1.使用當前類創建GSKVOReplacement對象 
  r = replacementForClass([self class]);
  ....
  info = (GSKVOInfo*)[self observationInfo];
  if (info == nil)
    {
      info = [[GSKVOInfo alloc] initWithInstance: self];
      [self setObservationInfo: info];
      //2.重新設置class
      object_setClass(self, [r replacement]);
    }
    ....
   //3.重寫replace的setter方法
   [r overrideSetterFor: aPath];
   //4.注冊當前類和觀察者到全局表中
   [info addObserver: anObserver
             forKeyPath: aPath
                options: options
                context: aContext];
}

忽略了一些分支,可以看到主要為上面四個步驟。我們可以一個一個拆開來看。

replacementForClass

// 單例生成一個GSKVOReplacement對象,保證一個類只有一個KVO子類
static GSKVOReplacement *
replacementForClass(Class c)
{
  GSKVOReplacement *r;
  setup();
  [kvoLock lock];
  r = (GSKVOReplacement*)NSMapGet(classTable, (void*)c);
  if (r == nil)
    {
      r = [[GSKVOReplacement alloc] initWithClass: c];
      NSMapInsert(classTable, (void*)c, (void*)r);
    }
  [kvoLock unlock];
  return r;
}
- (id) initWithClass: (Class)aClass
{
  NSValue       *template;
  NSString      *superName;
  NSString      *name;
  original = aClass;
  superName = NSStringFromClass(original);
  name = [@"GSKVO" stringByAppendingString: superName];// 添加前綴
  template = GSObjCMakeClass(name, superName, nil);// 通過objc_allocateClassPair得到class指針
  GSObjCAddClasses([NSArray arrayWithObject: template]);// objc_registerClassPair注冊class
  replacement = NSClassFromString(name);// 前面動態生成且注冊了GSKVO子類,然後就可以通過該方法得到
// 添加模板類的一些方法,包括重寫class和superClass讓對象類型不暴露,
// setValue:forkey在數據改變前後加上willChange和didChange方法 
  GSObjCAddClassBehavior(replacement, baseClass);
  /* Create the set of setter methods overridden.
   */
  keys = [NSMutableSet new];
  return self;
}

object_setClass(self, [r replacement]);

// replace就是新生成的KVOXXX的class
@interface  GSKVOReplacement : NSObject
{
  Class         original;       /* The original class */
  Class         replacement;    /* The replacement class */
  NSMutableSet  *keys;          /* The observed setter keys */
}
replacement = NSClassFromString(name);// 在initWithClass方法中賦值

overrideSetterFor

重寫setter方法,在值改變前後添加上willChange&didChange
- (void) overrideSetterFor: (NSString*)aKey
{
  if ([keys member: aKey] == nil)
    {
      NSMethodSignature *sig;// 當前key值對應setter的方法簽名
      SEL       sel;// 當前key值對應setter的方法名selector
      IMP       imp;// 當前key值對應setter的函數指針IMP
      const char    *type;
      NSString          *a[2];
      unsigned          i;
      BOOL              found = NO;
      
      // 得到setXxxx:和_setXxxx:方法名
      a[0] = [NSString stringWithFormat: @"set%@%@:", tmp, suffix];
      a[1] = [NSString stringWithFormat: @"_set%@%@:", tmp, suffix];
      for (i = 0; i < 2; i++)
        {
          /*
             得到方法簽名
           */
          sel = NSSelectorFromString(a[i]);
          sig = [original instanceMethodSignatureForSelector: sel];
          type = [sig getArgumentTypeAtIndex: 2];// 第三個參數即入參的類型
          switch (*type)
            {
              // 字符
              case _C_CHR:
              case _C_UCHR:
                imp = [[GSKVOSetter class]
                  instanceMethodForSelector: @selector(setterChar:)];// 返回setterChar:函數的函數指針IMP
                break;
              // 對象、類、指針
              case _C_ID:
              case _C_CLASS:
              case _C_PTR:
                imp = [[GSKVOSetter class]
                  instanceMethodForSelector: @selector(setter:)];// 返回setter:函數的函數指針IMP,後面有詳解
                break;
                break;
              ....
                
              default:
                imp = 0;
                break;
            }
          if (imp != 0)
            {
          if (class_addMethod(replacement, sel, imp, [sig methodType]))// 將原sel和新imp加到replacement類中去
        {
                  found = YES;
        }
          else
        {
          NSLog(@"Failed to add setter method for %s to %s",
            sel_getName(sel), class_getName(original));
        }
            }
        }
      if (found == YES)
        {
          [keys addObject: aKey];
        }
    }
}

這個步驟是將keypath對應的setter方法重寫找出來,把原有的SEL函數名和重寫後的實現IMP加入到子類中去。這樣做,新生成的子類就有和原父類一樣表現了,再加上之前的class替換,在KVO的對外接口上已經沒有差別。這裡也解釋了我一開始的問題,keypath到底指的是什麼,其實是setter方法,或者說方法名的後綴。因為我們用@property生成了默認的set方法是滿足規范的,所以會將keypath和property關聯起來。

// setter方法的實現細節
@implementation GSKVOSetter
- (void) setter: (void*)val
{
  NSString  *key;
  Class     c = [self class];
  void      (*imp)(id,SEL,void*);
  imp = (void (*)(id,SEL,void*))[c instanceMethodForSelector: _cmd];
  key = newKey(_cmd);
  if ([c automaticallyNotifiesObserversForKey: key] == YES)
    {
      // pre setting code here
      [self willChangeValueForKey: key];
      (*imp)(self, _cmd, val);
      // post setting code here
      [self didChangeValueForKey: key];
    }
  else
    {
      (*imp)(self, _cmd, val);
    }
  RELEASE(key);
}

對於這個setter方法的實現,我其實是沒大看懂的。[c instanceMethodForSelector: _cmd];這個取到的imp,應該是當前方法的函數指針(GSKVOSetter的setter),後面也是直接調用的該imp實現。沒有找到這個setter是如何和原類方法中實際的setter聯系起來的,之前通過sig方法簽名也只取出了sel,原有實現並沒有出現。希望有大牛看到這個能給我解答一下。

-(void) addObserver: forKeyPath: options: context:

這個部分就是觀察者的注冊了。通過以下類圖可以很方便得看到,所有的類的KVO觀察都是通過infoTable管理的。以被觀察對象實例作key,GSKVOInfo對象為value的形式保存在infoTable表裡,每個被觀察者實例會對應多個keypath,每個keypath會對應多個observer對象。順帶提一下,關於Notification的實現也類似,也是全局表維護通知的注冊監聽者和通知名。

GSKVOInfo的結構可以看出來,一個keyPath可以對應有多個觀察者。其中觀察對象的實例和option打包成GSKVOObservation對象保存在一起。

905873-60fba3a70616920e.jpg

三、總結

看完了KVO的實現部分,我們再回過頭來看開頭提到的幾個問題。

keyPath是什麼

首先keyPath,是對於setter方法的關聯,會使用keypath作為後綴去尋找原類的setter方法的方法簽名,和實際存取對象和property名稱沒有關系。所以這也是為什麼我們重命名了setter方法之後,沒有辦法再去使用KVO或KVC了,需要手動調用一次willChangeValue方法。

子類繼承父類的一個屬性,當這個屬性被改變時,KVO能否觀察到?

因為繼承的關系Father <- Son <- KVOSon,當我監聽一個父類屬性的keyPath的時候,Son實例同樣可以通過消息查找找到父類的setter方法,再將該方法加入到KVOSon類當中去。

子類繼承父類的一個未暴露的屬性,當這個屬性被改變時,KVO能否觀察到?

由於在overrideSetterFor中,我們是直接通過sel去得到方法簽名signature,所以和暴不暴露沒啥關系。

子類繼承父類屬性並重寫了它的setter方法,當這個屬性被改變時,KVO能否觀察到?

在上一條中知道,其實子類監聽父類屬性,並不依賴繼承,而是通過ISA指針在消息轉發的時候能夠獲取到父類方法就足夠。所以當我們重寫父類setter方法,相當於在子類定義了該setter函數,在我們去用sel找方法簽名時,直接在子類中就拿到了,甚至都不需要去到父類裡。所以理解了KVO監聽父類屬性和繼承沒有直接聯系這一點,就不再糾結set方法是否重寫這個問題了。

最後線程安全的部分,沒有做深入的研究,在這篇就不多做表述了。在我貼的源碼中都去掉了很多枝葉,其中就包括加鎖的部分。有興趣的朋友可以去下面貼的源碼地址去看完整版,其中對線程安全的考慮,遞歸鎖、惰性遞歸鎖使用,也是很值得學習的。

例子和源碼的資料

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved