iOS KVO 探索

179 阅读5分钟

1.KVO的常规写法

您必须执行以下步骤才能使对象能够接收 KVO 兼容属性的键值观察通知:

  • 使用 方法向被观察对象注册观察者addObserver:forKeyPath:options:context:
  • observeValueForKeyPath:ofObject:change:context:在观察者内部实现以接受更改通知消息。
  • 当观察者removeObserver:forKeyPath:不再应该接收消息时,使用该方法取消注册观察者。至少,在观察者从内存中释放之前调用这个方法。

context

addObserver:forKeyPath:options:context:消息中的上下文指针包含任意数据,这些数据将在相应的更改通知中传回给观察者。您可以指定NULL并完全依赖键路径字符串来确定更改通知的来源,但是这种方法可能会导致超类由于不同原因也在观察相同键路径的对象出现问题。

一种更安全、更可扩展的方法是使用上下文来确保您收到的通知是发送给您的观察者而不是超类的。

类中唯一命名的静态变量的地址是一个很好的上下文。在超类或子类中以类似方式选择的上下文不太可能重叠。您可以为整个类选择一个上下文,并依靠通知消息中的关键路径字符串来确定发生了什么变化。或者,您可以为每个观察到的键路径创建一个不同的上下文,这完全绕过了字符串比较的需要,从而提高了通知解析的效率。清单 1显示了以这种方式选择的agename属性的示例上下文。


//创建上下文指针
static void * PersonAgeContent  = &PersonAgeContent;
static void * PersonNameContent = &PersonNameContent;


- (void)viewDidLoad {\
    [super viewDidLoad];\
    // Do any additional setup after loading the view.

    self.person = [[Person alloc]init];
    self.person.age = 18;
    self.person.name = @"zhangsan";

    //KVO
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:PersonAgeContent];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContent];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    if(context == PersonAgeContent){
        NSLog(@"%@ -- ",change);
    }
    else if(context == PersonNameContent){
        NSLog(@"%@ -- ",change);
    }
    else {
        //do something
    }
}

手动观察与自动观察

 
//重写NSObject中的方法
- (BOOL)automaticallyNotifiesObserversForKey:(NSString*)theKey{
    return YES; // YES表示自动观察  NO为手动观察
}

- (void)setAge:(NSUInteger)age{

    [self willChangeValueForKey:@"age"];
    _age = age;
    [self willChangeValueForKey:@"age"];
    
}

 

一对多的观察

@interface Person : NSObject

@property(nonatomic,assign) NSUInteger totolCount;
@property(nonatomic,assign) NSUInteger completedCount;
@property(nonatomic,copy) NSString * progressStr;

@end


@implementation Person

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    
    if([key isEqualToString:@"progressStr"]){
        NSArray * affectingKeys = @[@"totalCount",@"completedCount"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    
    return keyPaths;
}

- (NSString *)progressStr{\
    return [NSString stringWithFormat:@"%.2f",self.completedCount * 1.0 / self.totolCount * 1.0];
}


@end

可变数组的观察

直接修改可变数组无法触发观察需要执行可变数组指定的KVC方法

  • mutableArrayValueForKey: 和 mutableArrayValueForKeyPath:

    它们返回一个行为类似于NSMutableArray对象的代理对象。


static void * PersonDataArrContent = &PersonDataArrContent;

self.person.dataArr = [NSMutableArray arrayWithCapacity:1];

[self.person addObserver:self forKeyPath:@"dataArr" options:NSKeyValueObservingOptionNew context:PersonDataArrContent];



[[self.person mutableArrayValueForKey:@"dataArr"addObject:@"observe"];


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

    if(context == PersonDataArrContent){
        NSLog(@"mutablearray change = %@",change);
    }
}

打印 mutablearray change = {
    indexes = "<_NSCachedIndexSet: 0x6000023554c0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        observe
    );
}

KVO的底层原理

自动键值观察是使用称为isa-swizzling的技术实现的。

isa指针,顾名思义,指向对象的类,它保持一个调度表。该调度表主要包含指向类实现的方法的指针,以及其他数据。

当观察者为对象的属性注册时,被观察对象的 isa 指针被修改,指向中间类而不是真正的类。因此,isa 指针的值不一定反映实例的实际类。

您永远不应该依赖isa指针来确定类成员资格。相反,您应该使用该class方法来确定对象实例的类。

static void * PersonNameContent = &PersonNameContent;

- (void)viewDidLoad {\
    [super viewDidLoad];\
    // Do any additional setup after loading the view.

    self.person = [[Person alloc]init];
    self.person.age = 18;
    self.person.name = @"zhangsan";

    //KVO
    //断点打在这句代码处
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContent];
    //断点打在这里
    
}

//前后断点 在lldb处 p object_getClassName(self.person)

(lldb) p object_getClassName(self.person)
(const char * _Nonnull) $0 = 0x000000010034823c "Person"
(lldb) p object_getClassName(self.person)
(const char * _Nonnull) $1 = 0x00006000016a3d60 "NSKVONotifying_Person类"

1.在addObserver代码执行后,系统动态生成了一个NSKVONotifying_Person

2.Person的isa指针改变指向,指向了NSKVONotifying_Person

3.NSKVONotifying_Person重写下面的

set方法(无法监听成员变量,只能监听set方法)、 也修改原来的成员变量的值,同时做

class方法 返回原来的LGPerson类

4.removeObserver方法之后重新将根据残余KeyPath的数量调整isa指向

根据上面的原理自定义KVO

移除观察者

接收到removeObserver:forKeyPath:context:消息后,观察对象将不再接收observeValueForKeyPath:ofObject:change:context:指定键路径和对象的任何消息。

移除观察者时,请记住以下几点:

  • 如果尚未注册为观察者,则要求将其移除为观察者会导致NSRangeException. 您可以removeObserver:forKeyPath:context:为相应的 调用只调用一次addObserver:forKeyPath:options:context:,或者如果这在您的应用程序中不可行,则将removeObserver:forKeyPath:context:调用放置在 try/catch 块中以处理潜在的异常。
  • 解除分配时,观察者不会自动删除自己。被观察的对象继续发送通知,而忽略了观察者的状态。但是,发送到已释放对象的更改通知与任何其他消息一样,会触发内存访问异常。因此,您要确保观察者在从记忆中消失之前将自己移除。
  • 该协议没有提供询问对象是观察者还是被观察者的方法。构建您的代码以避免与发布相关的错误。一个典型的模式是在观察者初始化期间注册为观察者(例如 ininitviewDidLoad)并在释放期间取消注册(通常是 in dealloc),确保正确配对和有序添加和删除消息,并且观察者在从内存中释放之前被取消注册.