美文网首页
十七、KVO

十七、KVO

作者: Mjs | 来源:发表于2020-10-27 18:13 被阅读0次

    我们可以首先在文档里了解一下。

    kvo都会简单的使用

    @interface LGPerson : NSObject
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, copy) NSString *nick;
    @property (nonatomic, copy) NSString *downloadProgress;
    @property (nonatomic, assign) double writtenData;
    @property (nonatomic, assign) double totalData;
    @property (nonatomic, strong) NSMutableArray *dateArray;
    @property (nonatomic, strong) LGStudent *st;
    @end
    
    
    @interface LGViewController ()
    @property (nonatomic, strong) LGPerson  *person;
    @property (nonatomic, strong) LGStudent *student;
    @end
    
    @interface LGViewController ()
    @property (nonatomic, strong) LGPerson  *person;
    @property (nonatomic, strong) LGStudent *student;
    @end
    
    @implementation LGViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.person  = [LGPerson new];
        self.student = [LGStudent shareInstance];
    
        // 1: context : 上下文
        // 多个对象 - 多个属性
        // 2: 移除观察者
        [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.person.nick = [NSString stringWithFormat:@"%@+",self.person.nick];
    }
    
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        // 性能 + 代码可读性
        NSLog(@"%@",change);
    }
    
    
    - (void)dealloc{
        [self.person removeObserver:self forKeyPath:@"nick" context:NULL];
    
    }
    @end
    

    我们都知道KVO是这样写的,但还是不知道为什么还怎么写

    1. content是什么

    The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing.
    addObserver:forKeyPath:options:context: message中的上下文指针包含将在相应的更改通知中传递回观察者的任意数据。您可以指定NULL并完全依赖于键路径字符串来确定更改通知的来源,但是这种方法可能会给其超类出于不同的原因也在观察相同键路径的对象造成问题。
    一种更安全、更可扩展的方法是使用content来确保接收到的通知是发送给观察者的,而不是超类。
    类中唯一命名的静态变量的地址可以作为一个良好的上下文(content)。在超类或子类中以类似方式选择的上下文将不太可能重叠。您可以为整个类选择一个上下文,并依赖通知消息中的关键路径字符串来确定更改的内容。或者,您可以为观察到的每个键路径创建不同的上下文,这样就完全不必进行字符串比较,从而提高通知解析的效率。

    
    static void *PersonNickContext = &PersonNickContext;
    
    
    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        if (context == PersonNickContext) {
        }
    }
    
    
    2. kvo的移除

    When removing an observer, keep several points in mind:
    Asking to be removed as an observer if not already registered as one results in an NSRangeException. You either call removeObserver:forKeyPath:context: exactly once for the corresponding call to addObserver:forKeyPath:options:context:, or if that is not feasible in your app, place the removeObserver:forKeyPath:context: call inside a try/catch block to process the potential exception.
    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.
    The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in init or viewDidLoad) and unregister during deallocation (usually in dealloc), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.
    当移除观察者时,请记住以下几点:
    如果请求删除未注册的观察者,则会导致NSRangeException异常。你可以只调用removeObserver:forKeyPath:context:一次来对应addObserver:forKeyPath:options:context:,或者如果在你的应用中不可行,在一个try/catch块中放置removeObserver:forKeyPath:context:调用来处理潜在的异常。
    当解除定位时,观察者不会自动移除自己。被观察的对象继续发送通知,而忽略了观察者的状态。然而,与任何其他消息一样,发送给已释放对象的更改通知会触发内存访问异常。因此,可以确保观察者在从内存中消失之前删除它们自己。
    协议没有提供询问对象是观察者还是被观察者的方法。构造您的代码以避免发布相关的错误。一种典型的模式是在观察者初始化期间注册为观察者(例如在init或viewDidLoad中),并在释放期间注销注册(通常在dealloc中),确保正确配对并有序地添加和删除消息,并且在观察者从内存中释放之前注销它。

    简单的理解就是:当对象释放的时候时候,观察者没有移除,被观察的对象会继续发送通知,照成野指针的问题。
    例子

    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.student = [LGStudent shareInstance];//单例
        [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    }
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.student.name = [NSString stringWithFormat:@"%@+",self.person.name];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"%@",change);
    }
    
    

    当我们进入界面,退出在进入,点击屏幕,程序就会崩溃,这就是通知没有移除造成的后果

    3. 手动和自动开关

    观察者默认是自动打开的,我们也可以手动打开首页在观察类中实现

    // 自动开关
    + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
        return NO;
    }
    

    再打开需要观察的对象

    - (void)setNick:(NSString *)nick{
        [self willChangeValueForKey:@"nick"];
        _nick = nick;
        [self didChangeValueForKey:@"nick"];
    }
    
    4. 路径处理
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.person  = [LGPerson new];
        // 4: 路径处理
        // 下载的进度 = 已下载 / 总下载
        [self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
    
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.person.writtenData += 10;
        self.person.totalData  += 1;
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"%@",change);
    }
    
    - (void)dealloc{
    //    [self.person removeObserver:self forKeyPath:@"nick" context:NULL];
        [self.person removeObserver:self forKeyPath:@"downloadProgress"];
    
    }
    
    
    @implementation LGPerson
    
    // 下载进度 -- 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{
        if (self.writtenData == 0) {
            self.writtenData = 10;
        }
        if (self.totalData == 0) {
            self.totalData = 100;
        }
        return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
    }
    
    @end
    
    

    这样就会将downloadProgress的监听改为对 writtenDatatotalData的监听

    5. 数组观察
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.person  = [LGPerson new];
        // 5: 数组观察
        self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
        [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        // KVC 集合 array
        [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"%@",change);
    }
    - (void)dealloc{
        [self.person removeObserver:self forKeyPath:@"dateArray"];
    
    }
    
    

    KVO是建立在KVC基础上的,所以我们要遵循KVC的处理
    注:在打印change的时候会有个kind,kind来源于

    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
        NSKeyValueChangeSetting = 1, //赋值  self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
        NSKeyValueChangeInsertion = 2,//插入 [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
        NSKeyValueChangeRemoval = 3,//移除  [[self.person mutableArrayValueForKey:@"dateArray"] removeAllObjects];
        NSKeyValueChangeReplacement = 4,//替换 [[self.person mutableArrayValueForKey:@"dateArray"] replaceObjectAtIndex:0 withObject:@"1"];
    };
    

    kvo底层探索

    1. kvo只对属性观察,不会对成员变量观察,因为属性实现了setter方法
    2. kvo会生成一个中间类

    Key-Value Observing Implementation Details

    Automatic key-value observing is implemented using a technique called isa-swizzling.
    The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
    When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
    You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

    自动键值观察是使用一种称为isa-swizzling的技术实现的。
    isa指针,顾名思义,指向维护分派表的对象的类。这个分派表本质上包含了指向类实现的方法和其他数据的指针。
    当观察者为对象的属性注册时,被观察对象的isa指针将被修改,指向中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。
    您不应该依赖isa指针来确定类成员关系。相反,您应该使用类方法来确定对象实例的类。

    在通知之前打上断点,在lldb打印p object_getClassName(self.person)

    (const char * _Nonnull) $0 = 0x00000001009ac4cf "LGPerson"
    

    跳过之后再次打印

    (const char * _Nonnull) $2 = 0x000060000383c9e0 "NSKVONotifying_LGPerson"
    

    可以看到LGPerson -> NSKVONotifying_LGPerson

    3.中间类中有什么方法

    实现一个方法打印类中所有的f方法

    #pragma mark - 遍历方法-ivar-property
    - (void)printClassAllMethod:(Class)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);
    }
    

    得到setNickName - class - dealloc - _isKVOA

    4.setter 子类 父类如何改变 nickName 传值

    我们在添加观察者断点处lldb添加watchpoint set variable self->_person->_nickName
    正常运行后,点击屏幕,改变值触发断点

    setter.png
    我们可以看到在点击赋值之间还有三个有关于kvo的方法
        0x109913060 <+592>: callq  0x1099131c1               ; NSKeyValueWillChange
        0x109913065 <+597>: addq   $0x20, %rsp
        0x109913069 <+601>: decq   %r14
        0x10991306c <+604>: jns    0x109913027               ; <+535>
        0x10991306e <+606>: movq   -0x550(%rbp), %rax
        0x109913075 <+613>: movq   %rax, -0x578(%rbp)
        0x10991307c <+620>: movq   -0x548(%rbp), %r14
        0x109913083 <+627>: movq   -0x588(%rbp), %rbx
        0x10991308a <+634>: movq   0x10(%rbp), %rdi
        0x10991308e <+638>: testq  %rdi, %rdi
        0x109913091 <+641>: je     0x109913096               ; <+646>
        0x109913093 <+643>: callq  *0x10(%rdi)
    ->  0x109913096 <+646>: testq  %r14, %r14
        0x109913099 <+649>: jle    0x1099130f9               ; <+745>
        0x10991309b <+651>: leaq   -0x560(%rbp), %rax
        0x1099130a2 <+658>: movq   -0x578(%rbp), %rcx
        0x1099130a9 <+665>: movq   %rcx, (%rax)
        0x1099130ac <+668>: movq   %r14, 0x8(%rax)
        0x1099130b0 <+672>: xorl   %ecx, %ecx
        0x1099130b2 <+674>: movq   %rcx, 0x10(%rax)
        0x1099130b6 <+678>: movq   %rcx, 0x18(%rax)
        0x1099130ba <+682>: movq   %rcx, 0x20(%rax)
        0x1099130be <+686>: movq   -0x568(%rbp), %rcx
        0x1099130c5 <+693>: movq   %rcx, 0x28(%rax)
        0x1099130c9 <+697>: subq   $0x8, %rsp
        0x1099130cd <+701>: leaq   -0x387e(%rip), %rcx       ; NSKeyValueDidChangeBySetting
        0x1099130d4 <+708>: leaq   0x783(%rip), %r9          ; NSKeyValuePopPendingNotificationLocal
        0x1099130db <+715>: movq   -0x580(%rbp), %rdi
        0x1099130e2 <+722>: movl   $0x0, %esi
        0x1099130e7 <+727>: movl   $0x0, %edx
        0x1099130ec <+732>: movq   %rbx, %r8
        0x1099130ef <+735>: pushq  %rax
        0x1099130f0 <+736>: callq  0x1099135fd               ; NSKeyValueDidChange
    

    第一个断点在NSKeyValueWillChangeNSKeyValueDidChange之间进行了对父类的赋值

    5.isa会在移除观察之后指回原来的类

    自定义KVO

    
    static NSString *const kJKVOPrefix = @"JKVONotifying_";
    static NSString *const kJKVOAssiociateKey = @"kJKVO_AssiociateKey";
    
    @interface JInfo : NSObject
    @property(nonatomic, weak)NSObject *observer;
    @property(nonatomic, copy)NSString *keyPath;
    @property(nonatomic, copy)JKVOBlock handleBlock;
    
    @end
    @implementation JInfo
    
    -(instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(JKVOBlock)block{
        if (self = [super init]) {
            _observer = observer;
            _keyPath = keyPath;
            _handleBlock = block;
        }
        return self;;
    }
    
    @end
    
    @implementation NSObject (JKVO)
    
    + (BOOL)j_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL {
        Class cls = self;
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
        
        if (!swiMethod) {
            return NO;
        }
        if (!oriMethod) {
            class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
            method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
        }
        
        BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        if (didAddMethod) {
            class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else{
            method_exchangeImplementations(oriMethod, swiMethod);
        }
        return YES;
    }
    
    -(void)j_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JKVOBlock)block{
        //1.验证是否存在setter方法 : 不让实例进来
        [self judgeSetterMethodFromKeyPath:keyPath];
        //2.动态生成子类
        Class newClass = [self createChildClassWithKeyPath:keyPath];
    
        SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
        Method setterMethod = class_getInstanceMethod([self class], setterSEL);
        const char *setterTypes = method_getTypeEncoding(setterMethod);
        class_addMethod(newClass, setterSEL, (IMP)j_setter, setterTypes);
        //3.isa的指向:JKVONotifying_
        //改变isa指向,地址不变
        object_setClass(self, newClass);
        //2.3.2:添加setter
        //4:保存信息
        JInfo *info = [[JInfo alloc]initWithObserver:observer forKeyPath:keyPath handleBlock:block];
        NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge void * _Nonnull)kJKVOAssiociateKey);
        if (!mArray) {
            mArray = [NSMutableArray arrayWithCapacity:1];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        [mArray addObject:info];
    }
    
    -(void)j_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
        NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJKVOAssiociateKey));
        
        if (observerArr.count<=0) {
            return;
        }
        
        for (JInfo *info in observerArr) {
            if ([info.keyPath isEqualToString:keyPath]) {
                [observerArr removeObject:info];
                objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
                break;
            }
        }
        if (observerArr.count <= 0) {
            //指回给父类
            Class superClass = [self class];
            object_setClass(self, superClass);
        }
    }
    
    #pragma mark - 验证是否存在setter方法
    -(void)judgeSetterMethodFromKeyPath:(NSString*)keyPath{
        Class superClass = object_getClass(self);
        SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
        Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
        if (!setterMethod) {
            @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"没有当前%@的setter",keyPath] userInfo:nil];
        }
    }
    
    #pragma mark - 从get方法获取set方法的名称 key ===>>> setKey:
    static NSString *setterForGetter(NSString *getter){
        if (getter.length <= 0) {
            return nil;
        }
        NSString *firstString = [[getter substringToIndex:1] uppercaseString];
        NSString *leaveString = [getter substringFromIndex:1];
        return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
    }
    #pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
    static NSString *getterForSetter(NSString *setter){
        
        if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
        
        NSRange range = NSMakeRange(3, setter.length-4);
        NSString *getter = [setter substringWithRange:range];
        NSString *firstString = [[getter substringToIndex:1] lowercaseString];
        return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
    }
    
    
    -(Class)createChildClassWithKeyPath:(NSString *)keyPath{
        NSString *oldClassName = NSStringFromClass([self class]);
        NSString *newClassName = [NSString stringWithFormat:@"%@%@",kJKVOPrefix,oldClassName];
        Class newClass = NSClassFromString(newClassName);
        //防止重复创建生成新类
        if (newClass) return newClass;
        /**
         *如果内存不存在,创建生成
         *参数一:父类
         *参数二:新类的名字
         *参数三:新类开辟的额外空间
         */
        //2.1: 申请类
        newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        //2.2:注册类
        objc_registerClassPair(newClass);
        //2.3.1:添加class:class的指向是Person
        SEL classSEL = NSSelectorFromString(@"class");
        Method classMethod = class_getInstanceMethod([self class], classSEL);
        const char *classTypes = method_getTypeEncoding(classMethod);
        class_addMethod(newClass, classSEL, (IMP)j_class, classTypes);
        //2.3.1:添加dealloc
        SEL deallocSEL = NSSelectorFromString(@"dealloc");
        Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
        const char *deallocTypes = method_getTypeEncoding(deallocMethod);
        class_addMethod(newClass, deallocSEL, (IMP)j_dealloc, deallocTypes);
        return newClass;
    }
    
    Class j_class(id self, SEL _cmd){
        return class_getSuperclass(object_getClass(self));
    }
    static void j_dealloc(id self, SEL _cmd){
        NSLog(@"dealloc来了");
        Class superClass = [self class];
        object_setClass(self , superClass);
    }
    static void j_setter(id self, SEL _cmd,id newValue){
        NSLog(@"来了:%@",newValue);
        NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
        id oldValue = [self valueForKey:keyPath];
        //4.消息转发 : 转发给父类
        // 改变父类的值 --- 可以强制类型转换
        void (*j_msgSendSuper)(void *,SEL , id) = (void*)objc_msgSendSuper;
        struct objc_super  superStruct = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self)),
        };
        j_msgSendSuper(&superStruct, _cmd ,newValue);
        
        //5.信息数据回调
        NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJKVOAssiociateKey));
        for (JInfo *info in mArray) {
            if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
                info.handleBlock(info.observer, keyPath, oldValue, newValue);
            }
        }
    }
    @end
    

    demo地址

    相关文章

      网友评论

          本文标题:十七、KVO

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