iOS底层收集

iOS进阶-11 KVO

2020-02-20  本文已影响0人  ricefun

相信读者对KVO的使用应该已经很熟练了,本文主要讲KVO的一些注意点和原理,对详细的使用不做过多的展示。

日常使用注意点

context 参数

1.context填NULL还是nil?先看源代码:

[self addObserver:self.person forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:<#(nullable void *)#>]

下面context应该填什么?日常我们一般不会用到context这个参数,一般会填nil或者NULL;那么到底是应该填nil,还是NULL;答案是:NULL;
why? 看看在context的placehold上显示的是context:<#(nullable void *)#>,是一个可空void *指针,既然不是oc对象,那么就应该填NULL。我们再看看KVO文档,文档中有这么一点段话

Context ,很明确了吧,当我们不需要这个context参数时,你可以填写NULL;
2.这个context的作用?show U code
######Person
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *nickName;

@end

######Animal
@interface Animal : NSObject
@property (nonatomic,copy) NSString *name;
@end

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

static void *PersonNameContext = &PersonNameContext;
static void *PersonNickNameContext = &PersonNickNameContext;
static void *AnimalNameContext = &AnimalNameContext;


@interface ViewController ()
@property (nonatomic,strong) Person *person;
@property (nonatomic,strong) Animal *animal;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
    
    self.person = [Person new];
    self.animal = [Animal new];
    
    [self.person  addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
    [self.animal addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:AnimalNameContext];
    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:PersonNickNameContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    //常规做法
//    if ([object isEqual:self.person]) {
//        if ([keyPath isEqualToString:@"name"]) {
//            <#statements#>
//        } else if ([keyPath isEqualToString:@"nickName"]) {
//            <#statements#>
//        }
//    } else if ([object isEqual:self.animal]) {
//        if ([keyPath isEqualToString:@"name"]) {
//            <#statements#>
//        }
//    }
    
    //context做法
    if (context == PersonNameContext) {
        if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"name=%@",self.person.name);
        }
    } else if (context == AnimalNameContext ) {
        if ([keyPath isEqualToString:@"name"]) {
            
        }
    } else if (context == PersonNickNameContext) {
        if ([keyPath isEqualToString:@"nickName"]) {
            NSLog(@"nickName=%@",self.person.nickName);
        }
    }
}
static int a = 1;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    a ++;
    self.person.name = [NSString stringWithFormat:@"name+%d",a];
    self.person.nickName = [NSString stringWithFormat:@"nickName+%d",a];
}

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

@end

所以:使用context参数会更加的便捷高效安全

观察者要移除

日常开发中,一定要写移除观察者的代码,如果没有移除,会有造成野指针,成为崩溃隐患。

多次修改代码高效设置

eg:假设在上面context 参数内容段中,因为需求Person类中的name属性昨天是需要观察的,而今天一上班,产品经理说需求又改了,又不需要再观察了这个那么属性。通常遇到这种情况的时候,我们会删掉(注销)之前写好的代码,然后又过了几天产品经理要求改回来;遇到这个情况估计你的内心会有一万匹草泥马跑过。因为KVO代码量分散且并不少,这种操作其实让人很烦;这个时候你可以在Person类中重写这个方法automaticallyNotifiesObserversForKey:(是否自动观察属性)这样操作:只会观察nickName,而不会观察name

#import "Person.h"

@implementation Person

// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return YES;
}

@end

使用automaticallyNotifiesObserversForKey:根据key去判断,可以让程序更加健壮;

多个因素影响

当被观察的对象受到其他多个因素影响时;
eg:下载进度受当前下载量和总下载量的影响,但是我们需要观察的是进度,可以使用keyPathsForValuesAffectingValueForKey:

#####DownLoadManager
@interface DownLoadManager : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;

@end
#import "DownLoadManager.h"

@implementation DownLoadManager
// 下载进度 -- 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;
}

- (NSString *)downloadProgress{
    return [NSString stringWithFormat:@"downloadProgress =%.02f%%",self.writtenData/self.totalData*100];
}

@end

######ViewController
#import "ViewController.h"
#import "DownLoadManager.h"

@interface ViewController ()
@property (nonatomic,strong) DownLoadManager *downLoadManager;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.downLoadManager = [DownLoadManager new];
    self.downLoadManager.writtenData = 10;
    self.downLoadManager.totalData = 100;
    
    //  多个因素影响 - 下载进度 = 当前下载量 / 总量
    [self.downLoadManager 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(@"downloadProgress = %@",self.downLoadManager.downloadProgress);
    }
}

//点击屏幕
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.downLoadManager.writtenData += 10;
    self.downLoadManager.totalData += 5;
}

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

@end

点击屏幕打印结果:

2020-02-21 11:08:38.002797+0800 KTest[5745:107244] downloadProgress = downloadProgress =20.00%
2020-02-21 11:08:38.002912+0800 KTest[5745:107244] downloadProgress = downloadProgress =19.05%
2020-02-21 11:08:39.408895+0800 KTest[5745:107244] downloadProgress = downloadProgress =28.57%
2020-02-21 11:08:39.409002+0800 KTest[5745:107244] downloadProgress = downloadProgress =27.27%
2020-02-21 11:08:40.105935+0800 KTest[5745:107244] downloadProgress = downloadProgress =36.36%
2020-02-21 11:08:40.106029+0800 KTest[5745:107244] downloadProgress = downloadProgress =34.78%
可变数组

观察可变数组的增删改查时,不要直接使用addObject:或者removeObject:直接使用会崩溃,需要先通过mutableArrayValueForKey:获得数组对象,才能进一步操作;原因:iOS默认不支持对数组的KVO,KVO是通过KVC实现的,普通方式监听的对象的地址的变化,而数组地址不变,而是里面的值发生了改变;
eg:

######Person
@interface Person : NSObject
@property (nonatomic,copy) NSMutableArray *studentNameArray;
@end

######ViewController
#import "ViewController.h"
#import "Person.h"
#import "Animal.h"
#import "DownLoadManager.h"

static void *PersonNameContext = &PersonNameContext;
static void *PersonNickNameContext = &PersonNickNameContext;
static void *AnimalNameContext = &AnimalNameContext;


@interface ViewController ()
@property (nonatomic,strong) Person *person;
@property (nonatomic,strong) Animal *animal;
@property (nonatomic,strong) DownLoadManager *downLoadManager;

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [Person new];
    self.animal = [Animal new];
     self.person.studentNameArray = [NSMutableArray arrayWithCapacity:1];
     [self.person addObserver:self forKeyPath:@"studentNameArray" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"studentNameArray"]) {
        NSLog(@"studentNameArray = %@",self.person.studentNameArray);
    }
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {   
    // KVO 建立在 KVC上
//    [self.person.studentNameArray addObject:@"lee"];不要这么做,会崩溃
    //使用mutableArrayValueForKey获取数组对象
    [[self.person mutableArrayValueForKey:@"studentNameArray"] addObject:@"lee"];
    [[self.person mutableArrayValueForKey:@"studentNameArray"] removeObject:@"lee"];
}

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

@end

KVO原理

Automatic key-value observing is implemented using a technique called isa-swizzling.
这段话来自官方文档:KVO是通过isa-swizzling实现的;
那具体是如何isa-swizzling的呢?

1.动态的生成子类:NSKVONotifying_XXX

验证:

######Person
@interface Person : NSObject
@property (nonatomic,copy) NSString *nickName;
@end

######KVOIMPViewController
#import "KVOIMPViewController.h"
#import <objc/runtime.h>
#import "Person.h"

@interface KVOIMPViewController ()
@property (nonatomic,strong) Person *person;

@end

@implementation KVOIMPViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor greenColor];
    
    self.person = [[Person alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    int a ;
}

[self.person addObserver:self forKey...int a两个地方分别打上断点;使用LLDB调试:

(lldb) po object_getClassName(self.person)
"Person"
(lldb) po object_getClassName(self.person)
"NSKVONotifying_Person"

可以看到在运行了addObserver:(NSObject *)observer forKeyPath:...之后,当前Person类变成了NSKVONotifying_Person
上面只能说明生成了NSKVONotifying_Person类但不能说明是Person类的子类;继续验证

- (void)viewDidLoad {
    [super viewDidLoad];
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_Person")];
[self printClasses:[Person class]];
}

#pragma mark - 遍历类以及子类
- (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);
}

打印结果

KTest[10265:237411] classes = (
    Person,
    "NSKVONotifying_Person"
)

可以看到Person类下面确实还有子类NSKVONotifying_Person
内部关系图:

KVO
2.动态子类生重写了很多方法

打印观察前后,类方法的变化

######Person
@interface Person : NSObject
//@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *nickName;
//@property (nonatomic,copy) NSMutableArray *studentNameArray;
@end

######KVOIMPViewController
#import "KVOIMPViewController.h"
#import <objc/runtime.h>
#import "Person.h"
@interface KVOIMPViewController ()
@property (nonatomic,strong) Person *person;
@end

@implementation KVOIMPViewController

- (void)viewDidLoad {
    [super viewDidLoad];    
    self.person = [[Person alloc] init];
    [self printClassAllMethod:NSClassFromString(@"Person")];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_Person")];
}

#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    NSLog(@"*********************%@类的方法list",NSStringFromClass(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(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

打印结果:

KTest[9724:222902] *********************Person类的方法list
KTest[9724:222902] .cxx_destruct-0x106ace0d0
KTest[9724:222902] nickName-0x106ace060
KTest[9724:222902] setNickName:-0x106ace090
KTest[9724:222902] *********************NSKVONotifying_Person类的方法list
KTest[9724:222902] setNickName:-0x7fff25721c7a
KTest[9724:222902] class-0x7fff2572073d
KTest[9724:222902] dealloc-0x7fff257204a2
KTest[9724:222902] _isKVOA-0x7fff2572049a

从打印可以看到:
NSKVONotifying_Person重写了setNickName class dealloc _isKVOA方法;
1.setNickName:
重写的setNickName:方法内部大概这么实现的

@implementation Person
- (void)setNickName:(NSString *)nickName {
    [self willChangeValueForKey:@"nickName"];
    _nickName = nickName;
    [self didChangeValueForKey:@"nickName"];
}
@end

2.class
为何重写class方法:为了让外界感受不到子类NSKVONotifying_Person的生成

  1. dealloc
    重写这个方法应该是为了在对象销毁的时候做一些操作吧,尚未探究
  2. _isKVOA
    _isKVOA:判断是否是KVO生成的类
3.移除观察之后 isa指针是否指回来?

断点调试:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor greenColor];
    
    self.person = [[Person alloc] init];
    NSLog(@"*******添加观察者之前");
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    NSLog(@"*******添加观察者之后");
    [self.person removeObserver:self forKeyPath:@"nickName"];
    NSLog(@"*******移除观察者之后");
}

打印结果:

KTest[10766:251988] *******添加观察者之前
(lldb) po object_getClassName(self.person)
"Person"

KTest[10766:251988] *******添加观察者之后
(lldb) po object_getClassName(self.person)
"NSKVONotifying_Person"

KTest[10766:251988] *******移除观察者之后
(lldb) po object_getClassName(self.person)
"Person"

可以看到,person对象的类又指回了Person

移除观察者后动态子类会被销毁吗?不会。

验证:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[Person alloc] init];
    NSLog(@"*******添加观察者之前");
    [self printClasses:[Person class]];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    NSLog(@"*******添加观察者之后");
    [self printClasses:[Person class]];
    [self.person removeObserver:self forKeyPath:@"nickName"];
    NSLog(@"*******移除观察者之后");
    [self printClasses:[Person class]];
}

#pragma mark - 遍历类以及子类
- (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);
}

打印结果:

KTest[11026:259919] *******添加观察者之前
KTest[11026:259919] classes = (
    Person
)
KTest[11026:259919] *******添加观察者之后
KTest[11026:259919] classes = (
    Person,
    "NSKVONotifying_Person"
)
KTest[11026:259919] *******移除观察者之后
KTest[11026:259919] classes = (
    Person,
    "NSKVONotifying_Person"
)

可以看到在移除观察者之后没有移除动态子类NSKVONotifying_Person

总结

日常注意点

1.context参数的在不使用时推荐填写NULL,其功能:程序更加便捷、高效、安全
2.观察者要移除
3.多个因素影响时可以使用:keyPathsForValuesAffectingValueForKey:
4.针对经常需要改动的代码可以使用:automaticallyNotifiesObserversForKey:方法对key进行选择处理
5.观察可变数组时,要使用mutableArrayValueForKey:

原理

1.KVO通过isa-swizzling实现
2.动态的生成子类:NSKVONotifying_XXX
3.主要是观察setXXX方法;内部重写
4.还重写了setXXX class dealloc _isKVOA方法;
5.移除观察之后 isa指针会重新指回来
6.移除观察者后动态子类会被销毁吗?不会。

上一篇下一篇

猜你喜欢

热点阅读