Objective-C 之 KVO 原理

鍵值觀察提供了一種機(jī)制,該機(jī)制允許將其他對象的特定屬性的更改通知給對象。對于應(yīng)用程序中模型層和控制器層之間的通信特別有用。 (在OS X中,控制器層綁定技術(shù)在很大程度上依賴于鍵值觀察。)控制器對象通常觀察模型對象的屬性,而視圖對象通過控制器觀察模型對象的屬性。但是,此外,模型對象可能會(huì)觀察其他模型對象(通常是確定從屬值何時(shí)更改),甚至是自身(再次確定從屬值何時(shí)更改)。
您可以觀察到一些屬性,包括簡單屬性,一對一關(guān)系和一對多關(guān)系。一對多關(guān)系的觀察者被告知所做更改的類型,以及更改涉及哪些對象。

一、基本使用

1.1 注冊觀察者

/// 注冊觀察者
/// @param observer 觀察者
/// @param keyPath 要觀察的屬性keyPath
/// @param options 觀察者選項(xiàng)。影響通知的生成方式及回調(diào)時(shí)字典中攜帶的信息
/// @param context 上下文
- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(nullable void *)context;

context接收一個(gè)void *類型的參數(shù),基本可以傳任何類型。假如子類和他的父類由于不同的原因都注冊了對同一個(gè)屬性的觀察,在回調(diào)中這兩種的處理是不同的,那么回調(diào)中的keyPath和被觀察者對象是無法區(qū)分的,此時(shí)就可以通過context這個(gè)參數(shù)來區(qū)分。

1.2 實(shí)現(xiàn)回調(diào)

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context

1.3 移除觀察者

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

1.4觀察集合類型屬性

@interface Animal : NSObject


@property (nonatomic,strong) NSMutableArray *friends;

@end

//viewContoller代碼
- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    Animal *animal = [Animal alloc];
    animal.friends = @[].mutableCopy;
    [animal addObserver:self
             forKeyPath:@"friends"
                options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                context:NULL];
    
    //1、被動(dòng)觸發(fā)錯(cuò)誤方式:這里無法觸發(fā)`kvo`回調(diào)
    [animal.friends addObject:@"dog"];
    //2、被動(dòng)觸發(fā)正確方法
    [[animal mutableArrayValueForKey:@"friends"] addObject:@"dog"];
    //3、手動(dòng)觸發(fā)
    [animal willChangeValueForKey:@"friends"];
    [animal.friends addObject:@"dog"];
    [animal didChangeValueForKey:@"friends"];

}

23行代碼相當(dāng)于

NSMutableArray *tmp = [NSMutableArray arrayWithArray:animal.friends];
[tmp addObject:@"dog"];
animal.friends = tmp;

因此觸發(fā)了kvo,21行,因?yàn)镵VO是給予set方法的,這樣不會(huì)觸發(fā)set方法,所以就不會(huì)觸發(fā)KVO通知。

1.5多屬性的關(guān)聯(lián)

我們需要在被觀察者類重寫兩個(gè)方法:

  1. 一個(gè)系統(tǒng)方法+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key或者+ (NSSet *)keyPathsForValuesAffecting<xxx>
  2. 一個(gè)是被觀察屬性的getter方法。

例如:有一個(gè)Downloader.h類,有三個(gè)屬性totalBytes,completedBytes,和百分比進(jìn)度progress

// Downloader.h
@interface Downloader : NSObject

@property (nonatomic) unsigned long long totalBytes;

@property (nonatomic) unsigned long long completedBytes;

@property (nonatomic, copy) NSString *progress;

@end

在UI層我們只關(guān)注progress,但進(jìn)度是受其他兩個(gè)屬性共同影響的,此時(shí)需要在Downloader.m實(shí)現(xiàn)中重寫兩個(gè)方法:

@implementation Downloader

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"progress"]) {
        NSArray *dependKeys = @[@"totalBytes", @"completedBytes"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:dependKeys];
    }
    return keyPaths;
}

- (NSString *)progress {
    if (0 == self.totalBytes || 0 == self.completedBytes) {
        return @"0";
    }
    
    double progress = (double)self.completedBytes / (double)self.totalBytes * 100;
    
    if (progress > 100) {
        progress = 100;
    }
    
    return [NSString stringWithFormat:@"%d%%", (int)ceil(progress)];
}

@end

二、KVO實(shí)現(xiàn)原理

Automatic key-value observing is implemented using a technique called isa-swizzling. 具體參考蘋果文檔

當(dāng)一個(gè)類的實(shí)例第一次注冊觀察者時(shí),系統(tǒng)會(huì)做以下事情:

  • 動(dòng)態(tài)生成一個(gè)繼承自該類的中間類:NSKVONotifying_xxx
  • 將對象的isa指向這個(gè)中間類(isa-swizzling
  • 觀察的是setter
  • 子類中重寫set<xxx>、-class、-dealloc方法,添加一個(gè)-_isKVOA方法,依然返回原類,而非子類
  • 移除所有的觀察后,isa會(huì)指回來,但是動(dòng)態(tài)子類不會(huì)銷毀

2.1 原理驗(yàn)證

被觀察類Animal添加代碼:

@interface Animal : NSObject{
    @public
    NSString *nickName;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic,strong) NSMutableArray *friends;

@end

viewController添加代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    Animal *animal = [Animal alloc];
    [self printClasses:[animal class]];
    [self printMethods:[animal class]];
    
    [animal  addObserver:self
                forKeyPath:@"nickName"
                   options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                   context:NULL];
    [animal addObserver:self
             forKeyPath:@"name"
                options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                context:NULL];
    [animal addObserver:self
             forKeyPath:@"friends"
                options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                context:NULL];
    
    printf("\n********************************************************\n\n");
    [self printClasses:[animal class]];
    [self printMethods:NSClassFromString(@"NSKVONotifying_Animal")];
    
    
    
    
    animal.name = @"dog";
    animal->nickName = @"cat";
}

/// 打印出指定類及其子類列表
- (void)printClasses:(Class)cls {
    int count = objc_getClassList(NULL, 0);
    NSMutableArray *results = [NSMutableArray arrayWithObject:cls];
    Class *classes = (Class *)malloc(sizeof(Class) * count);
    objc_getClassList(classes, count);
    for (int i = 0; i < count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [results addObject:classes[i]];
        }
    }
    NSLog(@"\nClasses: %@", results);
    free(classes);
}

/// 打印出指定類所有的方法
- (void)printMethods:(Class)cls {
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    printf("Methods of class: %s (\n", NSStringFromClass(cls).UTF8String);
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = method_getImplementation(method);
        printf("    %s-%p\n", NSStringFromSelector(sel).UTF8String, imp);
    }
    printf(")\n");
    free(methodList);
}

控制臺(tái)打印結(jié)果 :

2020-04-13 15:08:51.803441+0800 kvo[23854:22631006] 
Classes: (
    Animal
)
Methods of class: Animal (
    .cxx_destruct-0x10f868e50
    name-0x10f868d80
    setName:-0x10f868db0
    friends-0x10f868df0
    setFriends:-0x10f868e10
)

********************************************************

2020-04-13 15:08:51.809383+0800 kvo[23854:22631006] 
Classes: (
    Animal,
    "NSKVONotifying_Animal"
)
Methods of class: NSKVONotifying_Animal (
    setFriends:-0x7fff25701c8a
    setName:-0x7fff25701c8a
    class-0x7fff2570074d
    dealloc-0x7fff257004b2
    _isKVOA-0x7fff257004aa
)
2020-04-13 15:08:51.809932+0800 kvo[23854:22631006] -------------------{
    kind = 1;
    new = dog;
    old = "<null>";
}


通過上面打印結(jié)果發(fā)現(xiàn):只有屬性發(fā)生了回調(diào),實(shí)例變量并沒有。它們的區(qū)別就是有沒有setter方法,所以我們得出結(jié)果:KVO是通過setter方法進(jìn)行處理回調(diào)的。

蘋果官方推薦盡量使用屬性點(diǎn)語法的形式為屬性賦值和訪問屬性,這樣其實(shí)是在調(diào)用setter和getter,如果重寫了setter和getter在期中增加了額外代碼,可以保證代碼執(zhí)行的正確性。

viewController中繼續(xù)添加代碼,移除所有的觀察者。

[self performSelector:@selector(removeAllObserver) withObject:nil afterDelay:2];

- (void)removeAllObserver{
    [_animal removeObserver:self forKeyPath:@"nickName"];
    [_animal removeObserver:self forKeyPath:@"name"];
    [_animal removeObserver:self forKeyPath:@"friends"];
    
    printf("\n********************************************************\n\n");
    [self printClasses:[_animal class]];
}

打印結(jié)果:

2020-04-13 15:20:17.770486+0800 kvo[24356:22641029] 
Classes: (
    Animal,
    "NSKVONotifying_Animal"
)

你也可以通過lldb,來探索一下,整個(gè)過程中isa指針的指向,object_getClassName(animal)

2.2 kvc 和 kvo

蘋果文檔有介紹,在理解KVO之前,必須先理解KVC。上篇文章我們也討論了KVC的實(shí)現(xiàn)原理,KVC會(huì)先查找settergetter進(jìn)行調(diào)用,如果沒有查找到,則調(diào)用類方法+accessInstanceVariablesDirectly,如果返回YES,再去查找成員變量。
KVO也有類似的機(jī)制,在KVO接口中有這三個(gè)接口:

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;

+automaticallyNotifiesObserversForKey:默認(rèn)返回YES,動(dòng)態(tài)創(chuàng)建的中間類重寫了setter,雖然無法看到實(shí)現(xiàn)源碼,但可以猜測在修改屬性前后分別調(diào)用了-willChangeValueForKey:-didChangeValueForKey:類似方法,達(dá)到通知觀察者的目的。
如果子類中重載了+automaticallyNotifiesObserversForKey:并返回NO,則無法觸發(fā)自動(dòng)KVO通知機(jī)制,但我們可以通過手動(dòng)調(diào)用-willChangeValueForKey:-didChangeValueForKey:來觸發(fā)KVO回調(diào)。

三、自定義KVO

系統(tǒng)kvo使用時(shí)存在不方便的地方,根據(jù)kvo的原理和基本使用,我們可以簡單自定義kvo實(shí)現(xiàn)。

  1. 入?yún)z查
  2. 檢查是否有屬性的setter
  3. 動(dòng)態(tài)創(chuàng)建對象子類BLKVOClass_xxx
  4. isa-swizzling
  5. 重寫-class、-dealloc方法
  6. 重寫setter
  7. 保存觀察者信息,在屬性發(fā)生變化時(shí)回調(diào)

3.1 動(dòng)態(tài)創(chuàng)建對象子類

    Class newClass = NSClassFromString(newClassName);
    
    if (newClass) {
        return newClass;
    }
    
    /**
    * 如果內(nèi)存不存在,創(chuàng)建生成
    * 參數(shù)一: 父類
    * 參數(shù)二: 新類的名字
    * 參數(shù)三: 新類的開辟的額外空間
    */
   
    // 2.1 : 申請類
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 2.2 : 注冊類
    objc_registerClassPair(newClass);

3.2 isa-swizzling

重寫class方法

Class mm_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}

動(dòng)態(tài)子類添加class實(shí)現(xiàn),完成isa-swizzling

// 2.3.1 : 添加class : class的指向是父類
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod(newClass, classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)mm_class, classTypes);

3.3 dealloc

重寫delloc,1、安全移除所有observe,2、銷毀關(guān)聯(lián)對象,3、isa指回父類,4、調(diào)用系統(tǒng)dealloc。

- (void)mm_dealloc {
//    Class superClass = [self class];
    Class superCls = class_getSuperclass(object_getClass(self));
    object_setClass(self, superCls);
    
    // Call system -dealloc
    [self mm_dealloc];
}

四、FBKVOController

下面簡單聊一下FBKVOController,它里面有幾個(gè)關(guān)鍵類:

  1. _FBKVOSharedController,單利對象,處理、轉(zhuǎn)發(fā)KVOViewController傳過來的所有觀察者事件。
  2. _FBKVOInfo,數(shù)據(jù)模型,保存一個(gè)完整的KVO數(shù)據(jù)。
  3. KVOViewController,每個(gè)觀察者都有一個(gè)該類的實(shí)例對象,這個(gè)類用于處理觀察者傳過來的所有數(shù)據(jù),下圖是他的主要屬性構(gòu)成。
    KVOViewController.png

下面是一個(gè)簡單的調(diào)用實(shí)現(xiàn)代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Student *student = [[Student alloc] init];
    
    FBKVOController *kvoCtrl = [FBKVOController controllerWithObserver:self];
    
    [kvoCtrl observe:student keyPath:@"nickName" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        NSLog(@"****%@****",change);
    }];
    
    student.nickName = @"kkk";
    
}

observe對應(yīng)viewControllerstudent對應(yīng)object。當(dāng)viewController被釋放的時(shí)候,會(huì)先調(diào)用FBKVOControllerdealloc方法,在這里會(huì)將_objectInfosMap里所有的被觀察者安全得 remove。

拓展:抖音技術(shù)團(tuán)隊(duì)iOS大解密:玄之又玄的KVO、Objective-C & Swift 最輕量級(jí) Hook 方案

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

相關(guān)閱讀更多精彩內(nèi)容

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