iOS进阶-11 KVO

作者: ricefun | 来源:发表于2020-02-20 22:43 被阅读0次

    相信读者对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.移除观察者后动态子类会被销毁吗?不会。

    相关文章

      网友评论

        本文标题:iOS进阶-11 KVO

        本文链接:https://www.haomeiwen.com/subject/rggxqhtx.html