iOS开发底层之KVO探索上 - 17

Posted iOS_developer_zhong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS开发底层之KVO探索上 - 17相关的知识,希望对你有一定的参考价值。


前言

本章内容主要是围绕KVO进行探索,从KVO的介绍 -》KVO的坑点 -》 KVO的大致流程 -》KVO的自定义实现 -》优秀的KVO封装库介绍。

一、KVO是什么?

KVO的全称为:Key-Value Observing,“键值监听”。

主要作用为:监听某个对象属性值的改变,继而进行对应的业务处理。

简单使用的代码:

 self.person = [[MYPerson alloc] init];
 
    // 属性监听
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

}

// KVO的释放
-(void)dealloc {
    [self.person removeObserver:self  forKeyPath:@"likeSome"];
}

// 方便调试
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    int b = 100 +  (arc4random() % 101);
    NSString *tt = [NSString stringWithFormat:@"value: %d",b];
    self.person.name = tt;
}

// kvo值改变回调
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSLog(@"--keypath = 【%@】 -change = 【%@】", keyPath,change);
}

二、KVO注意项

1. KVO中的Context有什么作用?

根据官方文档 : KVO官方说明
上面介绍到: Context起到的作用就是一个标识符, 主要是为了监听对象更加安全,万一监听的属性路径一致,导致无法区分监听场景而诞生的,下面展示下官方的一段源码:


static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

- (void)registerAsObserverForAccount:(Account*)account {
	//不同的context的监听
    [account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
 
    [account addObserver:self
              forKeyPath:@"interestRate"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                  context:PersonAccountInterestRateContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
    } else {
        // Any unrecognized context must belong to super
    }
}

2. 忘记移除观察者,而造成程序的崩溃

官方文档中,有这么一句话:

An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.

大概的意思就是: 观察者不会在dealloc的时候自动移除,而观察对象后续的操作就算观察者依附的对象已经dealloc也会继续发送消息, 这样就会导致程序崩溃。 所以我们需要手动移除观察者, 后面有更加优化的方案解决这个问题。 请继续读下去。 

3. 控制某些属性不能使用KVO

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqual: @"name"]) {
        return  false;
    } else {
        return true;
    }
}

4. 一对多的观察。

是否有遇到过这种场景: A属性的值, 是取决于另外两个属性B、C之间的计算才能得出, 这个时候对A属性进行观察,怎么办呢?

官方举例: fullName = firstName + lastName
当firstName 或者 lastName ,任意一个属性发生改变,必然会影响到 fullName。
那么就需要用到下面这个方法:

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

// 写法二:
+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

// fullName的get方法
- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

// 其他的流程和前面写的一样只要去监听 fullName
[self.person addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew context:nil];

5. 对可变数组的KVO。

通过mutableArrayValueForKey 来实现, 直接上用法。

[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];

self.person.dateArray = [NSMutableArray array];

[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"hello"];

// 通过 mutableArrayValueForKey ,kvo是基于KVC之上

三、KVO的流程和原理

自动键值观察:是通过isa-swizzling的技术实现。

  1. 在原有类的基础上新增一个派生类 NSKVONotifying_ 开头。
    可通过代码来调试出来:
    self.person = [[MYPerson alloc] init];
    // 没有使用KVO之前,打印所有类。
    [self printClasses:[self.person class]];
    // 属性
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    // 使用KVO再打印所有类。 
    [self printClasses:[self.person class]];

// 利用runtime打印所有的类方法。 
- (void)printClasses:(Class)cls {
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    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);
}
    
    // 打印结果
    2021-09-10 17:31:51.644193+0800 001-内存对齐原则[18934:903108] classes = (
    MYPerson
)
	2021-09-10 17:31:51.665579+0800 001-内存对齐原则[18934:903108] classes = (
    MYPerson,
    "NSKVONotifying_MYPerson"
)

// 很清楚的看见,当使用kvo后, 会新增加一个NSKVONotifying_MYPerson 类。 
  1. 关于在移除观察者的时候, 会不会吧isa指向会原类。
    我们在移除观察者代码前后,打上断点去观察, 就会发现,isa会指向原类 。

-(void)dealloc {
    
    [self.person removeObserver:self  forKeyPath:@"name"];
    
}  

// lldb调试 结果
(lldb) p object_getClassName(self.person)
(const char * _Nonnull) $0 = 0x000000028232f380 "NSKVONotifying_MYPerson"
(lldb) p object_getClassName(self.person)
(const char * _Nonnull) $1 = 0x0000000100907618 "MYPerson"

整体流程为:
当前类设置观察者后, 会派生一个子类(名称开头为NSKVONotifying_), 这个派生类和原来的类一模一样, 并且会在原来的类之上增加对属性的 setter方法监听,也就是 willChange , didChange。 当这些被观察的属性发生改变的时候,就会给所有的观察者,发送指令, 这个属性改变了,你该干啥就要干啥了。 这些设置好后,就把原类的isa指向这个派生的类, 最后在原类移除观察者的时候,就会将isa还原回来。

以上是关于iOS开发底层之KVO探索上 - 17的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发底层之KVO探索上 - 17

iOS开发底层之KVO探索下 -18

iOS开发底层之KVO探索下 -18

iOS开发底层之KVO探索下 -18

iOS底层探索之KVO—FBKVOController分析

iOS底层探索之KVO—自定义KVO