OC底層原理二十三:KVO原理

OC底層原理 學(xué)習(xí)大綱

上一節(jié),我們介紹了KVC原理,而KVC工作,絕大部分通過(guò)繼承NSObject自動(dòng)處理好了。實(shí)際應(yīng)用中,我們關(guān)注相對(duì)較少。而基于KVCKVO,在應(yīng)用中卻是非常的廣泛。

現(xiàn)在我們使用的響應(yīng)式框架(RAC、RxSwif、Combine等),實(shí)際都是KVO機(jī)制的應(yīng)用。

本節(jié),我們?cè)敿?xì)講解KVO:

  1. KVO介紹
  2. KVO應(yīng)用
  3. KVO原理

引入:

  • 我們上一節(jié)分析KVC時(shí),官方對(duì)KVC的應(yīng)用中,第一個(gè)介紹的就是KVO
    ?? KVC文檔鏈接
    image.png

我們點(diǎn)擊進(jìn)入Key-Value Observing Programming Guide (KVO指引)


1. KVO介紹

KVO,全稱(chēng)為Key-value observing鍵值觀察。

  • 鍵值觀察是一種機(jī)制,允許對(duì)象其他對(duì)象指定屬性發(fā)生更改時(shí)得到通知

2 KVO應(yīng)用:

  • 測(cè)試代碼:(監(jiān)聽(tīng)person對(duì)象的name屬性的新值
// HTPerson
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation HTPerson
@end

// ViewController
@interface ViewController ()
@property (nonatomic, strong) HTPerson *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [HTPerson new];
    self.person.name = @"ht";
    
    // 1. 添加
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
}

// 2. 監(jiān)聽(tīng)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString: @"name"]) {
        NSLog(@"新值:%@", change[NSKeyValueChangeNewKey]);
    }
}

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

-(void)dealloc {
    // 3. 移除
    [self.person removeObserver:self forKeyPath:@"name" context: NULL];
}

@end
  • 這里為了測(cè)試,在touchesBegan點(diǎn)擊事件中添加了name的變更,多次點(diǎn)擊,打印結(jié)果如下:
    image.png

主要步驟: 1. 添加 -> 2. 監(jiān)聽(tīng) ->3. 移除

2.1 添加

  • addObserver 添加操作中,addObserver是監(jiān)聽(tīng)對(duì)象KeyPath是監(jiān)聽(tīng)路徑,option是監(jiān)聽(tīng)類(lèi)型,是一個(gè)枚舉,包含:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew     // 新值
    NSKeyValueObservingOptionOld     // 舊值
    NSKeyValueObservingOptionInitial // 初始值 
    NSKeyValueObservingOptionPrior   // 變化前
};

面試官:添加通知時(shí),context寫(xiě)什么內(nèi)容?
答:填nil
面試官:回去等通知 ??

  • 關(guān)于context的介紹:
image.png
  • 照顧英語(yǔ)不好的同學(xué),我們放上谷歌翻譯

    image.png

  • context的類(lèi)型為(void *),所以不能寫(xiě)nil。但可以寫(xiě)成Null。
    如果寫(xiě)Null,會(huì)默認(rèn)通過(guò)KeyPath路徑去確定需要監(jiān)聽(tīng)的對(duì)象。但是這種方法可能導(dǎo)致父類(lèi)由于不同原因觀察相同的路徑,而產(chǎn)生問(wèn)題。而且查詢(xún)父類(lèi),消耗的計(jì)算資源更多。

所以蘋(píng)果建議我們可以static void*創(chuàng)建靜態(tài)的context,這樣的好處是:

    1. 僅從本類(lèi)中查找當(dāng)前context節(jié)省計(jì)算資源,更安全;
    1. observeValueForKeyPath監(jiān)聽(tīng)對(duì)象時(shí),我們可以不再通過(guò)name去區(qū)分當(dāng)前響應(yīng)對(duì)象。而是使用context精準(zhǔn)區(qū)分當(dāng)前響應(yīng)對(duì)象:
      image.png
    1. removeObserver移除對(duì)象時(shí),可以通過(guò)context精準(zhǔn)移除觀察對(duì)象:
      image.png

2.2 監(jiān)聽(tīng)

  • observeValueForKeyPath 監(jiān)聽(tīng)當(dāng)前控制器的所有變化,change有以下4種情況:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};
  • keyPathobject、context和上述一樣。

2.3 移除

一定要移除! 一定要移除! 一定要移除!

網(wǎng)上有很多說(shuō)Xcode升級(jí)后,不再需要手動(dòng)移除監(jiān)聽(tīng)者。僅僅在當(dāng)前頁(yè)面操作時(shí),確實(shí)不用處理。

  • 但如果業(yè)務(wù)變得復(fù)雜,對(duì)于同一對(duì)象屬性,如果當(dāng)前頁(yè)面進(jìn)行了添加、監(jiān)聽(tīng)和移除,而其他頁(yè)面只進(jìn)行添加和監(jiān)聽(tīng),再觸發(fā)監(jiān)聽(tīng)時(shí),就會(huì)產(chǎn)生KVO Crash。所以我們要養(yǎng)成誰(shuí)使用誰(shuí)銷(xiāo)毀的習(xí)慣。

2.4 開(kāi)關(guān)

  • automaticallyNotifiesObserversForKey控制自動(dòng)手動(dòng)發(fā)送通知,默認(rèn)值為自動(dòng) ?? 官方介紹
image.png
  • 當(dāng)我們給HTPerson添加automaticallyNotifiesObserversForKey方法,返回值為NO后,所有監(jiān)聽(tīng)消息都不再發(fā)送。
  • 我們重寫(xiě)屬性setter方法,手動(dòng)調(diào)用API進(jìn)行消息發(fā)送:
@implementation HTPerson
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}

-(void)setName:(NSString *)name {
    
    if ([self.name isEqualToString: name]) return; // 值沒(méi)變化,不操作
    
    [self willChangeValueForKey:@"name"]; // 即將改變
    _name = name; // 賦值
    [self didChangeValueForKey:@"name"]; // 已改變
}
@end

2.5 路徑處理

  • 我們已downloadProgress下載進(jìn)度為例,下載進(jìn)度等于writtenData已下載數(shù)據(jù)量 / totalData總數(shù)據(jù)量。

  • 我們可以聚合writtenDatatotalData兩個(gè)屬性,變成監(jiān)聽(tīng)downloadProgress一個(gè)屬性。

// HTPerson
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@end

@implementation HTPerson

- (NSString *)downloadProgress {
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [NSString stringWithFormat:@"%.2f", 1.0f * self.writtenData / self.totalData];
}

// 下載進(jìn)入 writtenData / totalData
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray * affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
@end

// ViewController
@interface ViewController ()
@property (nonatomic, strong) HTPerson *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [HTPerson new];

    // 添加監(jiān)聽(tīng)
    [self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context: NULL];
    
}

// 處理監(jiān)聽(tīng)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString: @"downloadProgress"]) {
        NSLog(@"當(dāng)前進(jìn)度:%@", change[NSKeyValueChangeNewKey]);
    }
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.writtenData += 20;
    self.person.totalData +=10;
}

-(void)dealloc {
    // 移除監(jiān)聽(tīng)
    [self.person removeObserver:self forKeyPath:@"downloadProgress" context: NULL];
}

@end
  • 打印結(jié)果:


    image.png
  • 這里將writtenDatatotalData都變化了,可以看到每次打印的是2條記錄。說(shuō)明聚合的downloadProgress中,只要writtenDatatotalData值變化,都會(huì)觸發(fā)一次。
    (如果你用過(guò)RACRxSwift,此處一定非常熟悉,這就是RxSwift的merge的原理。)

  • 相關(guān)原理: ?? 官方鏈接

2.6 數(shù)組的觀察

  • 集合等類(lèi)型的監(jiān)聽(tīng),與屬性的監(jiān)聽(tīng)不同。我們可以查閱KVC的官方文檔
image.png
// HTPerson
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSMutableArray *dateArray;
@end

@implementation HTPerson

@end

// ViewController
@interface ViewController ()
@property (nonatomic, strong) HTPerson *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [HTPerson new];

    self.person.name = @"ht ";
    self.person.dateArray = [NSMutableArray new];
    
    // 添加監(jiān)聽(tīng)
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
    [self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context: NULL];
}

// 處理監(jiān)聽(tīng)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@: %@", keyPath, change);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.name = [NSString stringWithFormat:@"%@+", self.person.name];
//    [self.person.dateArray addObject:@"6"]; //此賦值僅改變數(shù)組內(nèi)部元素,不會(huì)引起數(shù)組地址的變化
    [[self.person mutableArrayValueForKeyPath:@"dateArray"] insertObject:@"6" atIndex:0];
    [[self.person mutableArrayValueForKeyPath:@"dateArray"] setObject:@"8" atIndexedSubscript:0];
    [[self.person mutableArrayValueForKeyPath:@"dateArray"] removeObjectAtIndex:0];
}

-(void)dealloc {
    // 移除監(jiān)聽(tīng)
    [self.person removeObserver:self forKeyPath:@"name" context: NULL];
    [self.person removeObserver:self forKeyPath:@"dateArray" context: NULL];
}

@end
  • 打印結(jié)果:
image.png
  • 數(shù)組設(shè)值,必須使用專(zhuān)屬API才可以觸發(fā)。直接賦值僅改變數(shù)組內(nèi)部元素不會(huì)引起數(shù)組地址變化。

  • 從打印的結(jié)果上,change4種情況:

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};
image.png

3. KVO底層原理

3.1 KVO只觀察Setter方法

  • 我們先觀察一個(gè)案例,此案例有public聲明的nickName成員變量和@property定義的屬性name,分別監(jiān)聽(tīng)這2個(gè)屬性值:
  • 測(cè)試代碼:
// HTPerson
@interface HTPerson : NSObject
{
@public NSString * nickName;
}
@property (nonatomic, copy) NSString *name;
@end

@implementation HTPerson

@end

// ViewController
@interface ViewController ()
@property (nonatomic, strong) HTPerson *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [HTPerson new];

    self.person.name = @"ht ";
    
    // 添加監(jiān)聽(tīng)
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context: NULL];
}

// 處理監(jiān)聽(tīng)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@: %@", keyPath, change);
}

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

-(void)dealloc {
    // 移除監(jiān)聽(tīng)
    [self.person removeObserver:self forKeyPath:@"name" context: NULL];
    [self.person removeObserver:self forKeyPath:@"nickName" context: NULL];
}

@end
  • 打印結(jié)果:


    image.png
  • 發(fā)現(xiàn)只監(jiān)聽(tīng)屬性的變化,而監(jiān)聽(tīng)不到成員變量的變化。而屬性成員變量區(qū)別,核心在于是否實(shí)現(xiàn)setter方法。

3.2 KVO派生類(lèi)

  • addObserver處打上斷點(diǎn),運(yùn)行到斷點(diǎn)處后,打印當(dāng)前person類(lèi):

    image.png

  • 驚奇的發(fā)現(xiàn),使用運(yùn)行時(shí)object_getClassName讀取的self.person類(lèi),是NSKVONotifying_HTPerson類(lèi)。而使用self.person.class直接打印的類(lèi),確是HTPerson類(lèi)。

  • 好像發(fā)現(xiàn)了一些不可告人的密碼 ?? 蘋(píng)果金屋藏嬌生成了個(gè)NSKVONotifying_HTPerson,但又故意的不讓外部知道,所以調(diào)用class方法,打印的還是HTPerson類(lèi)

  • 當(dāng)然,從開(kāi)發(fā)的層面,可以理解,對(duì)外事務(wù)越簡(jiǎn)單越好,高內(nèi)聚,減輕開(kāi)發(fā)人員學(xué)習(xí)使用成本。不過(guò)對(duì)于我們現(xiàn)在探究底層原理而言,就想知道這個(gè)NSKVONotifying_HTPerson是什么。

3.2.1 NSKVONotifying_HTPersonHTPerson什么關(guān)系?

  • 導(dǎo)入#import <objc/runtime.h>,添加打印本類(lèi)所有子類(lèi)的方法:
/// 遍歷本類(lèi)及子類(lèi)
-(void) printClasses: (Class)cls {
    
    // 注冊(cè)類(lèi)的總數(shù)
    int count = objc_getClassList(NULL, 0);
    // 創(chuàng)建1個(gè)數(shù)組
    NSMutableArray * mArray = [NSMutableArray arrayWithObject:cls];
    //獲取所有已注冊(cè)的類(lèi)
    Class * classes = (Class *)malloc(sizeof(Class) * count);
    objc_getClassList(classes, count);
    for (int i = 0; i < count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes: %@",mArray);
}
  • addObserver前后前后打印self.person類(lèi)
image.png
  • 觀察到添加觀察者后,HTPerson多了一個(gè)NSKVONotifying_HTPerson子類(lèi)。

我們添加遍歷IvarsProperty、Method的函數(shù):

/// 遍歷Ivars
-(void) printIvars: (Class)cls {
    
    // 仿寫(xiě)Ivar結(jié)構(gòu)
    typedef struct HT_ivar_t {
        int32_t *offset;
        const char *name;
        const char *type;
        uint32_t alignment_raw;
        uint32_t size;
    }HT_ivar_t;

    // 記錄函數(shù)個(gè)數(shù)
    unsigned int count = 0;
    // 讀取函數(shù)列表
    Ivar * ivars = class_copyIvarList(cls, &count);
    for (int i = 0; i < count; i++) {
        HT_ivar_t * ivar = (HT_ivar_t *) ivars[i];
        NSLog(@"ivar: %@", [NSString stringWithUTF8String: ivar->name]);
    }
    free(ivars);
    
}

/// 遍歷屬性
-(void) printProperties: (Class)cls {
    
    // 仿寫(xiě)objc_property_t結(jié)構(gòu)
    typedef struct Ht_property_t{
        const char *name;
        const char *attributes;
    }Ht_property_t;

    // 記錄函數(shù)個(gè)數(shù)
    unsigned int count = 0;
    // 讀取函數(shù)列表
    objc_property_t * props = class_copyPropertyList(cls, &count);
    for (int i = 0; i < count; i++) {
        Ht_property_t * prop = (Ht_property_t *)props[i];
        NSLog(@"property: %@", [NSString stringWithUTF8String:prop->name]);
    }
    free(props);
    
}

/// 遍歷方法
-(void) printMethodes: (Class)cls {
    
    // 記錄函數(shù)個(gè)數(shù)
    unsigned int count = 0;
    // 讀取函數(shù)列表
    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(@"method: %@-%p", NSStringFromSelector(sel), imp);
    }
    free(methodList);
}
  • addObserver處添加打印代碼,分別檢查HTperson本類(lèi)和NSKVONotifying_HTPerson派生類(lèi)的Ivars、PropertyMethod
    // 添加監(jiān)聽(tīng)
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context: NULL];
    NSLog(@"------- NSKVONotifying_HTPerson --------");
    [self printMethodes: objc_getClass("NSKVONotifying_HTPerson")];
    [self printIvars: objc_getClass("NSKVONotifying_HTPerson")];
    [self printProperties: objc_getClass("NSKVONotifying_HTPerson")];
    NSLog(@"------- HTPerson --------");
    [self printMethodes: HTPerson.class];
    [self printIvars: HTPerson.class];
    [self printProperties: HTPerson.class];
  • 打印結(jié)果:
image.png

拓展:

  • 檢驗(yàn)同樣繼承HTPerosn的子類(lèi)HTStudent,打印結(jié)果:
// HTStudent
@interface HTStudent : HTPerson
@end
@implementation HTStudent
@end
image.png

結(jié)論:

  • 直接繼承的子類(lèi),沒(méi)有任何方法屬性??梢源_定:
    KVO派生類(lèi)繼承自HTPerosn,重寫(xiě)了setName、class、dealloc方法,新增了_isKVOA方法

3.2.2 KVO派生類(lèi)給父類(lèi)屬性賦值

  • addObserver處添加斷點(diǎn),運(yùn)行代碼到此處時(shí),lldb輸入:watchpoint set variable self->_person->_name
image.png
  • 設(shè)置成功后,運(yùn)行代碼,點(diǎn)擊屏幕觸發(fā)touchesBegan事件,會(huì)進(jìn)入匯編頁(yè)面(觀察到設(shè)置屬性斷點(diǎn)處)
image.png
image.png
  • 可以觀察到,當(dāng)派生類(lèi)在調(diào)用willChangedidChange中間,調(diào)用了[HTPerson setName]方法,完成了給父類(lèi)HTPersonname屬性賦值。(此時(shí)的willChange和didChange方法是繼承自NSObject的)

3.2.3 KVO派生類(lèi)何時(shí)移除,是否真移除?

  • 我們?cè)?code>removeObserver處加入斷點(diǎn),分別在removeObserver前后使用object_getClassName()打印當(dāng)前isa指向的類(lèi)

    image.png

  • 發(fā)現(xiàn)NSKVONotifying_HTPerson在外部removeObserver時(shí),完成的移除操作。將isa指回了原類(lèi)。

  • 但是我們?cè)?code>removeObserver移除操作之后,打印HTPerosn類(lèi)和子類(lèi)的信息,發(fā)現(xiàn)NSKVONotifying_HTPerson派生類(lèi)并沒(méi)有移除

ps: 頁(yè)面銷(xiāo)毀之后再打印HTPerosn類(lèi)和子類(lèi),也一樣存在NSKVONotifying_HTPerson派生類(lèi)。

  • KVO派生類(lèi)只要生成,就會(huì)一直存在,這樣可以減少頻繁的添加操作

至此,我們已經(jīng)知道KVO是創(chuàng)建派生類(lèi)實(shí)現(xiàn)了鍵值觀察。

    1. 添加:addObserver時(shí),創(chuàng)建了派生類(lèi),派生類(lèi)是當(dāng)前類(lèi)的子類(lèi),重寫(xiě)被監(jiān)聽(tīng)屬性setter方法,并將當(dāng)前類(lèi)isa指向派生類(lèi)
      此時(shí)開(kāi)始,所有調(diào)用本類(lèi)的方法,都是調(diào)用的派生類(lèi)。派生類(lèi)沒(méi)有的方法,就會(huì)沿著繼承鏈查詢(xún)到本類(lèi)
    1. 賦值: 派生類(lèi)重寫(xiě)了被監(jiān)聽(tīng)屬性的setter方法,在派生類(lèi)setter方法觸發(fā)時(shí):在willChange之后,didChange之前,調(diào)用父類(lèi)屬性settter方法,完成父類(lèi)屬性的賦值`。
    1. 移除: 在removeObserver后,isa派生類(lèi)指回本類(lèi)。 但創(chuàng)建過(guò)的派生類(lèi),不會(huì)被本類(lèi)從子類(lèi)列表移除,會(huì)一直存在。
    1. 假象: 之所以外部打印class永遠(yuǎn)看不到派生類(lèi),是因?yàn)?code>派生類(lèi)將class方法重寫(xiě)了,故意不讓外界看到。
      (知道越多,煩惱越多 ?? ,就讓派生類(lèi)做個(gè)默默付出的無(wú)名英雄吧)

下一節(jié),我們純代碼自定義KVO。(簡(jiǎn)化版,重在理解派生類(lèi)流程功能

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容