iOS 进阶之路

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

2020-10-29  本文已影响0人  markhetao

OC底层原理 学习大纲

上一节,我们介绍了KVC原理,而KVC工作,绝大部分通过继承NSObject自动处理好了。实际应用中,我们关注相对较少。而基于KVCKVO,在应用中却是非常的广泛。

现在我们使用的响应式框架(RACRxSwifCombine等),实际都是KVO机制的应用

本节,我们详细讲解KVO:

  1. KVO介绍
  2. KVO应用
  3. KVO原理

引入:

  • 我们上一节分析KVC时,官方KVC的应用中,第一个介绍的就是KVO
    👉 KVC文档链接
    image.png

我们点击进入Key-Value Observing Programming Guide (KVO指引)


1. KVO介绍

KVO,全称为Key-value observing键值观察。

2 KVO应用:

// 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. 监听
- (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

主要步骤: 1. 添加 -> 2. 监听 ->3. 移除

2.1 添加

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew     // 新值
    NSKeyValueObservingOptionOld     // 旧值
    NSKeyValueObservingOptionInitial // 初始值 
    NSKeyValueObservingOptionPrior   // 变化前
};

面试官:添加通知时,context写什么内容?
答:填nil
面试官:回去等通知 😂

image.png

所以苹果建议我们可以static void*创建静态的context,这样的好处是:

2.2 监听

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

2.3 移除

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

网上有很多说Xcode升级后,不再需要手动移除监听者。仅仅在当前页面操作时,确实不用处理。

  • 但如果业务变得复杂,对于同一对象属性,如果当前页面进行了添加、监听和移除,而其他页面只进行添加和监听,再触发监听时,就会产生KVO Crash。所以我们要养成谁使用谁销毁的习惯。

2.4 开关

image.png
@implementation HTPerson
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}

-(void)setName:(NSString *)name {
    
    if ([self.name isEqualToString: name]) return; // 值没变化,不操作
    
    [self willChangeValueForKey:@"name"]; // 即将改变
    _name = name; // 赋值
    [self didChangeValueForKey:@"name"]; // 已改变
}
@end

2.5 路径处理

// 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];
}

// 下载进入 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];

    // 添加监听
    [self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context: NULL];
    
}

// 处理监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString: @"downloadProgress"]) {
        NSLog(@"当前进度:%@", change[NSKeyValueChangeNewKey]);
    }
}

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

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

@end

2.6 数组的观察

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];
    
    // 添加监听
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
    [self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context: NULL];
}

// 处理监听
- (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"]; //此赋值仅改变数组内部元素,不会引起数组地址的变化
    [[self.person mutableArrayValueForKeyPath:@"dateArray"] insertObject:@"6" atIndex:0];
    [[self.person mutableArrayValueForKeyPath:@"dateArray"] setObject:@"8" atIndexedSubscript:0];
    [[self.person mutableArrayValueForKeyPath:@"dateArray"] removeObjectAtIndex:0];
}

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

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

3. KVO底层原理

3.1 KVO只观察Setter方法

// 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 ";
    
    // 添加监听
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context: NULL];
}

// 处理监听
- (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 {
    // 移除监听
    [self.person removeObserver:self forKeyPath:@"name" context: NULL];
    [self.person removeObserver:self forKeyPath:@"nickName" context: NULL];
}

@end

3.2 KVO派生类

3.2.1 NSKVONotifying_HTPersonHTPerson什么关系?

/// 遍历本类及子类
-(void) printClasses: (Class)cls {
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建1个数组
    NSMutableArray * mArray = [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])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes: %@",mArray);
}
image.png

我们添加遍历IvarsPropertyMethod的函数:

/// 遍历Ivars
-(void) printIvars: (Class)cls {
    
    // 仿写Ivar结构
    typedef struct HT_ivar_t {
        int32_t *offset;
        const char *name;
        const char *type;
        uint32_t alignment_raw;
        uint32_t size;
    }HT_ivar_t;

    // 记录函数个数
    unsigned int count = 0;
    // 读取函数列表
    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 {
    
    // 仿写objc_property_t结构
    typedef struct Ht_property_t{
        const char *name;
        const char *attributes;
    }Ht_property_t;

    // 记录函数个数
    unsigned int count = 0;
    // 读取函数列表
    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 {
    
    // 记录函数个数
    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(@"method: %@-%p", NSStringFromSelector(sel), imp);
    }
    free(methodList);
}
    // 添加监听
    [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];
image.png

拓展:

  • 检验同样继承HTPerosn的子类HTStudent,打印结果:
// HTStudent
@interface HTStudent : HTPerson
@end
@implementation HTStudent
@end
image.png

结论:

3.2.2 KVO派生类给父类属性赋值

image.png image.png image.png

3.2.3 KVO派生类何时移除,是否真移除?

ps: 页面销毁之后再打印HTPerosn类和子类,也一样存在NSKVONotifying_HTPerson派生类。


至此,我们已经知道KVO是创建派生类实现了键值观察

下一节,我们纯代码自定义KVO。(简化版,重在理解派生类流程功能

上一篇 下一篇

猜你喜欢

热点阅读