iOS底层原理之`OC语法`(KVC和KVO)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Bolted_snail/article/details/82147675

文章目录

1. KVC

  • KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性。
  • 常见的API有:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;//通过keyPath可以设置属性的属性
- (void)setValue:(id)value forKey:(NSString *)key;//通过key设置自己的属性
- (id)valueForKeyPath:(NSString *)keyPath;//通过keyPath访问属性的属性
- (id)valueForKey:(NSString *)key; //通过key访问自己的属性
  • kvc设置属性原理:
    kvc设置属性原理

  • setValue:forKey的原理详解:

    1. 先去寻找setKey:方法(即对应key的set方法),找到了就去传递参数调用方法;
    2. 如果没找到,再去寻找_setKey:的方法,找到可传值;
    3. 没找到就去查看+ (BOOL)accessInstanceVariablesDirectly;这个方法是不是设置为YES(默认是YES),如果设置的是YES这表示直可以接访问实例成员变量;
    4. 然后会去查看有没有_key的成员变量、有就设值;
    5. 没有就去查看有没有_isKey的成员变量、有就设值;
    6. 没有就去查看有没有key的成员变量、有就设值;
    7. 没有就去查看有没有isKey的成员变量、有就设值;
    8. 如果还没找到就会抛异常:NSUnknownKeyException
    9. 如果上面accessInstanceVariablesDirectly设置为NO,并且setKey:、_setKey:这两个set方法没找到就会抛异常,不会去访问属性。
      示例1:
@interface Person : NSObject{
@public
//    int _age;
    int _isAge;
    int age;
    int isAge;

}

/**age属性*/
//@property(nonatomic,assign) int age;
@end

@implementation Person

//-(void)setAge:(int)age{
//    _age = age;
//}
//-(void)_setAge:(int)age{
//    _age = age;
//}
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person * p = [[Person alloc]init];
        [p setValue:@18 forKey:@"age"];
//      NSLog(@"%d",p->age);
        
    }
    return 0;
}

kvc设置属性原理

上面代码和调试结果可以看出:当把两个set方法和_age成员变量注销后会优先访问_isAge成员变量,可以通过注销其他的成员变量一次证明方法的顺序,这里不再赘述。

在Person类中重写+ (BOOL)accessInstanceVariablesDirectly;方法,并设置返回值为NO,编译就会抛异常。

@implementation Person
+ (BOOL)accessInstanceVariablesDirectly{
    return NO;
}
@end

抛异常.png

  • kvc取值原理:
    kvc取值原理.png
  • valueForKey:原理详解:
  1. 同设置属性一样,取值时访问方法的顺序是getKeykeyisKey_key,找到前面的方法就调用给成员变量取值就不会再往下找了。
  2. 如果上面的方法都没找到就会去查看accessInstanceVariablesDirectly`这个方法是不是设置为YES,如果设置的是YES这表示直可以接访问实例成员变量;
  3. 设置成员变量的顺序为:_key_isKeykeyiskey找到前面的成员变量取值就不会再往下找了;
  4. 如果没有找到上面的成员变量,就会抛异常:NSUnknownKeyException
  5. 如果上面accessInstanceVariablesDirectly设置为NO,并且上面的方法都没有找到也会抛异常。
  • 注意:上面的赋值取值方法以及成员变量可以是私有的,依然可以访问,即KVC是可以访问对象的私有属性的。
//.m文件
#import "Person.h"
@interface Person(){
    @private
    int _age;
    int _isAge;
    int age;
    int isAge;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person * p = [[Person alloc]init];
        [p setValue:@18 forKey:@"age"];
        NSLog(@"%@",[p valueForKey:@"age"]);
        
    }
    return 0;
}

kvc访问私有属性.png

2. KVO

  • KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。
  • 使用KVO分为三个步骤:
    1. 通过- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;方法注册观察者,观察者可以接收keyPath属性的变化事件。
    2. 在观察者中实现-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
    3. 当观察者不需要监听时,可以调用- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash
    4. 提示:通过kvc修改属性也是可以触发监听的。
  • 示例
#import <Foundation/Foundation.h>
@interface Person : NSObject
/**name*/
@property(nonatomic,copy) NSString * name ;
/**age*/
@property(nonatomic,assign) int age;
@end


#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
/**person */
@property(nonatomic,strong) Person * person;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[Person alloc]init];
    self.person.name = @"a";
    self.person.age = 16;
    //给person添加kvo监听,监听age和name属性值的变化
    /*
     observer:监听者
     keyPath:属性对应的键
     options:监听内容,一般是监听新值和旧值
     context:可以传递一些参数
    */
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"参数1"];
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"参数2"];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.name = @"aa";
    self.person.age = 18;
}
// 当监听对象的属性值发生改变时,就会调用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
//移除对应的监听者
-(void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name"];
    [self.person removeObserver:self forKeyPath:@"age"];
}
@end
  • 打印结果:
    kvo
  • 底层原理
    未使用KVO监听对象时,直接调用set方法赋值:
    未使用KVO监听的对象时

使用的KVO监听对象时,内部方法调用顺序示例:

#import <Foundation/Foundation.h>
@interface Person : NSObject
/**age*/
@property(nonatomic,assign) int age;
@end

@implementation Person
-(void)setAge:(int)age{
    _age = age;
     NSLog(@"setAge:");
}
-(void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"%@:willChangeValueForKey",key);
}
- (void)didChangeValueForKey:(NSString *)key{
    NSLog(@"%@:didChangeValueForKey - begin",key);
    [super didChangeValueForKey:key];
    NSLog(@"%@:didChangeValueForKey - end",key);
}
@end

#import "ViewController.h"
#import "Person.h"

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

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[Person alloc]init];
    self.person.age = 16;
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"参数2"];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.age = 18;
}
// 当监听对象的属性值发生改变时,就会调用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了- %@", object, keyPath, context);
}
-(void)dealloc{
    [self.person removeObserver:self forKeyPath:@"age"];
}
@end

打印结果:
调用顺序

通过转换为的cpp代码时,我们可以发现使用的KVO监听对象时:

  1. 当修改对象属性时会通过RuntimeAPI动态生成一个子类NSKVONotifying_xxx,让实例对象的isa指向这个全新的子类,新生成的子类重写了setter/getter方法,其中setter方法中会调用Foundation_NSSetXXXValueAndNotify函数;
  2. _NSSetXXXValueAndNotify内部又调用了 willChangeValueForKey:父类原来的setterdidChangeValueForKey:;
  3. 当调用didChangeValueForKey:方法时就会触发监听器调用-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{方法。
    在这里插入图片描述
  • 新生成的子类内部实现如下:
#import "NSKVONotifying_MJPerson.h"
@implementation NSKVONotifying_MJPerson
- (void)setAge:(int)age
{
    _NSSetIntValueAndNotify();
}
// 伪代码
void _NSSetIntValueAndNotify()
{
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key
{
    // 通知监听器,某某属性值发生了改变
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

// 屏幕内部实现,隐藏了NSKVONotifying_MJPerson类的存在
- (Class)class
{
    return [MJPerson class];
}
- (void)dealloc
{
    // 收尾工作
}
- (BOOL)_isKVOA
{
    return YES;
}
@end
  • 面试题:
  1. iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
    利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类;
    当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数、
    willChangeValueForKey:
    父类原来的setter
    didChangeValueForKey:
    内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:
  2. 直接修改成员变量会触发KVO么?
    不会触发KVO。
  3. 如何手动触发KVO?
    手动调用willChangeValueForKey:和didChangeValueForKey:,或者通过kvc去修改属性。
    手动触发KVO示例:
#import <Foundation/Foundation.h>
//Person只有一个公开的成员变量
@interface Person : NSObject{
    @public
    int age;
}
@end

//PersonObserver是自定义的监听者
#import "PersonObserver.h"
@implementation PersonObserver
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@的%@值发送变化了!!!",object,keyPath);
}
@end

#import "ViewController.h"
#import "Person.h"
#import "PersonObserver.h"
@interface ViewController ()
/**person */
@property(nonatomic,strong) Person * person;
@property(nonatomic ,strong) PersonObserver * observer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[Person alloc]init];
    self.person -> age = 16;
    self.observer = [[PersonObserver alloc]init];
    [self.person addObserver:self.observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person willChangeValueForKey:@"age"];
    self.person -> age = 18;
//    [self.person setValue:@18 forKey:@"age"];
    [self.person didChangeValueForKey:@"age"];
}
-(void)dealloc{
    [self.person removeObserver:self.observer forKeyPath:@"age"];
}
@end

结果可见:如果直接通过指针给属性赋值是无法触发监听方法的,但是要是手动调用PersonwillChangeValueForKey:didChangeValueForKey:方法就会触发监听方法,或者通过kvc也可以直接触发监听方法。

KVO原理分析及使用进阶

猜你喜欢

转载自blog.csdn.net/Bolted_snail/article/details/82147675