iOS底層原理之KVO分析(上)

什麼是KVO?

KVO是Key-Value Observing的簡寫,稱爲鍵值觀察,用來監聽對象屬性的變化markdown

KVO使用探究

咱們以IFPerson類爲例研究KVO的使用。函數

自動觸發KVO監聽

  • 主要代碼
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.person.name = @"kvo";
    NSLog(@"class = %@",object_getClass(self.person));
    //對person的name添加kvo監聽
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    NSLog(@"class = %@",object_getClass(self.person));
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.name = [NSString stringWithFormat:@"%@ +",self.person.name];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@" === %@",change);
    }
}

-(void)dealloc {
    //移除監聽
    [self.person removeObserver:self forKeyPath:@"name"];
}
複製代碼

運行代碼點擊屏幕效果以下spa

手動觸發KVO監聽

若是想要手動觸發KVO監聽,須要重寫+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key函數,該函數默認返回YES,表示自動觸發,返回NO,則表示手動觸發。對於本文,咱們判斷下key == name則手動觸發,不然自動觸發。修改代碼以下3d

//在`IFPerson.m`中
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}
-(void)setName:(NSString *)name {
    [self willChangeValueForKey:name];
    _name = name;
    [self didChangeValueForKey:name];
}
複製代碼

從上述代碼中,咱們能夠看到,要實現手動觸發KVO,有兩個步驟須要實現指針

  • 步驟一:重寫+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key函數,根據須要返回NO
  • 步驟二:對於監聽的對象屬性發生改變時,須要在改變先後調用willChangeValueForKey:didChangeValueForKey:

KVO底層原理探究

從上述中咱們能夠看到咱們能夠根據KVO監聽屬性的變化,那麼在底層是怎麼實現的呢?code

KVO監聽先後對象的變化

咱們在監聽name先後獲取personclass以及其指針地址看下是否有變化,代碼以下orm

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.person.name = @"kvo";
    NSLog(@"class = %@ address = %p",object_getClass(self.person),self.person);
    //對person的name添加kvo監聽
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    NSLog(@"class = %@ address = %p",object_getClass(self.person),self.person);
}
複製代碼

打印結果以下從上圖中咱們看到,self.personclass先後發生了改變,可是對象地址並無改變。咱們再經過獲取isa來驗證下。server

  • 監聽以前
  • 監聽以後
  • 監聽先後總結:從上面分析可知,在進行KVO監聽後,對象的class即isa指向發生了改變,由IFPerson變成了NSKVONotifying_IFPerson,可是對象的地址沒有改變

NSKVONotifying_IFPerson是什麼?

咱們知道了在進行KVO監聽後class變成了NSKVONotifying_IFPerson,那麼NSKVONotifying_IFPerson從何而來?下面咱們看下NSKVONotifying_IFPerson的父類是誰。對象

準備工做

因爲superClass是隱藏屬性,在外部沒法訪問,由於咱們定義一個結構體,並強轉下類型來得到superClass,代碼以下繼承

struct cw_objc_class {
    Class _Nonnull isa;
    Class _Nullable super_class;
};

// 強轉一下類型
    struct cw_objc_class *newClass = (__bridge struct cw_objc_class *)object_getClass(self.person);
    NSLog(@"superClass = %@",newClass->super_class);
複製代碼

從打印結果來看NSKVONotifying_IFPerson父類居然是IFPerson

NSKVONotifying_IFPerson類實現了哪些方法?

既然runtime在進行KVO監聽的時候派生了NSKVONotifying_IFPerson類,那麼NSKVONotifying_IFPerson類跟IFPerson類實現方法有什麼區別呢?咱們打印下監聽先後兩個類的實現方法區別

#pragma mark - 遍歷方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}
NSLog(@"監聽以前方法:");
    [self printClassAllMethod:object_getClass(self.person)];
    
    //對person的name添加kvo監聽
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    NSLog(@"監聽以前方法:");
    [self printClassAllMethod:object_getClass(self.person)];
複製代碼

打印結果以下從打印監聽先後方法列表咱們能夠看到NSKVONotifying_IFPerson類有以下變化

  • 重寫了被監聽屬性的setter方法,如setName:
  • 修改了isa指針指向的地址,由原來的IFPerson指向了NSKVONotifying_IFPerson
  • 實現了dealloc方法
  • 增長了一個_isKOA標識符

isa指針什麼時候從新指向IFPerson

既然isa指針在被監聽後指向了NSKVONotifying_IFPerson類,那麼什麼時候從新指向IFPerson呢?答案是在移除監聽的時候。爲了驗證,咱們在dealloc中打印下監聽先後isa指針的變化。

KVO總結

  • KVO在監聽屬性後,runtime會根據對象的類型生成一個NSKVONotifying_XXX(XXX是原來的類)派生類,該派生類NSKVONotifying_XXX繼承於原來的類
  • NSKVONotifying_XXX重寫被監聽屬性的setter方法,可是賦值的時候經過重寫的setter內部消息轉發仍是經過原來類的setter方法來實現
  • KVO在移除監聽時將isa指針指向地址由NSKVONotifying_XXX派生類從新指向原來類
相關文章
相關標籤/搜索