第四十四篇:iOS使用 runtime 与 响应式编程 KVO 原理

响应式编程 KVO 的原理

1.简单实现 KVO功能

  • 一开始 self.person 的 isa 指针类型为 Person,当控制器被触摸时 self.person.age += 1 (调用了 Person 类的 setAge: 方法), 然后会调用监听方法,如下:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
  • 此时通过段点可查看到 self.person 的 isa 指针为 NSKVONotifying_Person 类型,如图:
    这里写图片描述
//
//  Person.h
//
//  Created by 瞿杰 on 2017/8/16.
//  Copyright © 2017年 iThinkerYZ. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic , assign) NSInteger age ;

@property (nonatomic , copy) NSString * name ;

@end
//
//  Person.m
//
//  Created by 瞿杰 on 2017/8/16.
//  Copyright © 2017年 iThinkerYZ. All rights reserved.
//

#import "Person.h"

@implementation Person

@end
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController
@end
#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@property (nonatomic , strong) Person * person ;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[Person alloc] init];
    self.person.age = 1 ;

    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
}

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

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 实际调用了 setAge: 方法
    self.person.age++ ;
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"最新年龄:%ld , 类名:%@",self.person.age,NSStringFromClass([self.person class]));
}

@end

2.使用 self.person->_age++ 时 KVO 不会调用 observe 对象的 observeValueForKeyPath:…… 方法。

  • 把 Person 对象的 age 属性(默认是 @protected 妨问权限)转变成 @public 的权限

    //
    //  Person.h
    //
    //  Created by 瞿杰 on 2017/8/16.
    //  Copyright © 2017年 qujie. All rights reserved.
    //
    
    
    #import <Foundation/Foundation.h>
    
    
    @interface Person : NSObject
    {
        // 默认是私有变量,把它变成公共变量,外面就可以用 ->_age 这样的格式调用该对象属性,不然会报错
        @public
        NSInteger _age ;
    }
    @property (nonatomic , assign) NSInteger age ;
    
    @property (nonatomic , copy) NSString * name ;
    
    @end
    //
    //  Person.m
    //
    //  Created by 瞿杰 on 2017/8/16.
    //  Copyright © 2017年 qujie. All rights reserved.
    //
    
    
    #import "Person.h"
    
    
    @implementation Person
    
    @end
    
  • 在 ViewController 中注意第 46 行代码的使用如下

    //
    //  ViewController.m
    //
    //  Created by 瞿杰 on 2017/8/16.
    //  Copyright © 2017年 qujie. All rights reserved.
    //
    
    
    #import "ViewController.h"
    
    
    #import "Person.h"
    
    
    #import "NSObject+KVO.h"
    
    
    @interface ViewController ()
    
    @property (nonatomic , strong) Person * person ;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.person = [[Person alloc] init];
        self.person.age = 1 ;
    
        [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
    }
    
    -(void)dealloc
    {
        [self.person removeObserver:self forKeyPath:@"age"];
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
    //    // 实际调用了 setAge: 方法
    //    self.person.age++ ;
    
        // 实际就只是单纯的赋值
        self.person->_age++ ;
    }
    
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"最新年龄:%ld , 类名:%@",self.person.age,NSStringFromClass([self.person class]));
    }
    
    @end
  • 结果却是不会调下面的方法

    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"最新年龄:%ld , 类名:%@",self.person.age,NSStringFromClass([self.person class]));
    }

3.自定义 KVO

  • 以上面的代码为例,系统KVO的实现过程

    • 首先在添加观察者时,用runtime动态创建了一个子类 NSKVONotifying_Person,并修改 self.person 的 isa 指针指向该子类
    • 再用 runtime 动态添加self.person 对象的一些属性如 observe 观察者(strong 强引用,这也解释了为什么每个 dealloc 方法内需要移除监听 )
    • 当设置 self.person.age 时调用 setter 方法触发监听,然后就有了调用 oberve 的方法-(void)observeValueForKeyPath: ……
  • 实现自定义KVO

    • Person 类

      //
      //  Person.h
      //
      //  Created by 瞿杰 on 2017/8/16.
      //  Copyright © 2017年 qujie. All rights reserved.
      //
      
      
      #import <Foundation/Foundation.h>
      
      
      @interface Person : NSObject
      
      @property (nonatomic , assign) NSInteger age ;
      
      @property (nonatomic , copy) NSString * name ;
      
      @end
      
      //
      //  Person.m
      //
      //  Created by 瞿杰 on 2017/8/16.
      //  Copyright © 2017年 qujie. All rights reserved.
      //
      
      
      #import "Person.h"
      
      
      @implementation Person
      
      @end
    • 创建 NSObject 分类 NSObject+KVO 实现添加观察者方法:

      //
      //  NSObject+KVO.h
      //
      //  Created by 瞿杰 on 2017/8/16.
      //  Copyright © 2017年 qujie. All rights reserved.
      //
      
      
      #import <Foundation/Foundation.h>
      
      
      @interface NSObject (KVO)
      
      // 添加一个前缀为区分与系统不一样方法
      -(void)qj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context ;
      
      @end
      
    • 实现 NSObject+KVO 对象方法

      //
      //  NSObject+KVO.m
      //
      //  Created by 瞿杰 on 2017/8/16.
      //  Copyright © 2017年 qujie. All rights reserved.
      //
      
      
      #import "NSObject+KVO.h"
      
      
      // runtime 需要的头文件
      
      #import <objc/message.h>
      
      
      @implementation NSObject (KVO)
      
      -(void)qj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
      {
          // 1. 设置该方法的 当前对象的 isa 指针指向动态创建的 QJKVONotifying_Person 类
          Class subClass = [self createClassWithClassName:"QJKVONotifying_Person" superClassName:[self class]];
          object_setClass(self, subClass);
      
          // 2. 动态添加属性 observer 、keyPath 等
          //    OBJC_ASSOCIATION_RETAIN_NONATOMIC 即在 RAC 环境下等同于 @property (nonatomic , strong) [observer class] * observer ;
          objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
          objc_setAssociatedObject(self, "keyPath", keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
          objc_setAssociatedObject(self, "options", @(options), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
          objc_setAssociatedObject(self, "context", (__bridge id)(context), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
      
          // 3. 方法交换
          // 3.1 获取属性的 setter 方法名
          NSString * methodName = [NSString stringWithFormat:@"set%@%@:",[[keyPath substringToIndex:1] uppercaseString],[keyPath substringFromIndex:1]];
          // 3.2 跟据方法名 获取 方法
          Method InstanceMethod1 = class_getInstanceMethod([self class], NSSelectorFromString(methodName));
          // 3.3 把 setAgeMethod: 方法包装成 Method 类型
          Method InstanceMethod2 = class_getInstanceMethod([self class], @selector(setAgeMethod:));
      
          // 3.4 两个方法交换
          method_exchangeImplementations(InstanceMethod1, InstanceMethod2);
      }
      
      -(void)setAgeMethod:(NSInteger)age
      {
          // 1.取出之前动态添加属性对应的值
          NSObject * observer = objc_getAssociatedObject(self, "observer");
          NSString * keyPath = objc_getAssociatedObject(self, "keyPath");
          void * context = (__bridge void *)(objc_getAssociatedObject(self, "context"));
          NSKeyValueObservingOptions options = [objc_getAssociatedObject(self, @"options") integerValue];
      
          // 2.取出 keyPath 先前的值
          NSInteger oldAge = [[self valueForKeyPath:keyPath] integerValue];
      
          // 3.因为 setAgeMethod: 方法 与 方法名为 methodName 的方法交换了,所以不会告成循环
          [self setAgeMethod:age];
      
          // 4. 调用 KVO 监听的方法
          if ([observer respondsToSelector:@selector(observeValueForKeyPath:ofObject:change:context:)]) {
              NSMutableDictionary * changes = [NSMutableDictionary dictionary];
              [changes setValue:keyPath forKey:@"keyPath"];
      
              // 4.1通过操类型来设置字典的值
              if (options == NSKeyValueObservingOptionOld) {
                  [changes setValue:@(oldAge) forKey:NSKeyValueChangeOldKey];
              }
              else if (options == NSKeyValueObservingOptionNew){
                  [changes setValue:@([[self valueForKeyPath:keyPath] integerValue]) forKey:NSKeyValueChangeNewKey];
              }// 接下来的枚举就不再写了
      
              // 4.2 调用 observer 观察者实现的 KVO 方法
              [observer observeValueForKeyPath:keyPath ofObject:self change:changes context:context];
          }
      }
      
      /**
          跟据类名 className 和 父类名 superClassName 创建名为 className 的类
       */
      -(Class)createClassWithClassName:(const char * )className superClass:(Class)superClass
      {
          // 1.先查找项目中是否存在这个类
          Class subClass = objc_getClass(className);
      
          // 2.如果不存在则动态创建
          if (!subClass)
          {
              // 2.1 根据父类来创建当前的子类
              subClass = objc_allocateClassPair(superClass, className, 0);
          }
      
          return subClass ;
      }
      
      @end
      
    • 在 ViewController 中的用法如下

      //
      //  ViewController.m
      //
      //  Created by 瞿杰 on 2017/8/16.
      //  Copyright © 2017年 qujie. All rights reserved.
      //
      
      
      #import "ViewController.h"
      
      
      #import "Person.h"
      
      
      #import "NSObject+KVO.h"
      
      
      @interface ViewController ()
      
      @property (nonatomic , strong) Person * person ;
      
      @end
      
      @implementation ViewController
      
      - (void)viewDidLoad {
          [super viewDidLoad];
      
          self.person = [[Person alloc] init];
          self.person.age = 1 ;
      
      //    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
          [self.person qj_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
      }
      
      -(void)dealloc
      {
          [self.person removeObserver:self forKeyPath:@"age"];
      }
      
      -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
      {
          // 实际调用了 setAge: 方法
          self.person.age++ ;
      }
      
      -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
      {
          NSLog(@"最新年龄:%ld , 类名:%@",self.person.age,NSStringFromClass([self.person class]));
      }
      
      @end
    • 运行结果如下
      自定义KVO运行结果图

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值